From d2ba0e510ace2a60f90a0a5c685a89d6de09757b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Thu, 12 Jun 2025 19:40:54 +0200 Subject: [PATCH 01/23] refactor(rust): Rewrite everything in rust --- .github/workflows/create_release.yml | 139 +++- .github/workflows/create_release.yml.old | 73 -- .github/workflows/create_release_arm64.yml | 38 - .github/workflows/create_release_armv7.yml | 38 - .gitignore | 6 +- Cargo.toml | 20 + Dockerfile | 65 +- Dockerfile.multi | 50 ++ README.md | 150 ++-- api.rs | 386 ++++++++++ entrypoint.sh | 4 +- ntfy.py | 255 ------- ntfy.rs | 832 +++++++++++++++++++++ ntfy_api.py | 207 ----- pyproject.toml | 2 - requirements.txt | 4 - send_discord.py | 94 --- send_gotify.py | 98 --- send_ntfy.py | 98 --- send_slack.py | 131 ---- 20 files changed, 1543 insertions(+), 1147 deletions(-) delete mode 100644 .github/workflows/create_release.yml.old delete mode 100644 .github/workflows/create_release_arm64.yml delete mode 100644 .github/workflows/create_release_armv7.yml create mode 100644 Cargo.toml create mode 100644 Dockerfile.multi create mode 100644 api.rs delete mode 100644 ntfy.py create mode 100644 ntfy.rs delete mode 100644 ntfy_api.py delete mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 send_discord.py delete mode 100644 send_gotify.py delete mode 100644 send_ntfy.py delete mode 100644 send_slack.py diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index d8d60e3..fb26054 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -1,4 +1,4 @@ -name: Docker Build and Release +name: Build et Release Multi-Architectures on: push: @@ -6,25 +6,152 @@ on: - main jobs: - build-and-push-on-docker-hub: + version: runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.tag }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Calculer la prochaine version + id: version + run: | + # Récupérer la dernière version ou utiliser v0.1.0 si aucune n'existe + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.1.0") + echo "Dernière version: $LATEST_TAG" + + # Extraire les composants de version + VERSION=${LATEST_TAG#v} + MAJOR=$(echo $VERSION | cut -d. -f1) + MINOR=$(echo $VERSION | cut -d. -f2) + PATCH=$(echo $VERSION | cut -d. -f3) + + # Incrémenter le patch + PATCH=$((PATCH + 1)) + + # Nouvelle version + NEW_VERSION="v$MAJOR.$MINOR.$PATCH" + echo "Nouvelle version: $NEW_VERSION" + echo "tag=$NEW_VERSION" >> $GITHUB_OUTPUT + + build-binaries: + needs: version + runs-on: ubuntu-latest + strategy: + matrix: + include: + - target: x86_64-unknown-linux-musl + platform: linux/amd64 + name: github-ntfy-amd64 + - target: aarch64-unknown-linux-musl + platform: linux/arm64 + name: github-ntfy-arm64 + - target: armv7-unknown-linux-musleabihf + platform: linux/arm/v7 + name: github-ntfy-armv7 steps: - name: Checkout code uses: actions/checkout@v4 - - name: Set up Docker Buildx + - name: Installer Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.target }} + override: true + + - name: Installer cross + run: cargo install cross + + - name: Construire avec cross + run: | + cross build --release --target ${{ matrix.target }} + + - name: Préparer le binaire + run: | + mkdir -p release + cp target/${{ matrix.target }}/release/github-ntfy release/${{ matrix.name }} + + - name: Upload binaire comme artifact + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.name }} + path: release/${{ matrix.name }} + + docker-build-push: + needs: [version, build-binaries] + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Configurer QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: arm64,arm/v7 + + - name: Configurer Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Log in to Docker Hub + - name: Login Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Build and push Docker image + - name: Télécharger tous les binaires + uses: actions/download-artifact@v3 + with: + path: binaries + + - name: Préparer les binaires pour Docker + run: | + mkdir -p binaries-docker + cp binaries/github-ntfy-amd64/github-ntfy-amd64 binaries-docker/ + cp binaries/github-ntfy-arm64/github-ntfy-arm64 binaries-docker/ + cp binaries/github-ntfy-armv7/github-ntfy-armv7 binaries-docker/ + chmod +x binaries-docker/* + ls -la binaries-docker/ + + # Construire et pousser l'image multi-architecture + - name: Construire et pousser l'image Docker multi-architecture uses: docker/build-push-action@v6 with: context: . push: true - tags: ${{ secrets.DOCKER_USERNAME }}/github-ntfy:latest \ No newline at end of file + platforms: linux/amd64,linux/arm64,linux/arm/v7 + tags: | + ${{ secrets.DOCKER_USERNAME }}/github-ntfy:latest + ${{ secrets.DOCKER_USERNAME }}/github-ntfy:${{ needs.version.outputs.version }} + file: Dockerfile.multi + + create-release: + needs: [version, build-binaries] + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Télécharger tous les binaires + uses: actions/download-artifact@v3 + with: + path: binaries + + - name: Créer une release GitHub + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ needs.version.outputs.version }} + name: Release ${{ needs.version.outputs.version }} + files: | + binaries/github-ntfy-amd64/github-ntfy-amd64 + binaries/github-ntfy-arm64/github-ntfy-arm64 + binaries/github-ntfy-armv7/github-ntfy-armv7 + draft: false + prerelease: false + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/create_release.yml.old b/.github/workflows/create_release.yml.old deleted file mode 100644 index 8640cca..0000000 --- a/.github/workflows/create_release.yml.old +++ /dev/null @@ -1,73 +0,0 @@ -name: Docker Build and Release - -on: - push: - branches: - - main - -jobs: - build-and-push-on-docker-hub: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push Docker image - uses: docker/build-push-action@v6 - with: - context: . - push: true - tags: ${{ secrets.DOCKER_USERNAME }}/github-ntfy:latest - - release-on-github: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Get the latest tag - id: get_latest_tag - run: echo "latest_tag=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_ENV - - - name: Increment version - id: increment_version - run: | - latest_tag=${{ env.latest_tag }} - if [ -z "$latest_tag" ]; then - new_version="v1.5.2" - else - IFS='.' read -r -a version_parts <<< "${latest_tag#v}" - new_version="v${version_parts[0]}.$((version_parts[1] + 1)).0" - fi - echo "new_version=$new_version" >> $GITHUB_ENV - - - name: Read changelog - id: read_changelog - run: echo "changelog=$(base64 -w 0 CHANGELOG.md)" >> $GITHUB_ENV - - - name: Decode changelog - id: decode_changelog - run: echo "${{ env.changelog }}" | base64 -d > decoded_changelog.txt - - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.TOKEN }} - with: - tag_name: ${{ env.new_version }} - release_name: Release ${{ env.new_version }} - body: ${{ steps.decode_changelog.outputs.changelog }} - draft: false - prerelease: false \ No newline at end of file diff --git a/.github/workflows/create_release_arm64.yml b/.github/workflows/create_release_arm64.yml deleted file mode 100644 index 4d3e577..0000000 --- a/.github/workflows/create_release_arm64.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Docker Build and Release for arm64 - -on: - push: - branches: - - main - -jobs: - build-and-push-on-docker-hub: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - with: - platforms: arm64 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - with: - install: true - - - name: Log in to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push Docker image - uses: docker/build-push-action@v6 - with: - context: . - push: true - platforms: linux/arm64 - tags: ${{ secrets.DOCKER_USERNAME }}/github-ntfy:arm64 \ No newline at end of file diff --git a/.github/workflows/create_release_armv7.yml b/.github/workflows/create_release_armv7.yml deleted file mode 100644 index 13ea5d9..0000000 --- a/.github/workflows/create_release_armv7.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Docker Build and Release for armv7 - -on: - push: - branches: - - main - -jobs: - build-and-push-on-docker-hub: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - with: - platforms: arm/v7 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - with: - install: true - - - name: Log in to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push Docker image - uses: docker/build-push-action@v6 - with: - context: . - push: true - platforms: linux/arm/v7 - tags: ${{ secrets.DOCKER_USERNAME }}/github-ntfy:armv7 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 98cb172..d345671 100644 --- a/.gitignore +++ b/.gitignore @@ -405,4 +405,8 @@ docker-compose.yml github-ntfy/ github-ntfy/* -*.db \ No newline at end of file +*.db + +# Rust +target +target/* diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0c8bc28 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "github-ntfy" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "github-ntfy" +path = "ntfy.rs" + +[dependencies] +tokio = { version = "1", features = ["full"] } +reqwest = { version = "0.11", features = ["json", "blocking"] } +rusqlite = { version = "0.29", features = ["bundled"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +log = "0.4" +env_logger = "0.10" +dotenv = "0.15" +chrono = "0.4" +warp = "0.3" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f8e3313..eea38ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,50 @@ -FROM python:3.11.8-alpine3.19 +FROM 1.87.0-alpine3.22 as builder LABEL maintainer="BreizhHardware" LABEL version_number="1.4" -ADD ntfy.py / -ADD ntfy_api.py / -ADD requirements.txt / -ADD entrypoint.sh / -ADD send_ntfy.py / -ADD send_gotify.py / -ADD send_discord.py / -ADD send_slack.py / -ADD index.html /var/www/html/index.html -ADD script.js /var/www/html/script.js -RUN apk add --no-cache sqlite-dev sqlite-libs musl-dev nginx gcc -RUN pip install -r requirements.txt +WORKDIR /app + +# Installation of dependencies +RUN apk add --no-cache sqlite-dev musl-dev openssl-dev pkgconfig + +# Copy of the source files +COPY Cargo.toml Cargo.lock ./ + +# Create a temp source file to pre download dependencies +RUN mkdir src && \ + echo "fn main() {}" > src/main.rs && \ + cargo build --release && \ + rm -rf src + +# Copy real file +COPY ntfy.rs ./src/main.rs +COPY api.rs ./src/api.rs + +# Build the application +RUN cargo build --release + +# Final image +FROM alpine:3.22 + +# Install of runtime dependencies +RUN apk add --no-cache sqlite-libs openssl nginx + +# Copy the static files +COPY index.html /var/www/html/index.html +COPY script.js /var/www/html/script.js + +# Copy the built application from the builder stage +COPY --from=builder /app/target/release/github-ntfy /usr/local/bin/github-ntfy + +# Configure Nginx +COPY nginx.conf /etc/nginx/nginx.conf + +# Copy the entrypoint script +COPY entrypoint.sh / RUN chmod 700 /entrypoint.sh -# Définir les variables d'environnement pour username et password +# Define the working directory ENV USERNAME="" \ PASSWORD="" \ NTFY_URL="" \ @@ -28,12 +55,10 @@ ENV USERNAME="" \ GOTIFY_URL="" \ GOTIFY_TOKEN="" \ DISCORD_WEBHOOK_URL="" \ - SLACK_WEBHOOK_URL="" \ - FLASK_ENV=production + SLACK_WEBHOOK_URL="" + +RUN mkdir -p /github-ntfy && chmod 755 /github-ntfy -# Exposer le port 5000 pour l'API et le port 80 pour le serveur web EXPOSE 5000 80 -COPY nginx.conf /etc/nginx/nginx.conf - -ENTRYPOINT ["/entrypoint.sh"] +ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/Dockerfile.multi b/Dockerfile.multi new file mode 100644 index 0000000..612f5a1 --- /dev/null +++ b/Dockerfile.multi @@ -0,0 +1,50 @@ +FROM alpine:3.22 + +# Installer les dépendances nécessaires +RUN apk add --no-cache sqlite-libs openssl nginx + +# Copier les fichiers web statiques +COPY index.html /var/www/html/index.html +COPY script.js /var/www/html/script.js + +# Copier les binaires compilés en fonction de l'architecture +ARG TARGETARCH +ARG TARGETVARIANT +COPY binaries-docker/github-ntfy-${TARGETARCH}${TARGETVARIANT} /usr/local/bin/github-ntfy + +# Corriger pour arm/v7 +RUN if [ -f /usr/local/bin/github-ntfyv7 ]; then \ + mv /usr/local/bin/github-ntfyv7 /usr/local/bin/github-ntfy; \ + fi + +# Rendre le binaire exécutable +RUN chmod +x /usr/local/bin/github-ntfy + +# Copier la configuration nginx +COPY nginx.conf /etc/nginx/nginx.conf + +# Copier et rendre exécutable le script d'entrée +COPY entrypoint.sh / +RUN chmod 700 /entrypoint.sh + +# Définir les variables d'environnement +ENV USERNAME="" \ + PASSWORD="" \ + NTFY_URL="" \ + GHNTFY_TIMEOUT="3600" \ + GHNTFY_TOKEN="" \ + DOCKER_USERNAME="" \ + DOCKER_PASSWORD="" \ + GOTIFY_URL="" \ + GOTIFY_TOKEN="" \ + DISCORD_WEBHOOK_URL="" \ + SLACK_WEBHOOK_URL="" \ + DB_PATH="/data" + +# Créer le répertoire des données +RUN mkdir -p /data && chmod 755 /data + +# Exposer les ports pour l'API et le serveur web +EXPOSE 8080 80 + +ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/README.md b/README.md index 22c488f..0c31754 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

Welcome to ntfy_alerts 👋

- Version + Version License: GPL--3 @@ -9,19 +9,15 @@

-> This project allows you to receive notifications about new GitHub or Docker Hub releases on ntfy, gotify, and Discord. +> This project allows you to receive notifications about new GitHub or Docker Hub releases on ntfy, gotify, Discord and Slack. Implemented in Rust for better performance. ## Installation -To install the dependencies, run: -```sh -pip install -r requirements.txt -``` +### Docker (recommended) -## Usage +Use our multi-architecture Docker image, which automatically supports amd64, arm64 and armv7: -If you want to use the Docker image, you can use the following docker-compose file for x86_64: -````yaml +```yaml services: github-ntfy: image: breizhhardware/github-ntfy:latest @@ -38,86 +34,80 @@ services: - GOTIFY_TOKEN= # Required if gotify is used - DISCORD_WEBHOOK_URL= # Required if discord is used - SLACK_WEBHOOK_URL= # Required if Slack is used + - DB_PATH=/data # Database path volumes: - - /path/to/github-ntfy:/github-ntfy/ + - /path/to/data:/data ports: - 80:80 restart: unless-stopped -```` -For arm64 this docker compose file is ok: -````yaml -services: - github-ntfy: - image: breizhhardware/github-ntfy:arm64 - container_name: github-ntfy - environment: - - USERNAME=username # Required - - PASSWORD=password # Required - - NTFY_URL=ntfy_url # Required if ntfy is used - - GHNTFY_TIMEOUT=timeout # Default is 3600 (1 hour) - - GHNTFY_TOKEN= # Default is empty (Github token) - - DOCKER_USERNAME= # Default is empty (Docker Hub username) - - DOCKER_PASSWORD= # Default is empty (Docker Hub password) - - GOTIFY_URL=gotify_url # Required if gotify is used - - GOTIFY_TOKEN= # Required if gotify is used - - DISCORD_WEBHOOK_URL= # Required if discord is used - - SLACK_WEBHOOK_URL= # Required if Slack is used - volumes: - - /path/to/github-ntfy:/github-ntfy/ - ports: - - 80:80 - restart: unless-stopped -```` -For armV7 this docker compose is ok: -````yaml -services: - github-ntfy: - image: breizhhardware/github-ntfy:armv7 - container_name: github-ntfy - environment: - - USERNAME=username # Required - - PASSWORD=password # Required - - NTFY_URL=ntfy_url # Required if ntfy is used - - GHNTFY_TIMEOUT=timeout # Default is 3600 (1 hour) - - GHNTFY_TOKEN= # Default is empty (Github token) - - DOCKER_USERNAME= # Default is empty (Docker Hub username) - - DOCKER_PASSWORD= # Default is empty (Docker Hub password) - - GOTIFY_URL=gotify_url # Required if gotify is used - - GOTIFY_TOKEN= # Required if gotify is used - - DISCORD_WEBHOOK_URL= # Required if discord is used - - SLACK_WEBHOOK_URL= # Required if Slack is used - volumes: - - /path/to/github-ntfy:/github-ntfy/ - ports: - - 80:80 - restart: unless-stopped -```` -GHNTFY_TOKEN is a github token, it need to have repo, read:org and read:user +``` + +### Manual Installation +Install Rust if needed +```BASH +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +Clone the repository +```BASH +git clone https://github.com/BreizhHardware/ntfy_alerts.git +cd ntfy_alerts +``` + +Compile +```BASH +cargo build --release +``` + +Run +```BASH +./target/release/github-ntfy +``` + +## REST API +The application exposes a REST API on port 8080 to manage watched repositories: + + +Endpoint +Method +Description +/app_repo +POST +Add a GitHub repository to watch +/app_docker_repo +POST +Add a Docker repository to watch +/watched_repos +GET +List all watched GitHub repositories +/watched_docker_repos +GET +List all watched Docker repositories +/delete_repo +POST +Delete a GitHub repository +/delete_docker_repo +POST +Delete a Docker repository + +## Version Notes +- v2.0: Complete rewrite in Rust for better performance and reduced resource consumption +- v1.5: Stable Python version + +## Configuration +The GitHub token (GHNTFY_TOKEN) needs to have the following permissions: repo, read:org and read:user. ## Author +👤 BreizhHardware -👤 **BreizhHardware** -* Website: https://mrqt.fr?ref=github -* Twitter: [@BreizhHardware](https://twitter.com/BreizhHardware) -* Github: [@BreizhHardware](https://github.com/BreizhHardware) -* LinkedIn: [@félix-marquet-5071bb167](https://linkedin.com/in/félix-marquet-5071bb167) +- Website: [https://mrqt.fr](https://mrqt.fr?ref=github) +- Twitter: [@BreizhHardware](https://twitter.com/BreizhHardware) +- Github: [@BreizhHardware](https://github.com/BreizhHardware) +- LinkedIn: [@félix-marquet-5071bb167](https://linkedin.com/in/félix-marquet-5071bb167) -## Contribution - -If you want to contribut, feel free to open a pull request, but first read the [contribution guide](CONTRIBUTION.md)! - -## TODO: -- [x] Dockerize the ntfy.py -- [x] Add the watched repos list as a parameter -- [x] Add the application version as a database -- [x] Add the watched repos list as a web interface -- [x] Add Docker Hub compatibility -- [ ] Rework of the web interface -- [x] Compatibility with Gotify -- [x] Compatibility with Discord Webhook -- [x] Compatibility and distribution for arm64 and armv7 +## Contributing +Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. But first, please read the [CONTRIBUTION.md](CONTRIBUTION.md) file. ## Show your support - Give a ⭐️ if this project helped you! \ No newline at end of file diff --git a/api.rs b/api.rs new file mode 100644 index 0000000..7028dab --- /dev/null +++ b/api.rs @@ -0,0 +1,386 @@ +use log::{error, info}; +use rusqlite::{Connection, Result as SqliteResult, params}; +use serde_json::json; +use std::env; +use std::sync::Arc; +use tokio::sync::Mutex; +use warp::{Filter, Reply, Rejection}; +use warp::http::StatusCode; +use serde::{Serialize, Deserialize}; +use warp::cors::Cors; + +#[derive(Debug, Serialize, Deserialize)] +struct RepoRequest { + repo: String, +} + +pub async fn start_api() -> Result<(), Box> { + // Open the database + let db_path = env::var("DB_PATH").unwrap_or_else(|_| "./data".to_string()); + std::fs::create_dir_all(&db_path).ok(); + let repos_path = format!("{}/watched_repos.db", db_path); + + match Connection::open(&repos_path) { + Ok(conn) => { + info!("Database connection established successfully"); + let db = Arc::new(Mutex::new(conn)); + + // Route definitions + let add_github = warp::path("app_repo") + .and(warp::post()) + .and(warp::body::json()) + .and(with_db(db.clone())) + .and_then(add_github_repo); + + let add_docker = warp::path("app_docker_repo") + .and(warp::post()) + .and(warp::body::json()) + .and(with_db(db.clone())) + .and_then(add_docker_repo); + + let get_github = warp::path("watched_repos") + .and(warp::get()) + .and(with_db(db.clone())) + .and_then(get_github_repos); + + let get_docker = warp::path("watched_docker_repos") + .and(warp::get()) + .and(with_db(db.clone())) + .and_then(get_docker_repos); + + let delete_github = warp::path("delete_repo") + .and(warp::post()) + .and(warp::body::json()) + .and(with_db(db.clone())) + .and_then(delete_github_repo); + + let delete_docker = warp::path("delete_docker_repo") + .and(warp::post()) + .and(warp::body::json()) + .and(with_db(db.clone())) + .and_then(delete_docker_repo); + + // Configure CORS + let cors = warp::cors() + .allow_any_origin() + .allow_headers(vec!["Content-Type"]) + .allow_methods(vec!["GET", "POST"]); + + // Combine all routes with CORS + let routes = add_github + .or(add_docker) + .or(get_github) + .or(get_docker) + .or(delete_github) + .or(delete_docker) + .with(cors); + + // Start the server + info!("Starting API on 0.0.0.0:5000"); + warp::serve(routes).run(([0, 0, 0, 0], 5000)).await; + Ok(()) + }, + Err(e) => { + error!("Unable to open database: {}", e); + Err(Box::new(e)) + } + } +} + +fn with_db(db: Arc>) -> impl Filter>,), Error = std::convert::Infallible> + Clone { + warp::any().map(move || db.clone()) +} + +async fn add_github_repo(body: RepoRequest, db: Arc>) -> Result { + let repo = body.repo; + + if repo.is_empty() { + return Ok(warp::reply::with_status( + warp::reply::json(&json!({"error": "The 'repo' field is required."})), + StatusCode::BAD_REQUEST + )); + } + + let mut db_guard = db.lock().await; + + // Check if repository already exists + match db_guard.query_row( + "SELECT COUNT(*) FROM watched_repos WHERE repo = ?", + params![repo], + |row| row.get::<_, i64>(0) + ) { + Ok(count) if count > 0 => { + return Ok(warp::reply::with_status( + warp::reply::json(&json!({"error": format!("GitHub repository {} is already in the database.", repo)})), + StatusCode::CONFLICT + )); + }, + Err(e) => { + error!("Error while checking repository: {}", e); + return Ok(warp::reply::with_status( + warp::reply::json(&json!({"error": format!("Database error: {}", e)})), + StatusCode::INTERNAL_SERVER_ERROR + )); + }, + _ => {} + } + + // Add the repository + match db_guard.execute("INSERT INTO watched_repos (repo) VALUES (?)", params![repo]) { + Ok(_) => { + Ok(warp::reply::with_status( + warp::reply::json(&json!({"message": format!("GitHub repository {} has been added to watched repositories.", repo)})), + StatusCode::OK + )) + }, + Err(e) => { + error!("Error while adding repository: {}", e); + Ok(warp::reply::with_status( + warp::reply::json(&json!({"error": format!("Database error: {}", e)})), + StatusCode::INTERNAL_SERVER_ERROR + )) + } + } +} + +async fn add_docker_repo(body: RepoRequest, db: Arc>) -> Result { + let repo = body.repo; + + if repo.is_empty() { + return Ok(warp::reply::with_status( + warp::reply::json(&json!({"error": "The 'repo' field is required."})), + StatusCode::BAD_REQUEST + )); + } + + let mut db_guard = db.lock().await; + + // Check if repository already exists + match db_guard.query_row( + "SELECT COUNT(*) FROM docker_watched_repos WHERE repo = ?", + params![repo], + |row| row.get::<_, i64>(0) + ) { + Ok(count) if count > 0 => { + return Ok(warp::reply::with_status( + warp::reply::json(&json!({"error": format!("Docker repository {} is already in the database.", repo)})), + StatusCode::CONFLICT + )); + }, + Err(e) => { + error!("Error while checking repository: {}", e); + return Ok(warp::reply::with_status( + warp::reply::json(&json!({"error": format!("Database error: {}", e)})), + StatusCode::INTERNAL_SERVER_ERROR + )); + }, + _ => {} + } + + // Add the repository + match db_guard.execute("INSERT INTO docker_watched_repos (repo) VALUES (?)", params![repo]) { + Ok(_) => { + Ok(warp::reply::with_status( + warp::reply::json(&json!({"message": format!("Docker repository {} has been added to watched repositories.", repo)})), + StatusCode::OK + )) + }, + Err(e) => { + error!("Error while adding repository: {}", e); + Ok(warp::reply::with_status( + warp::reply::json(&json!({"error": format!("Database error: {}", e)})), + StatusCode::INTERNAL_SERVER_ERROR + )) + } + } +} + +async fn get_github_repos(db: Arc>) -> Result { + // Solution: collect all results inside the locked block + let repos = { + let db_guard = db.lock().await; + + let mut stmt = match db_guard.prepare("SELECT repo FROM watched_repos") { + Ok(stmt) => stmt, + Err(e) => { + error!("Error while preparing query: {}", e); + return Ok(warp::reply::with_status( + warp::reply::json(&json!({"error": format!("Database error: {}", e)})), + StatusCode::INTERNAL_SERVER_ERROR + )); + } + }; + + let rows = match stmt.query_map([], |row| row.get::<_, String>(0)) { + Ok(rows) => rows, + Err(e) => { + error!("Error while executing query: {}", e); + return Ok(warp::reply::with_status( + warp::reply::json(&json!({"error": format!("Database error: {}", e)})), + StatusCode::INTERNAL_SERVER_ERROR + )); + } + }; + + let mut repos = Vec::new(); + for row in rows { + if let Ok(repo) = row { + repos.push(repo); + } + } + + repos + }; // Lock is released here + + Ok(warp::reply::with_status( + warp::reply::json(&repos), + StatusCode::OK + )) +} + +async fn get_docker_repos(db: Arc>) -> Result { + // Solution: collect all results inside the locked block + let repos = { + let db_guard = db.lock().await; + + let mut stmt = match db_guard.prepare("SELECT repo FROM docker_watched_repos") { + Ok(stmt) => stmt, + Err(e) => { + error!("Error while preparing query: {}", e); + return Ok(warp::reply::with_status( + warp::reply::json(&json!({"error": format!("Database error: {}", e)})), + StatusCode::INTERNAL_SERVER_ERROR + )); + } + }; + + let rows = match stmt.query_map([], |row| row.get::<_, String>(0)) { + Ok(rows) => rows, + Err(e) => { + error!("Error while executing query: {}", e); + return Ok(warp::reply::with_status( + warp::reply::json(&json!({"error": format!("Database error: {}", e)})), + StatusCode::INTERNAL_SERVER_ERROR + )); + } + }; + + let mut repos = Vec::new(); + for row in rows { + if let Ok(repo) = row { + repos.push(repo); + } + } + + repos + }; // Lock is released here + + Ok(warp::reply::with_status( + warp::reply::json(&repos), + StatusCode::OK + )) +} + +async fn delete_github_repo(body: RepoRequest, db: Arc>) -> Result { + let repo = body.repo; + + if repo.is_empty() { + return Ok(warp::reply::with_status( + warp::reply::json(&json!({"error": "The 'repo' field is required."})), + StatusCode::BAD_REQUEST + )); + } + + let mut db_guard = db.lock().await; + + // Check if repository exists + match db_guard.query_row( + "SELECT COUNT(*) FROM watched_repos WHERE repo = ?", + params![repo], + |row| row.get::<_, i64>(0) + ) { + Ok(count) if count == 0 => { + return Ok(warp::reply::with_status( + warp::reply::json(&json!({"error": format!("GitHub repository {} is not in the database.", repo)})), + StatusCode::NOT_FOUND + )); + }, + Err(e) => { + error!("Error while checking repository: {}", e); + return Ok(warp::reply::with_status( + warp::reply::json(&json!({"error": format!("Database error: {}", e)})), + StatusCode::INTERNAL_SERVER_ERROR + )); + }, + _ => {} + } + + // Delete the repository + match db_guard.execute("DELETE FROM watched_repos WHERE repo = ?", params![repo]) { + Ok(_) => { + Ok(warp::reply::with_status( + warp::reply::json(&json!({"message": format!("GitHub repository {} has been removed from watched repositories.", repo)})), + StatusCode::OK + )) + }, + Err(e) => { + error!("Error while deleting repository: {}", e); + Ok(warp::reply::with_status( + warp::reply::json(&json!({"error": format!("Database error: {}", e)})), + StatusCode::INTERNAL_SERVER_ERROR + )) + } + } +} + +async fn delete_docker_repo(body: RepoRequest, db: Arc>) -> Result { + let repo = body.repo; + + if repo.is_empty() { + return Ok(warp::reply::with_status( + warp::reply::json(&json!({"error": "The 'repo' field is required."})), + StatusCode::BAD_REQUEST + )); + } + + let mut db_guard = db.lock().await; + + // Check if repository exists + match db_guard.query_row( + "SELECT COUNT(*) FROM docker_watched_repos WHERE repo = ?", + params![repo], + |row| row.get::<_, i64>(0) + ) { + Ok(count) if count == 0 => { + return Ok(warp::reply::with_status( + warp::reply::json(&json!({"error": format!("Docker repository {} is not in the database.", repo)})), + StatusCode::NOT_FOUND + )); + }, + Err(e) => { + error!("Error while checking repository: {}", e); + return Ok(warp::reply::with_status( + warp::reply::json(&json!({"error": format!("Database error: {}", e)})), + StatusCode::INTERNAL_SERVER_ERROR + )); + }, + _ => {} + } + + // Delete the repository + match db_guard.execute("DELETE FROM docker_watched_repos WHERE repo = ?", params![repo]) { + Ok(_) => { + Ok(warp::reply::with_status( + warp::reply::json(&json!({"message": format!("Docker repository {} has been removed from watched repositories.", repo)})), + StatusCode::OK + )) + }, + Err(e) => { + error!("Error while deleting repository: {}", e); + Ok(warp::reply::with_status( + warp::reply::json(&json!({"error": format!("Database error: {}", e)})), + StatusCode::INTERNAL_SERVER_ERROR + )) + } + } +} \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh index 3940da6..21b9134 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -6,5 +6,5 @@ echo -n "$USERNAME:$PASSWORD" | base64 > /auth.txt # Démarrer nginx en arrière-plan nginx -g 'daemon off;' & -# Exécute le script Python -exec python ./ntfy.py +# Exécute l'application Rust +exec /usr/local/bin/github-ntfy \ No newline at end of file diff --git a/ntfy.py b/ntfy.py deleted file mode 100644 index 420049c..0000000 --- a/ntfy.py +++ /dev/null @@ -1,255 +0,0 @@ -import requests -import time -import os -import logging -import sqlite3 -import subprocess -import json -import threading - -from send_ntfy import ( - github_send_to_ntfy, - docker_send_to_ntfy, -) -from send_gotify import ( - github_send_to_gotify, - docker_send_to_gotify, -) -from send_discord import ( - github_send_to_discord, - docker_send_to_discord, -) - -from send_slack import ( - github_send_to_slack, - docker_send_to_slack, -) - -# Configuring the logger -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", -) -logger = logging.getLogger(__name__) - -github_token = os.environ.get("GHNTFY_TOKEN") -github_headers = {} -if github_token: - github_headers["Authorization"] = f"token {github_token}" - -docker_username = os.environ.get("DOCKER_USERNAME") -docker_password = os.environ.get("DOCKER_PASSWORD") - -discord_webhook_url = os.environ.get("DISCORD_WEBHOOK_URL") - - -def create_dockerhub_token(username, password): - url = "https://hub.docker.com/v2/users/login" - headers = {"Content-Type": "application/json"} - data = json.dumps({"username": username, "password": password}) - - response = requests.post(url, headers=headers, data=data) - - if response.status_code == 200: - token = response.json().get("token") - if token: - return token - else: - logger.error("Failed to get Docker Hub token.") - else: - logger.error(f"Failed to get Docker Hub token. Status code: {response.status_code}") - return None - - -docker_token = create_dockerhub_token(docker_username, docker_password) -docker_header = {} -if docker_token: - docker_header["Authorization"] = f"Bearer {docker_token}" -# Connecting to the database to store previous versions -conn = sqlite3.connect( - "/github-ntfy/ghntfy_versions.db", - check_same_thread=False, -) -cursor = conn.cursor() - -# Creating the table if it does not exist -cursor.execute( - """CREATE TABLE IF NOT EXISTS versions - (repo TEXT PRIMARY KEY, version TEXT, changelog TEXT)""" -) -conn.commit() - -cursor.execute( - """CREATE TABLE IF NOT EXISTS docker_versions - (repo TEXT PRIMARY KEY, digest TEXT)""" -) -conn.commit() - -logger.info("Starting version monitoring...") - -conn2 = sqlite3.connect("/github-ntfy/watched_repos.db", check_same_thread=False) -cursor2 = conn2.cursor() - -cursor2.execute( - """CREATE TABLE IF NOT EXISTS watched_repos - (id INTEGER PRIMARY KEY, repo TEXT)""" -) -conn2.commit() - -cursor2.execute( - """CREATE TABLE IF NOT EXISTS docker_watched_repos - (id INTEGER PRIMARY KEY, repo TEXT)""" -) -conn2.commit() - - -def get_watched_repos(): - cursor2.execute("SELECT * FROM watched_repos") - watched_repos_rows = cursor2.fetchall() - watched_repos = [] - for repo in watched_repos_rows: - watched_repos.append(repo[1]) - return watched_repos - - -def get_docker_watched_repos(): - cursor2.execute("SELECT * FROM docker_watched_repos") - watched_repos_rows = cursor2.fetchall() - watched_repos = [] - for repo in watched_repos_rows: - watched_repos.append(repo[1]) - return watched_repos - - -def start_api(): - subprocess.Popen(["python", "ntfy_api.py"]) - - -def get_latest_releases(watched_repos): - releases = [] - for repo in watched_repos: - url = f"https://api.github.com/repos/{repo}/releases/latest" - response = requests.get(url, headers=github_headers) - if response.status_code == 200: - release_info = response.json() - changelog = get_changelog(repo) - release_date = release_info.get("published_at", "Release date not available") - releases.append( - { - "repo": repo, - "name": release_info["name"], - "tag_name": release_info["tag_name"], - "html_url": release_info["html_url"], - "changelog": changelog, - "published_at": release_date, - } - ) - else: - logger.error(f"Failed to fetch release info for {repo}") - return releases - - -def get_latest_docker_releases(watched_repos): - releases = [] - for repo in watched_repos: - url = f"https://hub.docker.com/v2/repositories/{repo}/tags/latest" - response = requests.get(url, headers=docker_header) - if response.status_code == 200: - release_info = response.json() - release_date = release_info["last_upated"] - digest = release_date["digest"] - releases.append( - { - "repo": repo, - "digest": digest, - "html_url": "https://hub.docker.com/r/" + repo, - "published_at": release_date, - } - ) - else: - logger.error(f"Failed to fetch Docker Hub info for {repo}") - return releases - - -def get_changelog(repo): - url = f"https://api.github.com/repos/{repo}/releases" - response = requests.get(url, headers=github_headers) - if response.status_code == 200: - releases = response.json() - if releases: - latest_release_list = releases[0] - if "body" in latest_release_list: - return latest_release_list["body"] - return "Changelog not available" - -def notify_all_services(github_latest_release, docker_latest_release, auth, ntfy_url, gotify_url, gotify_token, discord_webhook_url, slack_webhook_url): - threads = [] - - if ntfy_url: - if github_latest_release: - threads.append(threading.Thread(target=github_send_to_ntfy, args=(github_latest_release, auth, ntfy_url))) - if docker_latest_release: - threads.append(threading.Thread(target=docker_send_to_ntfy, args=(docker_latest_release, auth, ntfy_url))) - - if gotify_url and gotify_token: - if github_latest_release: - threads.append(threading.Thread(target=github_send_to_gotify, args=(github_latest_release, gotify_token, gotify_url))) - if docker_latest_release: - threads.append(threading.Thread(target=docker_send_to_gotify, args=(docker_latest_release, gotify_token, gotify_url))) - - if discord_webhook_url: - if github_latest_release: - threads.append(threading.Thread(target=github_send_to_discord, args=(github_latest_release, discord_webhook_url))) - if docker_latest_release: - threads.append(threading.Thread(target=docker_send_to_discord, args=(docker_latest_release, discord_webhook_url))) - - if slack_webhook_url: - if github_latest_release: - threads.append(threading.Thread(target=github_send_to_slack, args=(github_latest_release, slack_webhook_url))) - if docker_latest_release: - threads.append(threading.Thread(target=docker_send_to_slack, args=(docker_latest_release, slack_webhook_url))) - - for thread in threads: - thread.start() - - for thread in threads: - thread.join() - - - -if __name__ == "__main__": - start_api() - with open("/auth.txt", "r") as f: - auth = f.read().strip() - ntfy_url = os.environ.get("NTFY_URL") - gotify_url = os.environ.get("GOTIFY_URL") - gotify_token = os.environ.get("GOTIFY_TOKEN") - discord_webhook_url = os.environ.get("DISCORD_WEBHOOK_URL") - timeout = float(os.environ.get("GHNTFY_TIMEOUT")) - slack_webhook_url = os.environ.get("SLACK_WEBHOOK_URL") - - if auth and (ntfy_url or gotify_url or discord_webhook_url): - while True: - github_watched_repos_list = get_watched_repos() - github_latest_release = get_latest_releases(github_watched_repos_list) - docker_watched_repos_list = get_docker_watched_repos() - docker_latest_release = get_latest_docker_releases(docker_watched_repos_list) - - notify_all_services(github_latest_release, docker_latest_release, auth, ntfy_url, gotify_url, gotify_token, discord_webhook_url, slack_webhook_url) - - time.sleep(timeout) - else: - logger.error("Usage: python ntfy.py") - logger.error( - "auth: can be generataed by the folowing command: echo -n 'username:password' | base64 and need to be " - "stored in a file named auth.txt" - ) - logger.error("NTFY_URL: the url of the ntfy server need to be stored in an environment variable named NTFY_URL") - logger.error( - "GOTIFY_URL: the url of the gotify server need to be stored in an environment variable named GOTIFY_URL" - ) - logger.error( - "GOTIFY_TOKEN: the token of the gotify server need to be stored in an environment variable named GOTIFY_TOKEN" - ) - logger.error("DISCORD_WEBHOOK_URL: the webhook URL for Discord notifications need to be stored in an environment variable named DISCORD_WEBHOOK_URL") - logger.error("GHNTFY_TIMEOUT: the time interval between each check") diff --git a/ntfy.rs b/ntfy.rs new file mode 100644 index 0000000..d166bc1 --- /dev/null +++ b/ntfy.rs @@ -0,0 +1,832 @@ +use dotenv::dotenv; +use log::{error, info}; +use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE}; +use rusqlite::{Connection, Result as SqliteResult}; +use serde::{Deserialize}; +use serde_json::json; +use std::env; +use std::fs::File; +use std::io::Read; +use std::thread; +use std::time::Duration; +use tokio::task; +mod api; + +// Structures for GitHub data +#[derive(Debug, Deserialize, Clone)] +struct GithubRelease { + name: String, + tag_name: String, + html_url: String, + published_at: Option, + body: Option, +} + +#[derive(Debug, Clone)] +struct GithubReleaseInfo { + repo: String, + name: String, + tag_name: String, + html_url: String, + changelog: String, + published_at: String, +} + +// Structures for Docker data +#[derive(Debug, Deserialize)] +struct DockerTag { + digest: String, + last_updated: String, +} + +#[derive(Debug, Clone)] +struct DockerReleaseInfo { + repo: String, + digest: String, + html_url: String, + published_at: String, +} + +// Configuration +struct Config { + github_token: Option, + docker_username: Option, + docker_password: Option, + docker_token: Option, + ntfy_url: Option, + gotify_url: Option, + gotify_token: Option, + discord_webhook_url: Option, + slack_webhook_url: Option, + auth: String, + timeout: f64, +} + +impl Config { + fn from_env() -> Self { + dotenv().ok(); + + let docker_username = env::var("DOCKER_USERNAME").ok(); + let docker_password = env::var("DOCKER_PASSWORD").ok(); + let docker_token = if let (Some(username), Some(password)) = (&docker_username, &docker_password) { + create_dockerhub_token(username, password) + } else { + None + }; + + // Read authentication file + let mut auth = String::new(); + if let Ok(mut file) = File::open("/auth.txt") { + file.read_to_string(&mut auth).ok(); + auth = auth.trim().to_string(); + } + + Config { + github_token: env::var("GHNTFY_TOKEN").ok(), + docker_username, + docker_password, + docker_token, + ntfy_url: env::var("NTFY_URL").ok(), + gotify_url: env::var("GOTIFY_URL").ok(), + gotify_token: env::var("GOTIFY_TOKEN").ok(), + discord_webhook_url: env::var("DISCORD_WEBHOOK_URL").ok(), + slack_webhook_url: env::var("SLACK_WEBHOOK_URL").ok(), + auth, + timeout: env::var("GHNTFY_TIMEOUT") + .unwrap_or_else(|_| "3600".to_string()) + .parse() + .unwrap_or(3600.0), + } + } + + fn github_headers(&self) -> HeaderMap { + let mut headers = HeaderMap::new(); + if let Some(token) = &self.github_token { + headers.insert( + AUTHORIZATION, + HeaderValue::from_str(&format!("token {}", token)).unwrap(), + ); + } + headers + } + + fn docker_headers(&self) -> HeaderMap { + let mut headers = HeaderMap::new(); + if let Some(token) = &self.docker_token { + headers.insert( + AUTHORIZATION, + HeaderValue::from_str(&format!("Bearer {}", token)).unwrap(), + ); + } + headers + } +} + +// Functions for DockerHub +fn create_dockerhub_token(username: &str, password: &str) -> Option { + let client = reqwest::blocking::Client::new(); + let mut headers = HeaderMap::new(); + headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/json"), + ); + + let data = json!({ + "username": username, + "password": password + }); + + match client + .post("https://hub.docker.com/v2/users/login") + .headers(headers) + .json(&data) + .send() + { + Ok(response) => { + let status = response.status(); // Store status before consuming response + if status.is_success() { + if let Ok(json) = response.json::() { + return json.get("token").and_then(|t| t.as_str()).map(String::from); + } + } + error!("DockerHub authentication failed: {}", status); + None + } + Err(e) => { + error!("Error connecting to DockerHub: {}", e); + None + } + } +} + +// Database initialization +fn init_databases() -> SqliteResult<(Connection, Connection)> { + let db_path = env::var("DB_PATH").unwrap_or_else(|_| "/github-ntfy".to_string()); + std::fs::create_dir_all(&db_path).ok(); + + let versions_path = format!("{}/ghntfy_versions.db", db_path); + let repos_path = format!("{}/watched_repos.db", db_path); + + let conn = Connection::open(&versions_path)?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS versions ( + repo TEXT PRIMARY KEY, + version TEXT, + changelog TEXT + )", + [], + )?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS docker_versions ( + repo TEXT PRIMARY KEY, + digest TEXT + )", + [], + )?; + + let conn2 = Connection::open(&repos_path)?; + + conn2.execute( + "CREATE TABLE IF NOT EXISTS watched_repos ( + id INTEGER PRIMARY KEY, + repo TEXT + )", + [], + )?; + + conn2.execute( + "CREATE TABLE IF NOT EXISTS docker_watched_repos ( + id INTEGER PRIMARY KEY, + repo TEXT + )", + [], + )?; + + Ok((conn, conn2)) +} + +// Functions to retrieve watched repositories +fn get_watched_repos(conn: &Connection) -> SqliteResult> { + let mut stmt = conn.prepare("SELECT * FROM watched_repos")?; + let repos_iter = stmt.query_map([], |row| Ok(row.get::<_, String>(1)?))?; + + let mut repos = Vec::new(); + for repo in repos_iter { + repos.push(repo?); + } + Ok(repos) +} + +fn get_docker_watched_repos(conn: &Connection) -> SqliteResult> { + let mut stmt = conn.prepare("SELECT * FROM docker_watched_repos")?; + let repos_iter = stmt.query_map([], |row| Ok(row.get::<_, String>(1)?))?; + + let mut repos = Vec::new(); + for repo in repos_iter { + repos.push(repo?); + } + Ok(repos) +} + +// Retrieving latest versions +async fn get_latest_releases( + repos: &[String], + client: &reqwest::Client, + headers: HeaderMap, +) -> Vec { + let mut releases = Vec::new(); + + for repo in repos { + let url = format!("https://api.github.com/repos/{}/releases/latest", repo); + match client.get(&url).headers(headers.clone()).send().await { + Ok(response) => { + if response.status().is_success() { + if let Ok(release_info) = response.json::().await { + let changelog = get_changelog(repo, client, headers.clone()).await; + let published_at = release_info.published_at + .unwrap_or_else(|| "Release date not available".to_string()); + + releases.push(GithubReleaseInfo { + repo: repo.clone(), + name: release_info.name, + tag_name: release_info.tag_name, + html_url: release_info.html_url, + changelog, + published_at, + }); + } + } else { + error!("Failed to retrieve info for {}: {}", repo, response.status()); + } + } + Err(e) => { + error!("Error during request for {}: {}", repo, e); + } + } + } + + releases +} + +async fn get_changelog( + repo: &str, + client: &reqwest::Client, + headers: HeaderMap, +) -> String { + let url = format!("https://api.github.com/repos/{}/releases", repo); + + match client.get(&url).headers(headers).send().await { + Ok(response) => { + if response.status().is_success() { + if let Ok(releases) = response.json::>().await { + if !releases.is_empty() { + if let Some(body) = &releases[0].body { + return body.clone(); + } + } + } + } + } + Err(e) => { + error!("Error retrieving changelog for {}: {}", repo, e); + } + } + + "Changelog not available".to_string() +} + +async fn get_latest_docker_releases( + repos: &[String], + client: &reqwest::Client, + headers: HeaderMap, +) -> Vec { + let mut releases = Vec::new(); + + for repo in repos { + let url = format!("https://hub.docker.com/v2/repositories/{}/tags/latest", repo); + match client.get(&url).headers(headers.clone()).send().await { + Ok(response) => { + if response.status().is_success() { + if let Ok(release_info) = response.json::().await { + releases.push(DockerReleaseInfo { + repo: repo.clone(), + digest: release_info.digest, + html_url: format!("https://hub.docker.com/r/{}", repo), + published_at: release_info.last_updated, + }); + } + } else { + error!("Failed to retrieve Docker info for {}: {}", repo, response.status()); + } + } + Err(e) => { + error!("Error during Docker request for {}: {}", repo, e); + } + } + } + + releases +} + +// Complete notification sending function +async fn send_notifications( + github_releases: Vec, + docker_releases: Vec, + config: &Config, +) { + let mut tasks = Vec::new(); + + // Create tasks for GitHub notifications + for release in &github_releases { + if let Some(url) = &config.ntfy_url { + let release = release.clone(); + let auth = config.auth.clone(); + let url = url.clone(); + tasks.push(task::spawn(async move { + github_send_to_ntfy(release, &auth, &url).await; + })); + } + + if let (Some(gotify_url), Some(gotify_token)) = (&config.gotify_url, &config.gotify_token) { + let release = release.clone(); + let url = gotify_url.clone(); + let token = gotify_token.clone(); + tasks.push(task::spawn(async move { + github_send_to_gotify(release, &token, &url).await; + })); + } + + if let Some(discord_url) = &config.discord_webhook_url { + let release = release.clone(); + let url = discord_url.clone(); + tasks.push(task::spawn(async move { + github_send_to_discord(release, &url).await; + })); + } + + if let Some(slack_url) = &config.slack_webhook_url { + let release = release.clone(); + let url = slack_url.clone(); + tasks.push(task::spawn(async move { + github_send_to_slack(release, &url).await; + })); + } + } + + // Create tasks for Docker notifications + for release in &docker_releases { + if let Some(url) = &config.ntfy_url { + let release = release.clone(); + let auth = config.auth.clone(); + let url = url.clone(); + tasks.push(task::spawn(async move { + docker_send_to_ntfy(release, &auth, &url).await; + })); + } + + if let (Some(gotify_url), Some(gotify_token)) = (&config.gotify_url, &config.gotify_token) { + let release = release.clone(); + let url = gotify_url.clone(); + let token = gotify_token.clone(); + tasks.push(task::spawn(async move { + docker_send_to_gotify(release, &token, &url).await; + })); + } + + if let Some(discord_url) = &config.discord_webhook_url { + let release = release.clone(); + let url = discord_url.clone(); + tasks.push(task::spawn(async move { + docker_send_to_discord(release, &url).await; + })); + } + + if let Some(slack_url) = &config.slack_webhook_url { + let release = release.clone(); + let url = slack_url.clone(); + tasks.push(task::spawn(async move { + docker_send_to_slack(release, &url).await; + })); + } + } + + // Wait for all tasks to complete + for task in tasks { + let _ = task.await; + } +} + +async fn github_send_to_ntfy(release: GithubReleaseInfo, auth: &str, ntfy_url: &str) { + let client = reqwest::Client::new(); + let app_name = release.repo.split('/').last().unwrap_or(&release.repo); + + let mut headers = HeaderMap::new(); + headers.insert("Title", HeaderValue::from_str(&format!("New version for {}", app_name)) + .unwrap_or_else(|_| HeaderValue::from_static(""))); + headers.insert("Priority", HeaderValue::from_static("urgent")); + headers.insert("Markdown", HeaderValue::from_static("yes")); + headers.insert("Actions", HeaderValue::from_str(&format!("view, Update {}, {}, clear=true", app_name, release.html_url)) + .unwrap_or_else(|_| HeaderValue::from_static(""))); + + let message = format!( + "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n```{}```\n\n 🔗 *Release Url*: {}", + release.tag_name, + app_name, + release.published_at.replace("T", " ").replace("Z", ""), + release.changelog, + release.html_url + ); + + match client.post(ntfy_url) + .headers(headers) + .body(message) + .send() + .await + { + Ok(response) if response.status().is_success() => { + info!("Message sent to Ntfy for {}", app_name); + }, + Ok(response) => { + error!("Failed to send message to Ntfy. Status code: {}", response.status()); + }, + Err(e) => { + error!("Error sending to Ntfy: {}", e); + } + } +} + +async fn github_send_to_gotify(release: GithubReleaseInfo, token: &str, gotify_url: &str) { + let client = reqwest::Client::new(); + let app_name = release.repo.split('/').last().unwrap_or(&release.repo); + + let url = format!("{}/message?token={}", gotify_url, token); + + let message = format!( + "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n```{}```\n\n🔗 *Release Url*:{}", + release.tag_name, + app_name, + release.published_at.replace("T", " ").replace("Z", ""), + release.changelog, + release.html_url + ); + + let content = json!({ + "title": format!("New version for {}", app_name), + "message": message, + "priority": "2" + }); + + match client.post(&url) + .json(&content) + .send() + .await + { + Ok(response) if response.status().is_success() => { + info!("Message sent to Gotify for {}", app_name); + }, + Ok(response) => { + error!("Failed to send message to Gotify. Status code: {}", response.status()); + }, + Err(e) => { + error!("Error sending to Gotify: {}", e); + } + } +} + +async fn github_send_to_discord(release: GithubReleaseInfo, webhook_url: &str) { + let client = reqwest::Client::new(); + let app_name = release.repo.split('/').last().unwrap_or(&release.repo); + + let mut message = format!( + "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n```{}```", + release.tag_name, + app_name, + release.published_at.replace("T", " ").replace("Z", ""), + release.changelog + ); + + if message.len() > 2000 { + message = format!( + "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n🔗 *Release Link*: {}", + release.tag_name, + app_name, + release.published_at.replace("T", " ").replace("Z", ""), + release.html_url + ); + } + + let data = json!({ + "content": message, + "username": "GitHub Ntfy" + }); + + let headers = HeaderMap::new(); + + match client.post(webhook_url) + .headers(headers) + .json(&data) + .send() + .await + { + Ok(response) if response.status().is_success() => { + info!("Message sent to Discord for {}", app_name); + }, + Ok(response) => { + error!("Failed to send message to Discord. Status code: {}", response.status()); + }, + Err(e) => { + error!("Error sending to Discord: {}", e); + } + } +} + +async fn github_send_to_slack(release: GithubReleaseInfo, webhook_url: &str) { + let client = reqwest::Client::new(); + let app_name = release.repo.split('/').last().unwrap_or(&release.repo); + + let mut message = format!( + "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n```{}```", + release.tag_name, + app_name, + release.published_at.replace("T", " ").replace("Z", ""), + release.changelog + ); + + if message.len() > 2000 { + message = format!( + "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n `truncated..` use 🔗 instead", + release.tag_name, + app_name, + release.published_at.replace("T", " ").replace("Z", "") + ); + } + + let data = json!({ + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": message + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "🔗 Release Url" + }, + "url": release.html_url, + "action_id": "button-action" + } + }, + { + "type": "divider" + } + ] + }); + + let headers = HeaderMap::from_iter([( + CONTENT_TYPE, + HeaderValue::from_static("application/json") + )]); + + match client.post(webhook_url) + .headers(headers) + .json(&data) + .send() + .await + { + Ok(response) if response.status().is_success() => { + info!("Message sent to Slack for {}", app_name); + }, + Ok(response) => { + error!("Failed to send message to Slack. Status code: {}", response.status()); + }, + Err(e) => { + error!("Error sending to Slack: {}", e); + } + } +} + +async fn docker_send_to_ntfy(release: DockerReleaseInfo, auth: &str, ntfy_url: &str) { + let client = reqwest::Client::new(); + let app_name = release.repo.split('/').last().unwrap_or(&release.repo); + + let mut headers = HeaderMap::new(); + headers.insert("Authorization", HeaderValue::from_str(&format!("Basic {}", auth)) + .unwrap_or_else(|_| HeaderValue::from_static(""))); + headers.insert("Title", HeaderValue::from_str(&format!("🆕 New version for {}", app_name)) + .unwrap_or_else(|_| HeaderValue::from_static(""))); + headers.insert("Priority", HeaderValue::from_static("urgent")); + headers.insert("Markdown", HeaderValue::from_static("yes")); + headers.insert("Actions", HeaderValue::from_str(&format!("View, Update {}, {}, clear=true", app_name, release.html_url)) + .unwrap_or_else(|_| HeaderValue::from_static(""))); + + let message = format!( + "🐳 *Docker Image Updated!*\n\n🔐 *New Digest*: `{}`\n\n📦 *App*: {}\n\n📢 *Published*: {}\n\n 🔗 *Release Url*: {}", + release.digest, + app_name, + release.published_at.replace("T", " ").replace("Z", ""), + release.html_url + ); + + match client.post(ntfy_url) + .headers(headers) + .body(message) + .send() + .await + { + Ok(response) if response.status().is_success() => { + info!("Message sent to Ntfy for {}", app_name); + }, + Ok(response) => { + error!("Failed to send message to Ntfy. Status code: {}", response.status()); + }, + Err(e) => { + error!("Error sending to Ntfy: {}", e); + } + } +} + +async fn docker_send_to_gotify(release: DockerReleaseInfo, token: &str, gotify_url: &str) { + let client = reqwest::Client::new(); + let app_name = release.repo.split('/').last().unwrap_or(&release.repo); + + let url = format!("{}/message?token={}", gotify_url, token); + + let message = format!( + "🐳 *Docker Image Updated!*\n\n🔐 *New Digest*: `{}`\n\n📦 *App*: {}\n\n📢 *Published*: {}\n\n🔗 *Release Url*:{}", + release.digest, + app_name, + release.published_at.replace("T", " ").replace("Z", ""), + release.html_url + ); + + let content = json!({ + "title": format!("New version for {}", app_name), + "message": message, + "priority": "2" + }); + + match client.post(&url) + .json(&content) + .send() + .await + { + Ok(response) if response.status().is_success() => { + info!("Message sent to Gotify for {}", app_name); + }, + Ok(response) => { + error!("Failed to send message to Gotify. Status code: {}", response.status()); + }, + Err(e) => { + error!("Error sending to Gotify: {}", e); + } + } +} + +async fn docker_send_to_discord(release: DockerReleaseInfo, webhook_url: &str) { + let client = reqwest::Client::new(); + let app_name = release.repo.split('/').last().unwrap_or(&release.repo); + + let message = format!( + "🐳 *Docker Image Updated!*\n\n🔐 *New Digest*: `{}`\n\n📦 *App*: {}\n\n📢 *Published*: {}\n\n🔗 *Link*: {}", + release.digest, + app_name, + release.published_at.replace("T", " ").replace("Z", ""), + release.html_url + ); + + let data = json!({ + "content": message, + "username": "GitHub Ntfy" + }); + + match client.post(webhook_url) + .json(&data) + .send() + .await + { + Ok(response) if response.status().is_success() => { + info!("Message sent to Discord for {}", app_name); + }, + Ok(response) => { + error!("Failed to send message to Discord. Status code: {}", response.status()); + }, + Err(e) => { + error!("Error sending to Discord: {}", e); + } + } +} + +async fn docker_send_to_slack(release: DockerReleaseInfo, webhook_url: &str) { + let client = reqwest::Client::new(); + let app_name = release.repo.split('/').last().unwrap_or(&release.repo); + + let message = format!( + "🐳 *Docker Image Updated!*\n\n🔐 *New Digest*: `{}`\n\n📦 *App*: {}\n\n📢*Published*: {}", + release.digest, + app_name, + release.published_at.replace("T", " ").replace("Z", "") + ); + + let data = json!({ + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": message + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "🔗 Release Url" + }, + "url": release.html_url, + "action_id": "button-action" + } + }, + { + "type": "divider" + } + ] + }); + + let headers = HeaderMap::from_iter([( + CONTENT_TYPE, + HeaderValue::from_static("application/json") + )]); + + match client.post(webhook_url) + .headers(headers) + .json(&data) + .send() + .await + { + Ok(response) if response.status().is_success() => { + info!("Message sent to Slack for {}", app_name); + }, + Ok(response) => { + error!("Failed to send message to Slack. Status code: {}", response.status()); + }, + Err(e) => { + error!("Error sending to Slack: {}", e); + } + } +} + +// Function to start the API in a separate thread +fn start_api() { + std::thread::spawn(|| { + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(async { + match api::start_api().await { + Ok(_) => info!("API closed correctly"), + Err(e) => error!("API error: {}", e), + } + }); + }); +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + env_logger::init(); + + let config = Config::from_env(); + let (_conn_versions, conn_repos) = init_databases()?; + + start_api(); + + let client = reqwest::Client::new(); + + if config.auth.is_empty() || (config.ntfy_url.is_none() && config.gotify_url.is_none() + && config.discord_webhook_url.is_none() && config.slack_webhook_url.is_none()) { + error!("Incorrect configuration!"); + error!("auth: can be generated with the command: echo -n 'username:password' | base64"); + error!("NTFY_URL: URL of the ntfy server"); + error!("GOTIFY_URL: URL of the gotify server"); + error!("GOTIFY_TOKEN: Gotify token"); + error!("DISCORD_WEBHOOK_URL: Discord webhook URL"); + error!("SLACK_WEBHOOK_URL: Slack webhook URL"); + error!("GHNTFY_TIMEOUT: interval between checks"); + return Ok(()); + } + + info!("Starting version monitoring..."); + + loop { + let github_repos = get_watched_repos(&conn_repos)?; + let docker_repos = get_docker_watched_repos(&conn_repos)?; + + let github_releases = get_latest_releases(&github_repos, &client, config.github_headers()).await; + let docker_releases = get_latest_docker_releases(&docker_repos, &client, config.docker_headers()).await; + + send_notifications(github_releases, docker_releases, &config).await; + + tokio::time::sleep(Duration::from_secs_f64(config.timeout)).await; + } +} \ No newline at end of file diff --git a/ntfy_api.py b/ntfy_api.py deleted file mode 100644 index 5abe8eb..0000000 --- a/ntfy_api.py +++ /dev/null @@ -1,207 +0,0 @@ -from flask import Flask, request, jsonify -from flask_cors import CORS -import sqlite3 - -app = Flask(__name__) -CORS(app) -app.logger.setLevel("WARNING") - - -def get_db_connection(): - conn = sqlite3.connect("/github-ntfy/watched_repos.db") - conn.row_factory = sqlite3.Row - return conn - - -def close_db_connection(conn): - conn.close() - - -@app.route("/app_repo", methods=["POST"]) -def app_repo(): - data = request.json - repo = data.get("repo") - - # Vérifier si le champ 'repo' est présent dans les données JSON - if not repo: - return ( - jsonify({"error": "The repo field is required."}), - 400, - ) - - # Établir une connexion à la base de données - conn = get_db_connection() - cursor = conn.cursor() - - try: - # Vérifier si le dépôt existe déjà dans la base de données - cursor.execute( - "SELECT * FROM watched_repos WHERE repo=?", - (repo,), - ) - existing_repo = cursor.fetchone() - if existing_repo: - return ( - jsonify({"error": f"The GitHub repo {repo} is already in the database."}), - 409, - ) - - # Ajouter le dépôt à la base de données - cursor.execute( - "INSERT INTO watched_repos (repo) VALUES (?)", - (repo,), - ) - conn.commit() - return jsonify({"message": f"The GitHub repo {repo} as been added to the watched repos."}) - finally: - # Fermer la connexion à la base de données - close_db_connection(conn) - - -@app.route("/app_docker_repo", methods=["POST"]) -def app_docker_repo(): - data = request.json - repo = data.get("repo") - - # Vérifier si le champ 'repo' est présent dans les données JSON - if not repo: - return ( - jsonify({"error": "The repo field is required."}), - 400, - ) - - # Établir une connexion à la base de données - conn = get_db_connection() - cursor = conn.cursor() - - try: - # Vérifier si le dépôt existe déjà dans la base de données - cursor.execute( - "SELECT * FROM docker_watched_repos WHERE repo=?", - (repo,), - ) - existing_repo = cursor.fetchone() - if existing_repo: - return ( - jsonify({"error": f"The Docker repo {repo} is already in the database."}), - 409, - ) - - # Ajouter le dépôt à la base de données - cursor.execute( - "INSERT INTO docker_watched_repos (repo) VALUES (?)", - (repo,), - ) - conn.commit() - return jsonify({"message": f"The Docker repo {repo} as been added to the watched repos."}) - finally: - # Fermer la connexion à la base de données - close_db_connection(conn) - - -@app.route("/watched_repos", methods=["GET"]) -def get_watched_repos(): - db = get_db_connection() - cursor = db.cursor() - cursor.execute("SELECT repo FROM watched_repos") - watched_repos = [repo[0] for repo in cursor.fetchall()] - cursor.close() - db.close() - return jsonify(watched_repos) - - -@app.route("/watched_docker_repos", methods=["GET"]) -def get_watched_docker_repos(): - db = get_db_connection() - cursor = db.cursor() - cursor.execute("SELECT repo FROM docker_watched_repos") - watched_repos = [repo[0] for repo in cursor.fetchall()] - cursor.close() - db.close() - return jsonify(watched_repos) - - -@app.route("/delete_repo", methods=["POST"]) -def delete_repo(): - data = request.json - repo = data.get("repo") - - # Vérifier si le champ 'repo' est présent dans les données JSON - if not repo: - return ( - jsonify({"error": "The repo field is required."}), - 400, - ) - - # Établir une connexion à la base de données - conn = get_db_connection() - cursor = conn.cursor() - - try: - # Vérifier si le dépôt existe dans la base de données - cursor.execute( - "SELECT * FROM watched_repos WHERE repo=?", - (repo,), - ) - existing_repo = cursor.fetchone() - if not existing_repo: - return ( - jsonify({"error": f"The GitHub repo {repo} is not in the database."}), - 404, - ) - - # Supprimer le dépôt de la base de données - cursor.execute( - "DELETE FROM watched_repos WHERE repo=?", - (repo,), - ) - conn.commit() - return jsonify({"message": f"The GitHub repo {repo} as been deleted from the watched repos."}) - finally: - # Fermer la connexion à la base de données - close_db_connection(conn) - - -@app.route("/delete_docker_repo", methods=["POST"]) -def delete_docker_repo(): - data = request.json - repo = data.get("repo") - - # Vérifier si le champ 'repo' est présent dans les données JSON - if not repo: - return ( - jsonify({"error": "The repo field is required."}), - 400, - ) - - # Établir une connexion à la base de données - conn = get_db_connection() - cursor = conn.cursor() - - try: - # Vérifier si le dépôt existe dans la base de données - cursor.execute( - "SELECT * FROM docker_watched_repos WHERE repo=?", - (repo,), - ) - existing_repo = cursor.fetchone() - if not existing_repo: - return ( - jsonify({"error": f"The Docker repo {repo} is not in the database."}), - 404, - ) - - # Supprimer le dépôt de la base de données - cursor.execute( - "DELETE FROM docker_watched_repos WHERE repo=?", - (repo,), - ) - conn.commit() - return jsonify({"message": f"The Docker repo {repo} as been deleted from the watched repos."}) - finally: - # Fermer la connexion à la base de données - close_db_connection(conn) - - -if __name__ == "__main__": - app.run(debug=False) diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index e34796e..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,2 +0,0 @@ -[tool.black] -line-length = 120 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 322bf8e..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -requests==2.31.0 -pysqlite3==0.5.2 -flask==3.0.2 -flask-cors==4.0.0 \ No newline at end of file diff --git a/send_discord.py b/send_discord.py deleted file mode 100644 index 35e882f..0000000 --- a/send_discord.py +++ /dev/null @@ -1,94 +0,0 @@ -import requests -import sqlite3 -import logging - -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", -) -logger = logging.getLogger(__name__) - -def get_db_connection(): - return sqlite3.connect("/github-ntfy/ghntfy_versions.db", check_same_thread=False) - -def github_send_to_discord(releases, webhook_url): - conn = get_db_connection() - cursor = conn.cursor() - for release in releases: - app_name = release["repo"].split("/")[-1] - version_number = release["tag_name"] - app_url = release["html_url"] - changelog = release["changelog"] - release_date = release["published_at"].replace("T", " ").replace("Z", "") - - cursor.execute("SELECT version FROM versions WHERE repo=?", (app_name,)) - previous_version = cursor.fetchone() - if previous_version and previous_version[0] == version_number: - logger.info(f"The version of {app_name} has not changed. No notification sent.") - continue # Move on to the next application - - message = f"📌 *New version*: {version_number}\n\n📦*For*: {app_name}\n\n📅 *Published on*: {release_date}\n\n📝 *Changelog*:\n\n```{changelog}```" - if len(message) > 2000: - message = f"📌 *New version*: {version_number}\n\n📦*For*: {app_name}\n\n📅 *Published on*: {release_date}\n\n🔗 *Release Link*: {app_url}" - # Updating the previous version for this application - cursor.execute( - "INSERT OR REPLACE INTO versions (repo, version, changelog) VALUES (?, ?, ?)", - (app_name, version_number, changelog), - ) - conn.commit() - data = { - "content": message, - "username": "GitHub Ntfy" - } - headers = { - "Content-Type": "application/json" - } - - response = requests.post(webhook_url, json=data, headers=headers) - if 200 <= response.status_code < 300: - logger.info(f"Message sent to Discord for {app_name}") - else: - logger.error(f"Failed to send message to Discord. Status code: {response.status_code}") - logger.error(f"Response: {response.text}") - conn.close() - -def docker_send_to_discord(releases, webhook_url): - conn = get_db_connection() - cursor = conn.cursor() - for release in releases: - app_name = release["repo"].split("/")[-1] - digest_number = release["digest"] - app_url = release["html_url"] - release_date = release["published_at"].replace("T", " ").replace("Z", "") - - cursor.execute("SELECT digest FROM docker_versions WHERE repo=?", (app_name,)) - previous_digest = cursor.fetchone() - if previous_digest and previous_digest[0] == digest_number: - logger.info(f"The digest of {app_name} has not changed. No notification sent.") - continue - - message = f"🐳 *Docker Image Updated!*\n\n🔐 *New Digest*: `{digest_number}`\n\n📦 *App*: {app_name}\n\n📢*Published*: {release_date}\n\n🔗 *Link*: {app_url}" - - cursor.execute( - "INSERT OR REPLACE INTO docker_versions (repo, digest) VALUES (?, ?)", - (app_name, digest_number), - ) - conn.commit() - - data = { - "content": message, - "username": "GitHub Ntfy" - } - headers = { - "Content-Type": "application/json" - } - - logger.info(f"Sending payload to Discord: {data}") - - response = requests.post(webhook_url, json=data, headers=headers) - if 200 <= response.status_code < 300: - logger.info(f"Message sent to Discord for {app_name}") - else: - logger.error(f"Failed to send message to Discord. Status code: {response.status_code}") - logger.error(f"Response: {response.text}") - conn.close() \ No newline at end of file diff --git a/send_gotify.py b/send_gotify.py deleted file mode 100644 index 6cf9a98..0000000 --- a/send_gotify.py +++ /dev/null @@ -1,98 +0,0 @@ -import requests -import sqlite3 -import logging - -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", -) -logger = logging.getLogger(__name__) - -def get_db_connection(): - return sqlite3.connect("/github-ntfy/ghntfy_versions.db", check_same_thread=False) - -def github_send_to_gotify(releases, token, url): - conn = get_db_connection() - cursor = conn.cursor() - url = url + "/message" - url = url + "?token=" + token - for release in releases: - app_name = release["repo"].split("/")[-1] # Getting the application name from the repo - version_number = release["tag_name"] # Getting the version number - app_url = release["html_url"] # Getting the application URL - changelog = release["changelog"] # Getting the changelog - release_date = release["published_at"] # Getting the release date - release_date = release_date.replace("T", " ").replace("Z", "") # Formatting the release date - - # Checking if the version has changed since the last time - cursor.execute( - "SELECT version FROM versions WHERE repo=?", - (app_name,), - ) - previous_version = cursor.fetchone() - if previous_version and previous_version[0] == version_number: - logger.info(f"The version of {app_name} has not changed. No notification sent.") - continue # Move on to the next application - - message = f"📌 *New version*: {version_number}\n\n📦*For*: {app_name}\n\n📅 *Published on*: {release_date}\n\n📝 *Changelog*:\n\n```{changelog}```\n\n🔗 *Release Url*:{app_url}" - # Updating the previous version for this application - cursor.execute( - "INSERT OR REPLACE INTO versions (repo, version, changelog) VALUES (?, ?, ?)", - (app_name, version_number, changelog), - ) - conn.commit() - - content = { - "title": f"New version for {app_name}", - "message": message, - "priority": "2", - } - response = requests.post(url, json=content) - if response.status_code == 200: - logger.info(f"Message sent to Gotify for {app_name}") - continue - else: - logger.error(f"Failed to send message to Gotify. Status code: {response.status_code}") - - -def docker_send_to_gotify(releases, token, url): - conn = get_db_connection() - cursor = conn.cursor() - url = url + "/message" - url = url + "?token=" + token - for release in releases: - app_name = release["repo"].split("/")[-1] # Getting the application name from the repo - digest_number = release["digest"] - app_url = release["html_url"] # Getting the application URL - release_date = release["published_at"] # Getting the release date - release_date = release_date.replace("T", " ").replace("Z", "") # Formatting the release date - - # Checking if the version has changed since the last time - cursor.execute( - "SELECT digest FROM docker_versions WHERE repo=?", - (app_name,), - ) - previous_digest = cursor.fetchone() - if previous_digest and previous_digest[0] == digest_number: - logger.info(f"The digest of {app_name} has not changed. No notification sent.") - continue # Move on to the next application - - message = f"🐳 *Docker Image Updated!*\n\n🔐 *New Digest*: `{digest_number}`\n\n📦 *App*: {app_name}\n\n📢 *Published*: {release_date}\n\n🔗 *Release Url*:{app_url}" - # Updating the previous digest for this application - cursor.execute( - "INSERT OR REPLACE INTO docker_versions (repo, digest) VALUES (?, ?, ?)", - (app_name, digest_number), - ) - conn.commit() - - content = { - "title": f"New version for {app_name}", - "message": message, - "priority": "2", - } - response = requests.post(url, json=content) - if response.status_code == 200: - logger.info(f"Message sent to Gotify for {app_name}") - continue - else: - logger.error(f"Failed to send message to Gotify. Status code: {response.status_code}") diff --git a/send_ntfy.py b/send_ntfy.py deleted file mode 100644 index 3e4395d..0000000 --- a/send_ntfy.py +++ /dev/null @@ -1,98 +0,0 @@ -import requests -import sqlite3 -import logging - -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", -) -logger = logging.getLogger(__name__) - -def get_db_connection(): - return sqlite3.connect("/github-ntfy/ghntfy_versions.db", check_same_thread=False) - -def github_send_to_ntfy(releases, auth, url): - conn = get_db_connection() - cursor = conn.cursor() - for release in releases: - app_name = release["repo"].split("/")[-1] # Getting the application name from the repo - version_number = release["tag_name"] # Getting the version number - app_url = release["html_url"] # Getting the application URL - changelog = release["changelog"] # Getting the changelog - release_date = release["published_at"] # Getting the release date - release_date = release_date.replace("T", " ").replace("Z", "") # Formatting the release date - - # Checking if the version has changed since the last time - cursor.execute( - "SELECT version FROM versions WHERE repo=?", - (app_name,), - ) - previous_version = cursor.fetchone() - if previous_version and previous_version[0] == version_number: - logger.info(f"The version of {app_name} has not changed. No notification sent.") - continue # Move on to the next application - - message = f"📌 *New version*: {version_number}\n\n📦*For*: {app_name}\n\n📅 *Published on*: {release_date}\n\n📝 *Changelog*:\n\n```{changelog}```\n\n 🔗 *Release Url*: {app_url}" - # Updating the previous version for this application - cursor.execute( - "INSERT OR REPLACE INTO versions (repo, version, changelog) VALUES (?, ?, ?)", - (app_name, version_number, changelog), - ) - conn.commit() - - headers = { - "Authorization": f"Basic {auth}", - "Title": f"New version for {app_name}", - "Priority": "urgent", - "Markdown": "yes", - "Actions": f"view, Update {app_name}, {app_url}, clear=true", - } - response = requests.post(f"{url}", headers=headers, data=message) - if response.status_code == 200: - logger.info(f"Message sent to Ntfy for {app_name}") - continue - else: - logger.error(f"Failed to send message to Ntfy. Status code: {response.status_code}") - - -def docker_send_to_ntfy(releases, auth, url): - conn = get_db_connection() - cursor = conn.cursor() - for release in releases: - app_name = release["repo"].split("/")[-1] # Getting the application name from the repo - digest_number = release["digest"] - app_url = release["html_url"] # Getting the application URL - release_date = release["published_at"] # Getting the release date - release_date = release_date.replace("T", " ").replace("Z", "") # Formatting the release date - - # Checking if the version has changed since the last time - cursor.execute( - "SELECT digest FROM docker_versions WHERE repo=?", - (app_name,), - ) - previous_digest = cursor.fetchone() - if previous_digest and previous_digest[0] == digest_number: - logger.info(f"The digest of {app_name} has not changed. No notification sent.") - continue # Move on to the next application - - message = f"🐳 *Docker Image Updated!*\n\n🔐 *New Digest*: `{digest_number}`\n\n📦 *App*: {app_name}\n\n📢*Published*: {release_date}\n\n 🔗 *Release Url*: {app_url}" - # Updating the previous digest for this application - cursor.execute( - "INSERT OR REPLACE INTO docker_versions (repo, digest) VALUES (?, ?, ?)", - (app_name, digest_number), - ) - conn.commit() - - headers = { - "Authorization": f"Basic {auth}", - "Title": f"🆕 New version for {app_name}", - "Priority": "urgent", - "Markdown": "yes", - "Actions": f"View, Update {app_name}, {app_url}, clear=true", - } - response = requests.post(f"{url}", headers=headers, data=message) - if response.status_code == 200: - logger.info(f"Message sent to Ntfy for {app_name}") - continue - else: - logger.error(f"Failed to send message to Ntfy. Status code: {response.status_code}") diff --git a/send_slack.py b/send_slack.py deleted file mode 100644 index a064675..0000000 --- a/send_slack.py +++ /dev/null @@ -1,131 +0,0 @@ -import requests -import sqlite3 -import logging - -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", -) -logger = logging.getLogger(__name__) - -def get_db_connection(): - return sqlite3.connect("/github-ntfy/ghntfy_versions.db", check_same_thread=False) - -def github_send_to_slack(releases, webhook_url): - conn = get_db_connection() - cursor = conn.cursor() - for release in releases: - app_name = release["repo"].split("/")[-1] - version_number = release["tag_name"] - app_url = release["html_url"] - changelog = release["changelog"] - release_date = release["published_at"].replace("T", " ").replace("Z", "") - - cursor.execute("SELECT version FROM versions WHERE repo=?", (app_name,)) - previous_version = cursor.fetchone() - if previous_version and previous_version[0] == version_number: - logger.info(f"The version of {app_name} has not changed. No notification sent.") - continue - - message = f"📌 *New version*: {version_number}\n\n📦*For*: {app_name}\n\n📅 *Published on*: {release_date}\n\n📝 *Changelog*:\n\n```{changelog}```" - if len(message) > 2000: - message = f"📌 *New version*: {version_number}\n\n📦*For*: {app_name}\n\n📅 *Published on*: {release_date}\n\n📝 *Changelog*:\n\n `truncated..` use 🔗 instead " - - cursor.execute( - "INSERT OR REPLACE INTO versions (repo, version, changelog) VALUES (?, ?, ?)", - (app_name, version_number, changelog), - ) - conn.commit() - - - message = { - "blocks": [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": f"{message}" - }, - "accessory": { - "type": "button", - "text": { - "type": "plain_text", - "text": "🔗 Release Url" - }, - "url": f"{app_url}", - "action_id": "button-action" - } - }, - { - "type": "divider" - } - ] - } - headers = { - "Content-Type": "application/json" - } - response = requests.post(webhook_url, json=message, headers=headers) - if response.status_code == 200: - logger.info(f"Message sent to Slack for {app_name}") - else: - logger.error(f"Failed to send message to Slack. Status code: {response.status_code}") - logger.error(f"Response: {response.text}") - conn.close() - -def docker_send_to_slack(releases, webhook_url): - conn = get_db_connection() - cursor = conn.cursor() - for release in releases: - app_name = release["repo"].split("/")[-1] - digest_number = release["digest"] - app_url = release["html_url"] - release_date = release["published_at"].replace("T", " ").replace("Z", "") - - cursor.execute("SELECT digest FROM docker_versions WHERE repo=?", (app_name,)) - previous_digest = cursor.fetchone() - if previous_digest and previous_digest[0] == digest_number: - logger.info(f"The digest of {app_name} has not changed. No notification sent.") - continue - - message = f"🐳 *Docker Image Updated!*\n\n🔐 *New Digest*: `{digest_number}`\n\n📦 *App*: {app_name}\n\n📢*Published*: {release_date}" - - cursor.execute( - "INSERT OR REPLACE INTO docker_versions (repo, digest) VALUES (?, ?)", - (app_name, digest_number), - ) - conn.commit() - - message = { - "blocks": [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": f"{message}" - }, - "accessory": { - "type": "button", - "text": { - "type": "plain_text", - "text": "🔗 Release Url" - }, - "url": f"{app_url}", - "action_id": "button-action" - } - }, - { - "type": "divider" - } - ] - } - headers = { - "Content-Type": "application/json" - } - response = requests.post(webhook_url, json=message, headers=headers) - if 200 <= response.status_code < 300: - logger.info(f"Message sent to Slack for {app_name}") - else: - logger.error(f"Failed to send message to Slack. Status code: {response.status_code}") - logger.error(f"Response: {response.text}") - conn.close() - From 426403ad929f9e5460619b78300dc9a14696f15b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Thu, 12 Jun 2025 19:41:10 +0200 Subject: [PATCH 02/23] refactor(rust): Rewrite everything in rust --- Cargo.lock | 2104 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2104 insertions(+) create mode 100644 Cargo.lock diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..6c164e1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2104 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "github-ntfy" +version = "0.1.0" +dependencies = [ + "chrono", + "dotenv", + "env_logger", + "log", + "reqwest", + "rusqlite", + "serde", + "serde_json", + "tokio", + "warp", +] + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64", + "bytes", + "headers-core", + "http 0.2.12", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http 0.2.12", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown 0.15.4", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libsqlite3-sys" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "multer" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 0.2.12", + "httparse", + "log", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "redox_syscall" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rusqlite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" +dependencies = [ + "bitflags 2.9.1", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "2.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6397daf94fa90f058bd0fd88429dd9e5738999cca8d701813c80723add80462" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.3.1", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "warp" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4378d202ff965b011c64817db11d5829506d3404edeadb61f190d111da3f231c" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "headers", + "http 0.2.12", + "hyper", + "log", + "mime", + "mime_guess", + "multer", + "percent-encoding", + "pin-project", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-tungstenite", + "tokio-util", + "tower-service", + "tracing", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3bfe459f85da17560875b8bf1423d6f113b7a87a5d942e7da0ac71be7c61f8b" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] From cc39b743e66c53b69d391bc5ad2b8861f270a5b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Thu, 12 Jun 2025 19:55:09 +0200 Subject: [PATCH 03/23] refactor(rust): restructure project and update configuration management --- .github/workflows/create_dev.yml | 96 ++++ .github/workflows/create_release.yml | 2 +- Cargo.toml | 4 +- Dockerfile | 3 +- ntfy.rs | 832 --------------------------- api.rs => src/api.rs | 0 src/config.rs | 81 +++ src/database.rs | 73 +++ src/docker.rs | 73 +++ src/github.rs | 67 +++ src/main.rs | 64 +++ src/models.rs | 36 ++ src/notifications/discord.rs | 78 +++ src/notifications/docker.rs | 53 ++ src/notifications/github.rs | 133 +++++ src/notifications/gotify.rs | 78 +++ src/notifications/mod.rs | 97 ++++ src/notifications/ntfy.rs | 84 +++ src/notifications/slack.rs | 131 +++++ 19 files changed, 1148 insertions(+), 837 deletions(-) create mode 100644 .github/workflows/create_dev.yml delete mode 100644 ntfy.rs rename api.rs => src/api.rs (100%) create mode 100644 src/config.rs create mode 100644 src/database.rs create mode 100644 src/docker.rs create mode 100644 src/github.rs create mode 100644 src/main.rs create mode 100644 src/models.rs create mode 100644 src/notifications/discord.rs create mode 100644 src/notifications/docker.rs create mode 100644 src/notifications/github.rs create mode 100644 src/notifications/gotify.rs create mode 100644 src/notifications/mod.rs create mode 100644 src/notifications/ntfy.rs create mode 100644 src/notifications/slack.rs diff --git a/.github/workflows/create_dev.yml b/.github/workflows/create_dev.yml new file mode 100644 index 0000000..58f261a --- /dev/null +++ b/.github/workflows/create_dev.yml @@ -0,0 +1,96 @@ +name: Build et Push Docker Dev Image + +on: + push: + branches: + - dev + +jobs: + build-binaries: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - target: x86_64-unknown-linux-musl + platform: linux/amd64 + name: github-ntfy-amd64 + - target: aarch64-unknown-linux-musl + platform: linux/arm64 + name: github-ntfy-arm64 + - target: armv7-unknown-linux-musleabihf + platform: linux/arm/v7 + name: github-ntfy-armv7 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Installer Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.target }} + override: true + + - name: Installer cross + run: cargo install cross + + - name: Construire avec cross + run: | + cross build --release --target ${{ matrix.target }} + + - name: Préparer le binaire + run: | + mkdir -p release + cp target/${{ matrix.target }}/release/github-ntfy release/${{ matrix.name }} + + - name: Upload binaire comme artifact + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.name }} + path: release/${{ matrix.name }} + + docker-build-push: + needs: [build-binaries] + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Configurer QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: arm64,arm/v7 + + - name: Configurer Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Télécharger tous les binaires + uses: actions/download-artifact@v3 + with: + path: binaries + + - name: Préparer les binaires pour Docker + run: | + mkdir -p binaries-docker + cp binaries/github-ntfy-amd64/github-ntfy-amd64 binaries-docker/ + cp binaries/github-ntfy-arm64/github-ntfy-arm64 binaries-docker/ + cp binaries/github-ntfy-armv7/github-ntfy-armv7 binaries-docker/ + chmod +x binaries-docker/* + ls -la binaries-docker/ + + # Construire et pousser l'image Docker dev + - name: Construire et pousser l'image Docker dev + uses: docker/build-push-action@v6 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64,linux/arm/v7 + tags: ${{ secrets.DOCKER_USERNAME }}/github-ntfy:dev + file: Dockerfile.multi \ No newline at end of file diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index fb26054..330ccf5 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -154,4 +154,4 @@ jobs: prerelease: false generate_release_notes: true env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.TOKEN }} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 0c8bc28..854ce9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "github-ntfy" -version = "0.1.0" +version = "2.0.0" edition = "2021" [[bin]] name = "github-ntfy" -path = "ntfy.rs" +path = "src/main.rs" [dependencies] tokio = { version = "1", features = ["full"] } diff --git a/Dockerfile b/Dockerfile index eea38ee..42bb1c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,8 +18,7 @@ RUN mkdir src && \ rm -rf src # Copy real file -COPY ntfy.rs ./src/main.rs -COPY api.rs ./src/api.rs +COPY src/ ./src/ # Build the application RUN cargo build --release diff --git a/ntfy.rs b/ntfy.rs deleted file mode 100644 index d166bc1..0000000 --- a/ntfy.rs +++ /dev/null @@ -1,832 +0,0 @@ -use dotenv::dotenv; -use log::{error, info}; -use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE}; -use rusqlite::{Connection, Result as SqliteResult}; -use serde::{Deserialize}; -use serde_json::json; -use std::env; -use std::fs::File; -use std::io::Read; -use std::thread; -use std::time::Duration; -use tokio::task; -mod api; - -// Structures for GitHub data -#[derive(Debug, Deserialize, Clone)] -struct GithubRelease { - name: String, - tag_name: String, - html_url: String, - published_at: Option, - body: Option, -} - -#[derive(Debug, Clone)] -struct GithubReleaseInfo { - repo: String, - name: String, - tag_name: String, - html_url: String, - changelog: String, - published_at: String, -} - -// Structures for Docker data -#[derive(Debug, Deserialize)] -struct DockerTag { - digest: String, - last_updated: String, -} - -#[derive(Debug, Clone)] -struct DockerReleaseInfo { - repo: String, - digest: String, - html_url: String, - published_at: String, -} - -// Configuration -struct Config { - github_token: Option, - docker_username: Option, - docker_password: Option, - docker_token: Option, - ntfy_url: Option, - gotify_url: Option, - gotify_token: Option, - discord_webhook_url: Option, - slack_webhook_url: Option, - auth: String, - timeout: f64, -} - -impl Config { - fn from_env() -> Self { - dotenv().ok(); - - let docker_username = env::var("DOCKER_USERNAME").ok(); - let docker_password = env::var("DOCKER_PASSWORD").ok(); - let docker_token = if let (Some(username), Some(password)) = (&docker_username, &docker_password) { - create_dockerhub_token(username, password) - } else { - None - }; - - // Read authentication file - let mut auth = String::new(); - if let Ok(mut file) = File::open("/auth.txt") { - file.read_to_string(&mut auth).ok(); - auth = auth.trim().to_string(); - } - - Config { - github_token: env::var("GHNTFY_TOKEN").ok(), - docker_username, - docker_password, - docker_token, - ntfy_url: env::var("NTFY_URL").ok(), - gotify_url: env::var("GOTIFY_URL").ok(), - gotify_token: env::var("GOTIFY_TOKEN").ok(), - discord_webhook_url: env::var("DISCORD_WEBHOOK_URL").ok(), - slack_webhook_url: env::var("SLACK_WEBHOOK_URL").ok(), - auth, - timeout: env::var("GHNTFY_TIMEOUT") - .unwrap_or_else(|_| "3600".to_string()) - .parse() - .unwrap_or(3600.0), - } - } - - fn github_headers(&self) -> HeaderMap { - let mut headers = HeaderMap::new(); - if let Some(token) = &self.github_token { - headers.insert( - AUTHORIZATION, - HeaderValue::from_str(&format!("token {}", token)).unwrap(), - ); - } - headers - } - - fn docker_headers(&self) -> HeaderMap { - let mut headers = HeaderMap::new(); - if let Some(token) = &self.docker_token { - headers.insert( - AUTHORIZATION, - HeaderValue::from_str(&format!("Bearer {}", token)).unwrap(), - ); - } - headers - } -} - -// Functions for DockerHub -fn create_dockerhub_token(username: &str, password: &str) -> Option { - let client = reqwest::blocking::Client::new(); - let mut headers = HeaderMap::new(); - headers.insert( - CONTENT_TYPE, - HeaderValue::from_static("application/json"), - ); - - let data = json!({ - "username": username, - "password": password - }); - - match client - .post("https://hub.docker.com/v2/users/login") - .headers(headers) - .json(&data) - .send() - { - Ok(response) => { - let status = response.status(); // Store status before consuming response - if status.is_success() { - if let Ok(json) = response.json::() { - return json.get("token").and_then(|t| t.as_str()).map(String::from); - } - } - error!("DockerHub authentication failed: {}", status); - None - } - Err(e) => { - error!("Error connecting to DockerHub: {}", e); - None - } - } -} - -// Database initialization -fn init_databases() -> SqliteResult<(Connection, Connection)> { - let db_path = env::var("DB_PATH").unwrap_or_else(|_| "/github-ntfy".to_string()); - std::fs::create_dir_all(&db_path).ok(); - - let versions_path = format!("{}/ghntfy_versions.db", db_path); - let repos_path = format!("{}/watched_repos.db", db_path); - - let conn = Connection::open(&versions_path)?; - - conn.execute( - "CREATE TABLE IF NOT EXISTS versions ( - repo TEXT PRIMARY KEY, - version TEXT, - changelog TEXT - )", - [], - )?; - - conn.execute( - "CREATE TABLE IF NOT EXISTS docker_versions ( - repo TEXT PRIMARY KEY, - digest TEXT - )", - [], - )?; - - let conn2 = Connection::open(&repos_path)?; - - conn2.execute( - "CREATE TABLE IF NOT EXISTS watched_repos ( - id INTEGER PRIMARY KEY, - repo TEXT - )", - [], - )?; - - conn2.execute( - "CREATE TABLE IF NOT EXISTS docker_watched_repos ( - id INTEGER PRIMARY KEY, - repo TEXT - )", - [], - )?; - - Ok((conn, conn2)) -} - -// Functions to retrieve watched repositories -fn get_watched_repos(conn: &Connection) -> SqliteResult> { - let mut stmt = conn.prepare("SELECT * FROM watched_repos")?; - let repos_iter = stmt.query_map([], |row| Ok(row.get::<_, String>(1)?))?; - - let mut repos = Vec::new(); - for repo in repos_iter { - repos.push(repo?); - } - Ok(repos) -} - -fn get_docker_watched_repos(conn: &Connection) -> SqliteResult> { - let mut stmt = conn.prepare("SELECT * FROM docker_watched_repos")?; - let repos_iter = stmt.query_map([], |row| Ok(row.get::<_, String>(1)?))?; - - let mut repos = Vec::new(); - for repo in repos_iter { - repos.push(repo?); - } - Ok(repos) -} - -// Retrieving latest versions -async fn get_latest_releases( - repos: &[String], - client: &reqwest::Client, - headers: HeaderMap, -) -> Vec { - let mut releases = Vec::new(); - - for repo in repos { - let url = format!("https://api.github.com/repos/{}/releases/latest", repo); - match client.get(&url).headers(headers.clone()).send().await { - Ok(response) => { - if response.status().is_success() { - if let Ok(release_info) = response.json::().await { - let changelog = get_changelog(repo, client, headers.clone()).await; - let published_at = release_info.published_at - .unwrap_or_else(|| "Release date not available".to_string()); - - releases.push(GithubReleaseInfo { - repo: repo.clone(), - name: release_info.name, - tag_name: release_info.tag_name, - html_url: release_info.html_url, - changelog, - published_at, - }); - } - } else { - error!("Failed to retrieve info for {}: {}", repo, response.status()); - } - } - Err(e) => { - error!("Error during request for {}: {}", repo, e); - } - } - } - - releases -} - -async fn get_changelog( - repo: &str, - client: &reqwest::Client, - headers: HeaderMap, -) -> String { - let url = format!("https://api.github.com/repos/{}/releases", repo); - - match client.get(&url).headers(headers).send().await { - Ok(response) => { - if response.status().is_success() { - if let Ok(releases) = response.json::>().await { - if !releases.is_empty() { - if let Some(body) = &releases[0].body { - return body.clone(); - } - } - } - } - } - Err(e) => { - error!("Error retrieving changelog for {}: {}", repo, e); - } - } - - "Changelog not available".to_string() -} - -async fn get_latest_docker_releases( - repos: &[String], - client: &reqwest::Client, - headers: HeaderMap, -) -> Vec { - let mut releases = Vec::new(); - - for repo in repos { - let url = format!("https://hub.docker.com/v2/repositories/{}/tags/latest", repo); - match client.get(&url).headers(headers.clone()).send().await { - Ok(response) => { - if response.status().is_success() { - if let Ok(release_info) = response.json::().await { - releases.push(DockerReleaseInfo { - repo: repo.clone(), - digest: release_info.digest, - html_url: format!("https://hub.docker.com/r/{}", repo), - published_at: release_info.last_updated, - }); - } - } else { - error!("Failed to retrieve Docker info for {}: {}", repo, response.status()); - } - } - Err(e) => { - error!("Error during Docker request for {}: {}", repo, e); - } - } - } - - releases -} - -// Complete notification sending function -async fn send_notifications( - github_releases: Vec, - docker_releases: Vec, - config: &Config, -) { - let mut tasks = Vec::new(); - - // Create tasks for GitHub notifications - for release in &github_releases { - if let Some(url) = &config.ntfy_url { - let release = release.clone(); - let auth = config.auth.clone(); - let url = url.clone(); - tasks.push(task::spawn(async move { - github_send_to_ntfy(release, &auth, &url).await; - })); - } - - if let (Some(gotify_url), Some(gotify_token)) = (&config.gotify_url, &config.gotify_token) { - let release = release.clone(); - let url = gotify_url.clone(); - let token = gotify_token.clone(); - tasks.push(task::spawn(async move { - github_send_to_gotify(release, &token, &url).await; - })); - } - - if let Some(discord_url) = &config.discord_webhook_url { - let release = release.clone(); - let url = discord_url.clone(); - tasks.push(task::spawn(async move { - github_send_to_discord(release, &url).await; - })); - } - - if let Some(slack_url) = &config.slack_webhook_url { - let release = release.clone(); - let url = slack_url.clone(); - tasks.push(task::spawn(async move { - github_send_to_slack(release, &url).await; - })); - } - } - - // Create tasks for Docker notifications - for release in &docker_releases { - if let Some(url) = &config.ntfy_url { - let release = release.clone(); - let auth = config.auth.clone(); - let url = url.clone(); - tasks.push(task::spawn(async move { - docker_send_to_ntfy(release, &auth, &url).await; - })); - } - - if let (Some(gotify_url), Some(gotify_token)) = (&config.gotify_url, &config.gotify_token) { - let release = release.clone(); - let url = gotify_url.clone(); - let token = gotify_token.clone(); - tasks.push(task::spawn(async move { - docker_send_to_gotify(release, &token, &url).await; - })); - } - - if let Some(discord_url) = &config.discord_webhook_url { - let release = release.clone(); - let url = discord_url.clone(); - tasks.push(task::spawn(async move { - docker_send_to_discord(release, &url).await; - })); - } - - if let Some(slack_url) = &config.slack_webhook_url { - let release = release.clone(); - let url = slack_url.clone(); - tasks.push(task::spawn(async move { - docker_send_to_slack(release, &url).await; - })); - } - } - - // Wait for all tasks to complete - for task in tasks { - let _ = task.await; - } -} - -async fn github_send_to_ntfy(release: GithubReleaseInfo, auth: &str, ntfy_url: &str) { - let client = reqwest::Client::new(); - let app_name = release.repo.split('/').last().unwrap_or(&release.repo); - - let mut headers = HeaderMap::new(); - headers.insert("Title", HeaderValue::from_str(&format!("New version for {}", app_name)) - .unwrap_or_else(|_| HeaderValue::from_static(""))); - headers.insert("Priority", HeaderValue::from_static("urgent")); - headers.insert("Markdown", HeaderValue::from_static("yes")); - headers.insert("Actions", HeaderValue::from_str(&format!("view, Update {}, {}, clear=true", app_name, release.html_url)) - .unwrap_or_else(|_| HeaderValue::from_static(""))); - - let message = format!( - "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n```{}```\n\n 🔗 *Release Url*: {}", - release.tag_name, - app_name, - release.published_at.replace("T", " ").replace("Z", ""), - release.changelog, - release.html_url - ); - - match client.post(ntfy_url) - .headers(headers) - .body(message) - .send() - .await - { - Ok(response) if response.status().is_success() => { - info!("Message sent to Ntfy for {}", app_name); - }, - Ok(response) => { - error!("Failed to send message to Ntfy. Status code: {}", response.status()); - }, - Err(e) => { - error!("Error sending to Ntfy: {}", e); - } - } -} - -async fn github_send_to_gotify(release: GithubReleaseInfo, token: &str, gotify_url: &str) { - let client = reqwest::Client::new(); - let app_name = release.repo.split('/').last().unwrap_or(&release.repo); - - let url = format!("{}/message?token={}", gotify_url, token); - - let message = format!( - "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n```{}```\n\n🔗 *Release Url*:{}", - release.tag_name, - app_name, - release.published_at.replace("T", " ").replace("Z", ""), - release.changelog, - release.html_url - ); - - let content = json!({ - "title": format!("New version for {}", app_name), - "message": message, - "priority": "2" - }); - - match client.post(&url) - .json(&content) - .send() - .await - { - Ok(response) if response.status().is_success() => { - info!("Message sent to Gotify for {}", app_name); - }, - Ok(response) => { - error!("Failed to send message to Gotify. Status code: {}", response.status()); - }, - Err(e) => { - error!("Error sending to Gotify: {}", e); - } - } -} - -async fn github_send_to_discord(release: GithubReleaseInfo, webhook_url: &str) { - let client = reqwest::Client::new(); - let app_name = release.repo.split('/').last().unwrap_or(&release.repo); - - let mut message = format!( - "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n```{}```", - release.tag_name, - app_name, - release.published_at.replace("T", " ").replace("Z", ""), - release.changelog - ); - - if message.len() > 2000 { - message = format!( - "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n🔗 *Release Link*: {}", - release.tag_name, - app_name, - release.published_at.replace("T", " ").replace("Z", ""), - release.html_url - ); - } - - let data = json!({ - "content": message, - "username": "GitHub Ntfy" - }); - - let headers = HeaderMap::new(); - - match client.post(webhook_url) - .headers(headers) - .json(&data) - .send() - .await - { - Ok(response) if response.status().is_success() => { - info!("Message sent to Discord for {}", app_name); - }, - Ok(response) => { - error!("Failed to send message to Discord. Status code: {}", response.status()); - }, - Err(e) => { - error!("Error sending to Discord: {}", e); - } - } -} - -async fn github_send_to_slack(release: GithubReleaseInfo, webhook_url: &str) { - let client = reqwest::Client::new(); - let app_name = release.repo.split('/').last().unwrap_or(&release.repo); - - let mut message = format!( - "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n```{}```", - release.tag_name, - app_name, - release.published_at.replace("T", " ").replace("Z", ""), - release.changelog - ); - - if message.len() > 2000 { - message = format!( - "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n `truncated..` use 🔗 instead", - release.tag_name, - app_name, - release.published_at.replace("T", " ").replace("Z", "") - ); - } - - let data = json!({ - "blocks": [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": message - }, - "accessory": { - "type": "button", - "text": { - "type": "plain_text", - "text": "🔗 Release Url" - }, - "url": release.html_url, - "action_id": "button-action" - } - }, - { - "type": "divider" - } - ] - }); - - let headers = HeaderMap::from_iter([( - CONTENT_TYPE, - HeaderValue::from_static("application/json") - )]); - - match client.post(webhook_url) - .headers(headers) - .json(&data) - .send() - .await - { - Ok(response) if response.status().is_success() => { - info!("Message sent to Slack for {}", app_name); - }, - Ok(response) => { - error!("Failed to send message to Slack. Status code: {}", response.status()); - }, - Err(e) => { - error!("Error sending to Slack: {}", e); - } - } -} - -async fn docker_send_to_ntfy(release: DockerReleaseInfo, auth: &str, ntfy_url: &str) { - let client = reqwest::Client::new(); - let app_name = release.repo.split('/').last().unwrap_or(&release.repo); - - let mut headers = HeaderMap::new(); - headers.insert("Authorization", HeaderValue::from_str(&format!("Basic {}", auth)) - .unwrap_or_else(|_| HeaderValue::from_static(""))); - headers.insert("Title", HeaderValue::from_str(&format!("🆕 New version for {}", app_name)) - .unwrap_or_else(|_| HeaderValue::from_static(""))); - headers.insert("Priority", HeaderValue::from_static("urgent")); - headers.insert("Markdown", HeaderValue::from_static("yes")); - headers.insert("Actions", HeaderValue::from_str(&format!("View, Update {}, {}, clear=true", app_name, release.html_url)) - .unwrap_or_else(|_| HeaderValue::from_static(""))); - - let message = format!( - "🐳 *Docker Image Updated!*\n\n🔐 *New Digest*: `{}`\n\n📦 *App*: {}\n\n📢 *Published*: {}\n\n 🔗 *Release Url*: {}", - release.digest, - app_name, - release.published_at.replace("T", " ").replace("Z", ""), - release.html_url - ); - - match client.post(ntfy_url) - .headers(headers) - .body(message) - .send() - .await - { - Ok(response) if response.status().is_success() => { - info!("Message sent to Ntfy for {}", app_name); - }, - Ok(response) => { - error!("Failed to send message to Ntfy. Status code: {}", response.status()); - }, - Err(e) => { - error!("Error sending to Ntfy: {}", e); - } - } -} - -async fn docker_send_to_gotify(release: DockerReleaseInfo, token: &str, gotify_url: &str) { - let client = reqwest::Client::new(); - let app_name = release.repo.split('/').last().unwrap_or(&release.repo); - - let url = format!("{}/message?token={}", gotify_url, token); - - let message = format!( - "🐳 *Docker Image Updated!*\n\n🔐 *New Digest*: `{}`\n\n📦 *App*: {}\n\n📢 *Published*: {}\n\n🔗 *Release Url*:{}", - release.digest, - app_name, - release.published_at.replace("T", " ").replace("Z", ""), - release.html_url - ); - - let content = json!({ - "title": format!("New version for {}", app_name), - "message": message, - "priority": "2" - }); - - match client.post(&url) - .json(&content) - .send() - .await - { - Ok(response) if response.status().is_success() => { - info!("Message sent to Gotify for {}", app_name); - }, - Ok(response) => { - error!("Failed to send message to Gotify. Status code: {}", response.status()); - }, - Err(e) => { - error!("Error sending to Gotify: {}", e); - } - } -} - -async fn docker_send_to_discord(release: DockerReleaseInfo, webhook_url: &str) { - let client = reqwest::Client::new(); - let app_name = release.repo.split('/').last().unwrap_or(&release.repo); - - let message = format!( - "🐳 *Docker Image Updated!*\n\n🔐 *New Digest*: `{}`\n\n📦 *App*: {}\n\n📢 *Published*: {}\n\n🔗 *Link*: {}", - release.digest, - app_name, - release.published_at.replace("T", " ").replace("Z", ""), - release.html_url - ); - - let data = json!({ - "content": message, - "username": "GitHub Ntfy" - }); - - match client.post(webhook_url) - .json(&data) - .send() - .await - { - Ok(response) if response.status().is_success() => { - info!("Message sent to Discord for {}", app_name); - }, - Ok(response) => { - error!("Failed to send message to Discord. Status code: {}", response.status()); - }, - Err(e) => { - error!("Error sending to Discord: {}", e); - } - } -} - -async fn docker_send_to_slack(release: DockerReleaseInfo, webhook_url: &str) { - let client = reqwest::Client::new(); - let app_name = release.repo.split('/').last().unwrap_or(&release.repo); - - let message = format!( - "🐳 *Docker Image Updated!*\n\n🔐 *New Digest*: `{}`\n\n📦 *App*: {}\n\n📢*Published*: {}", - release.digest, - app_name, - release.published_at.replace("T", " ").replace("Z", "") - ); - - let data = json!({ - "blocks": [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": message - }, - "accessory": { - "type": "button", - "text": { - "type": "plain_text", - "text": "🔗 Release Url" - }, - "url": release.html_url, - "action_id": "button-action" - } - }, - { - "type": "divider" - } - ] - }); - - let headers = HeaderMap::from_iter([( - CONTENT_TYPE, - HeaderValue::from_static("application/json") - )]); - - match client.post(webhook_url) - .headers(headers) - .json(&data) - .send() - .await - { - Ok(response) if response.status().is_success() => { - info!("Message sent to Slack for {}", app_name); - }, - Ok(response) => { - error!("Failed to send message to Slack. Status code: {}", response.status()); - }, - Err(e) => { - error!("Error sending to Slack: {}", e); - } - } -} - -// Function to start the API in a separate thread -fn start_api() { - std::thread::spawn(|| { - let runtime = tokio::runtime::Runtime::new().unwrap(); - runtime.block_on(async { - match api::start_api().await { - Ok(_) => info!("API closed correctly"), - Err(e) => error!("API error: {}", e), - } - }); - }); -} - -#[tokio::main] -async fn main() -> Result<(), Box> { - env_logger::init(); - - let config = Config::from_env(); - let (_conn_versions, conn_repos) = init_databases()?; - - start_api(); - - let client = reqwest::Client::new(); - - if config.auth.is_empty() || (config.ntfy_url.is_none() && config.gotify_url.is_none() - && config.discord_webhook_url.is_none() && config.slack_webhook_url.is_none()) { - error!("Incorrect configuration!"); - error!("auth: can be generated with the command: echo -n 'username:password' | base64"); - error!("NTFY_URL: URL of the ntfy server"); - error!("GOTIFY_URL: URL of the gotify server"); - error!("GOTIFY_TOKEN: Gotify token"); - error!("DISCORD_WEBHOOK_URL: Discord webhook URL"); - error!("SLACK_WEBHOOK_URL: Slack webhook URL"); - error!("GHNTFY_TIMEOUT: interval between checks"); - return Ok(()); - } - - info!("Starting version monitoring..."); - - loop { - let github_repos = get_watched_repos(&conn_repos)?; - let docker_repos = get_docker_watched_repos(&conn_repos)?; - - let github_releases = get_latest_releases(&github_repos, &client, config.github_headers()).await; - let docker_releases = get_latest_docker_releases(&docker_repos, &client, config.docker_headers()).await; - - send_notifications(github_releases, docker_releases, &config).await; - - tokio::time::sleep(Duration::from_secs_f64(config.timeout)).await; - } -} \ No newline at end of file diff --git a/api.rs b/src/api.rs similarity index 100% rename from api.rs rename to src/api.rs diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..6d2b82b --- /dev/null +++ b/src/config.rs @@ -0,0 +1,81 @@ +use dotenv::dotenv; +use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE}; +use std::env; +use std::fs::File; +use std::io::Read; +use crate::docker::create_dockerhub_token; + +// Configuration +pub struct Config { + pub github_token: Option, + pub docker_username: Option, + pub docker_password: Option, + pub docker_token: Option, + pub ntfy_url: Option, + pub gotify_url: Option, + pub gotify_token: Option, + pub discord_webhook_url: Option, + pub slack_webhook_url: Option, + pub auth: String, + pub timeout: f64, +} + +impl Config { + pub fn from_env() -> Self { + dotenv().ok(); + + let docker_username = env::var("DOCKER_USERNAME").ok(); + let docker_password = env::var("DOCKER_PASSWORD").ok(); + let docker_token = if let (Some(username), Some(password)) = (&docker_username, &docker_password) { + create_dockerhub_token(username, password) + } else { + None + }; + + // Read authentication file + let mut auth = String::new(); + if let Ok(mut file) = File::open("/auth.txt") { + file.read_to_string(&mut auth).ok(); + auth = auth.trim().to_string(); + } + + Config { + github_token: env::var("GHNTFY_TOKEN").ok(), + docker_username, + docker_password, + docker_token, + ntfy_url: env::var("NTFY_URL").ok(), + gotify_url: env::var("GOTIFY_URL").ok(), + gotify_token: env::var("GOTIFY_TOKEN").ok(), + discord_webhook_url: env::var("DISCORD_WEBHOOK_URL").ok(), + slack_webhook_url: env::var("SLACK_WEBHOOK_URL").ok(), + auth, + timeout: env::var("GHNTFY_TIMEOUT") + .unwrap_or_else(|_| "3600".to_string()) + .parse() + .unwrap_or(3600.0), + } + } + + pub fn github_headers(&self) -> HeaderMap { + let mut headers = HeaderMap::new(); + if let Some(token) = &self.github_token { + headers.insert( + AUTHORIZATION, + HeaderValue::from_str(&format!("token {}", token)).unwrap(), + ); + } + headers + } + + pub fn docker_headers(&self) -> HeaderMap { + let mut headers = HeaderMap::new(); + if let Some(token) = &self.docker_token { + headers.insert( + AUTHORIZATION, + HeaderValue::from_str(&format!("Bearer {}", token)).unwrap(), + ); + } + headers + } +} \ No newline at end of file diff --git a/src/database.rs b/src/database.rs new file mode 100644 index 0000000..2dd2507 --- /dev/null +++ b/src/database.rs @@ -0,0 +1,73 @@ +use log::info; +use rusqlite::{Connection, Result as SqliteResult}; +use std::env; + +pub fn init_databases() -> SqliteResult<(Connection, Connection)> { + let db_path = env::var("DB_PATH").unwrap_or_else(|_| "/github-ntfy".to_string()); + std::fs::create_dir_all(&db_path).ok(); + + let versions_path = format!("{}/ghntfy_versions.db", db_path); + let repos_path = format!("{}/watched_repos.db", db_path); + + let conn = Connection::open(&versions_path)?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS versions ( + repo TEXT PRIMARY KEY, + version TEXT, + changelog TEXT + )", + [], + )?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS docker_versions ( + repo TEXT PRIMARY KEY, + digest TEXT + )", + [], + )?; + + let conn2 = Connection::open(&repos_path)?; + + conn2.execute( + "CREATE TABLE IF NOT EXISTS watched_repos ( + id INTEGER PRIMARY KEY, + repo TEXT + )", + [], + )?; + + conn2.execute( + "CREATE TABLE IF NOT EXISTS docker_watched_repos ( + id INTEGER PRIMARY KEY, + repo TEXT + )", + [], + )?; + + Ok((conn, conn2)) +} + +// Functions to retrieve watched repositories +pub fn get_watched_repos(conn: &Connection) -> SqliteResult> { + let mut stmt = conn.prepare("SELECT * FROM watched_repos")?; + let repos_iter = stmt.query_map([], |row| Ok(row.get::<_, String>(1)?))?; + + let mut repos = Vec::new(); + for repo in repos_iter { + repos.push(repo?); + } + Ok(repos) +} + +pub fn get_docker_watched_repos(conn: &Connection) -> SqliteResult> { + let mut stmt = conn.prepare("SELECT * FROM docker_watched_repos")?; + let repos_iter = stmt.query_map([], |row| Ok(row.get::<_, String>(1)?))?; + + let mut repos = Vec::new(); + for repo in repos_iter { + repos.push(repo?); + } + Ok(repos) +} \ No newline at end of file diff --git a/src/docker.rs b/src/docker.rs new file mode 100644 index 0000000..6029c85 --- /dev/null +++ b/src/docker.rs @@ -0,0 +1,73 @@ +use log::error; +use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; +use serde_json::json; +use crate::models::{DockerTag, DockerReleaseInfo}; + +pub fn create_dockerhub_token(username: &str, password: &str) -> Option { + let client = reqwest::blocking::Client::new(); + let mut headers = HeaderMap::new(); + headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/json"), + ); + + let data = json!({ + "username": username, + "password": password + }); + + match client + .post("https://hub.docker.com/v2/users/login") + .headers(headers) + .json(&data) + .send() + { + Ok(response) => { + let status = response.status(); + if status.is_success() { + if let Ok(json) = response.json::() { + return json["token"].as_str().map(|s| s.to_string()); + } + } + error!("DockerHub authentication failed: {}", status); + None + } + Err(e) => { + error!("Error connecting to DockerHub: {}", e); + None + } + } +} + +pub async fn get_latest_docker_releases( + repos: &[String], + client: &reqwest::Client, + headers: HeaderMap, +) -> Vec { + let mut releases = Vec::new(); + + for repo in repos { + let url = format!("https://hub.docker.com/v2/repositories/{}/tags/latest", repo); + match client.get(&url).headers(headers.clone()).send().await { + Ok(response) => { + if response.status().is_success() { + if let Ok(tag) = response.json::().await { + releases.push(DockerReleaseInfo { + repo: repo.clone(), + digest: tag.digest.clone(), + html_url: format!("https://hub.docker.com/r/{}", repo), + published_at: tag.last_updated, + }); + } + } else { + error!("Error fetching Docker tag for {}: {}", repo, response.status()); + } + } + Err(e) => { + error!("Error fetching Docker tag for {}: {}", repo, e); + } + } + } + + releases +} \ No newline at end of file diff --git a/src/github.rs b/src/github.rs new file mode 100644 index 0000000..77aa2b3 --- /dev/null +++ b/src/github.rs @@ -0,0 +1,67 @@ +use log::error; +use reqwest::header::HeaderMap; +use crate::models::{GithubRelease, GithubReleaseInfo}; + +pub async fn get_latest_releases( + repos: &[String], + client: &reqwest::Client, + headers: HeaderMap, +) -> Vec { + let mut releases = Vec::new(); + + for repo in repos { + let url = format!("https://api.github.com/repos/{}/releases/latest", repo); + match client.get(&url).headers(headers.clone()).send().await { + Ok(response) => { + if response.status().is_success() { + if let Ok(release) = response.json::().await { + let changelog = get_changelog(repo, client, headers.clone()).await; + + releases.push(GithubReleaseInfo { + repo: repo.clone(), + name: release.name, + tag_name: release.tag_name, + html_url: release.html_url, + changelog, + published_at: release.published_at.unwrap_or_else(|| "Unknown date".to_string()), + }); + } + } else { + error!("Error fetching GitHub release for {}: {}", repo, response.status()); + } + } + Err(e) => { + error!("Error fetching GitHub release for {}: {}", repo, e); + } + } + } + + releases +} + +pub async fn get_changelog( + repo: &str, + client: &reqwest::Client, + headers: HeaderMap, +) -> String { + let url = format!("https://api.github.com/repos/{}/releases", repo); + + match client.get(&url).headers(headers).send().await { + Ok(response) => { + if response.status().is_success() { + if let Ok(releases) = response.json::>().await { + if !releases.is_empty() { + if let Some(body) = &releases[0].body { + return body.clone(); + } + } + } + } + } + Err(e) => { + error!("Error retrieving changelog for {}: {}", repo, e); + } + } + + "Changelog not available".to_string() +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9918a10 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,64 @@ +mod config; +mod models; +mod database; +mod github; +mod docker; +mod notifications; +mod api; + +use log::{error, info}; +use std::thread; +use std::time::Duration; +use tokio::task; + +// Function to start the API in a separate thread +fn start_api() { + std::thread::spawn(|| { + let runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(async { + match api::start_api().await { + Ok(_) => info!("API closed correctly"), + Err(e) => error!("API error: {}", e), + } + }); + }); +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + env_logger::init(); + + let config = config::Config::from_env(); + let (_conn_versions, conn_repos) = database::init_databases()?; + + start_api(); + + let client = reqwest::Client::new(); + + if config.auth.is_empty() || (config.ntfy_url.is_none() && config.gotify_url.is_none() + && config.discord_webhook_url.is_none() && config.slack_webhook_url.is_none()) { + error!("Incorrect configuration!"); + error!("auth: can be generated with the command: echo -n 'username:password' | base64"); + error!("NTFY_URL: URL of the ntfy server"); + error!("GOTIFY_URL: URL of the gotify server"); + error!("GOTIFY_TOKEN: Gotify token"); + error!("DISCORD_WEBHOOK_URL: Discord webhook URL"); + error!("SLACK_WEBHOOK_URL: Slack webhook URL"); + error!("GHNTFY_TIMEOUT: interval between checks"); + return Ok(()); + } + + info!("Starting version monitoring..."); + + loop { + let github_repos = database::get_watched_repos(&conn_repos)?; + let docker_repos = database::get_docker_watched_repos(&conn_repos)?; + + let github_releases = github::get_latest_releases(&github_repos, &client, config.github_headers()).await; + let docker_releases = docker::get_latest_docker_releases(&docker_repos, &client, config.docker_headers()).await; + + notifications::send_notifications(github_releases, docker_releases, &config).await; + + tokio::time::sleep(Duration::from_secs_f64(config.timeout)).await; + } +} \ No newline at end of file diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..fc75249 --- /dev/null +++ b/src/models.rs @@ -0,0 +1,36 @@ +use serde::Deserialize; + +// Structures for GitHub data +#[derive(Debug, Deserialize, Clone)] +pub struct GithubRelease { + pub name: String, + pub tag_name: String, + pub html_url: String, + pub published_at: Option, + pub body: Option, +} + +#[derive(Debug, Clone)] +pub struct GithubReleaseInfo { + pub repo: String, + pub name: String, + pub tag_name: String, + pub html_url: String, + pub changelog: String, + pub published_at: String, +} + +// Structures for Docker data +#[derive(Debug, Deserialize)] +pub struct DockerTag { + pub digest: String, + pub last_updated: String, +} + +#[derive(Debug, Clone)] +pub struct DockerReleaseInfo { + pub repo: String, + pub digest: String, + pub html_url: String, + pub published_at: String, +} \ No newline at end of file diff --git a/src/notifications/discord.rs b/src/notifications/discord.rs new file mode 100644 index 0000000..195cc2d --- /dev/null +++ b/src/notifications/discord.rs @@ -0,0 +1,78 @@ +use log::{error, info}; +use serde_json::json; +use crate::models::{GithubReleaseInfo, DockerReleaseInfo}; + +pub async fn send_github_notification(release: &GithubReleaseInfo, token: &str, gotify_url: &str) { + let client = reqwest::Client::new(); + let app_name = release.repo.split('/').last().unwrap_or(&release.repo); + + let url = format!("{}/message?token={}", gotify_url, token); + + let message = format!( + "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n```{}```\n\n🔗 *Release Url*:{}", + release.tag_name, + app_name, + release.published_at.replace("T", " ").replace("Z", ""), + release.changelog, + release.html_url + ); + + let content = json!({ + "title": format!("New version for {}", app_name), + "message": message, + "priority": "2" + }); + + match client.post(&url) + .json(&content) + .send() + .await + { + Ok(response) if response.status().is_success() => { + info!("Message sent to Gotify for {}", app_name); + }, + Ok(response) => { + error!("Failed to send message to Gotify. Status code: {}", response.status()); + }, + Err(e) => { + error!("Error sending to Gotify: {}", e); + } + } +} + +pub async fn send_docker_notification(release: &DockerReleaseInfo, token: &str, gotify_url: &str) { + let client = reqwest::Client::new(); + let app_name = release.repo.split('/').last().unwrap_or(&release.repo); + + let url = format!("{}/message?token={}", gotify_url, token); + + let message = format!( + "🐳 *Docker Image Updated!*\n\n🔐 *New Digest*: `{}`\n\n📦 *App*: {}\n\n📢 *Published*: {}\n\n🔗 *Release Url*:{}", + release.digest, + app_name, + release.published_at.replace("T", " ").replace("Z", ""), + release.html_url + ); + + let content = json!({ + "title": format!("New version for {}", app_name), + "message": message, + "priority": "2" + }); + + match client.post(&url) + .json(&content) + .send() + .await + { + Ok(response) if response.status().is_success() => { + info!("Message sent to Gotify for {}", app_name); + }, + Ok(response) => { + error!("Failed to send message to Gotify. Status code: {}", response.status()); + }, + Err(e) => { + error!("Error sending to Gotify: {}", e); + } + } +} \ No newline at end of file diff --git a/src/notifications/docker.rs b/src/notifications/docker.rs new file mode 100644 index 0000000..f689608 --- /dev/null +++ b/src/notifications/docker.rs @@ -0,0 +1,53 @@ +use tokio::task; +use crate::models::DockerReleaseInfo; +use crate::config::Config; +use crate::notifications::{ntfy, gotify, discord, slack}; + +pub async fn send_notifications(releases: &[DockerReleaseInfo], config: &Config) { + let mut tasks = Vec::new(); + + for release in releases { + // Send to Ntfy + if let Some(url) = &config.ntfy_url { + let release_clone = release.clone(); + let auth = config.auth.clone(); + let url_clone = url.clone(); + tasks.push(task::spawn(async move { + ntfy::send_docker_notification(&release_clone, &auth, &url_clone).await; + })); + } + + // Send to Gotify + if let (Some(gotify_url), Some(gotify_token)) = (&config.gotify_url, &config.gotify_token) { + let release_clone = release.clone(); + let token = gotify_token.clone(); + let url = gotify_url.clone(); + tasks.push(task::spawn(async move { + gotify::send_docker_notification(&release_clone, &token, &url).await; + })); + } + + // Send to Discord + if let Some(discord_url) = &config.discord_webhook_url { + let release_clone = release.clone(); + let url = discord_url.clone(); + tasks.push(task::spawn(async move { + discord::send_docker_notification(&release_clone, &url).await; + })); + } + + // Send to Slack + if let Some(slack_url) = &config.slack_webhook_url { + let release_clone = release.clone(); + let url = slack_url.clone(); + tasks.push(task::spawn(async move { + slack::send_docker_notification(&release_clone, &url).await; + })); + } + } + + // Wait for all tasks to complete + for task in tasks { + let _ = task.await; + } +} \ No newline at end of file diff --git a/src/notifications/github.rs b/src/notifications/github.rs new file mode 100644 index 0000000..5c747ac --- /dev/null +++ b/src/notifications/github.rs @@ -0,0 +1,133 @@ +use log::{error, info}; +use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; +use serde_json::json; +use crate::models::GithubReleaseInfo; + +pub async fn send_to_ntfy(release: GithubReleaseInfo, auth: &str, ntfy_url: &str) { + let client = reqwest::Client::new(); + let app_name = release.repo.split('/').last().unwrap_or(&release.repo); + + let mut headers = HeaderMap::new(); + headers.insert("Title", HeaderValue::from_str(&format!("New version for {}", app_name)) + .unwrap_or_else(|_| HeaderValue::from_static(""))); + headers.insert("Priority", HeaderValue::from_static("urgent")); + headers.insert("Markdown", HeaderValue::from_static("yes")); + headers.insert("Actions", HeaderValue::from_str(&format!("view, Update {}, {}, clear=true", app_name, release.html_url)) + .unwrap_or_else(|_| HeaderValue::from_static(""))); + + let message = format!( + "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n```{}```\n\n 🔗 *Release Url*: {}", + release.tag_name, + app_name, + release.published_at.replace("T", " ").replace("Z", ""), + release.changelog, + release.html_url + ); + + match client.post(ntfy_url) + .headers(headers) + .body(message) + .send() + .await + { + Ok(response) if response.status().is_success() => { + info!("Message sent to Ntfy for {}", app_name); + }, + Ok(response) => { + error!("Failed to send message to Ntfy. Status code: {}", response.status()); + }, + Err(e) => { + error!("Error sending to Ntfy: {}", e); + } + } +} + +pub async fn send_to_gotify(release: GithubReleaseInfo, token: &str, gotify_url: &str) { + let client = reqwest::Client::new(); + let app_name = release.repo.split('/').last().unwrap_or(&release.repo); + + let url = format!("{}/message?token={}", gotify_url, token); + + let message = format!( + "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n```{}```\n\n🔗 *Release Url*:{}", + release.tag_name, + app_name, + release.published_at.replace("T", " ").replace("Z", ""), + release.changelog, + release.html_url + ); + + let content = json!({ + "title": format!("New version for {}", app_name), + "message": message, + "priority": "2" + }); + + match client.post(&url) + .json(&content) + .send() + .await + { + Ok(response) if response.status().is_success() => { + info!("Message sent to Gotify for {}", app_name); + }, + Ok(response) => { + error!("Failed to send message to Gotify. Status code: {}", response.status()); + }, + Err(e) => { + error!("Error sending to Gotify: {}", e); + } + } +} + +pub async fn send_to_discord(release: GithubReleaseInfo, webhook_url: &str) { + let client = reqwest::Client::new(); + let app_name = release.repo.split('/').last().unwrap_or(&release.repo); + + let mut message = format!( + "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n```{}```", + release.tag_name, + app_name, + release.published_at.replace("T", " ").replace("Z", ""), + release.changelog + ); + + if message.len() > 2000 { + message = format!( + "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n🔗 *Release Link*: {}", + release.tag_name, + app_name, + release.published_at.replace("T", " ").replace("Z", ""), + release.html_url + ); + } + + let data = json!({ + "content": message, + "username": "GitHub Ntfy" + }); + + let headers = HeaderMap::new(); + + match client.post(webhook_url) + .headers(headers) + .json(&data) + .send() + .await + { + Ok(response) if response.status().is_success() => { + info!("Message sent to Discord for {}", app_name); + }, + Ok(response) => { + error!("Failed to send message to Discord. Status code: {}", response.status()); + }, + Err(e) => { + error!("Error sending to Discord: {}", e); + } + } +} + +pub async fn send_to_slack(release: GithubReleaseInfo, webhook_url: &str) { + // Implémentation pour Slack similaire à celle pour Discord + // ... +} \ No newline at end of file diff --git a/src/notifications/gotify.rs b/src/notifications/gotify.rs new file mode 100644 index 0000000..195cc2d --- /dev/null +++ b/src/notifications/gotify.rs @@ -0,0 +1,78 @@ +use log::{error, info}; +use serde_json::json; +use crate::models::{GithubReleaseInfo, DockerReleaseInfo}; + +pub async fn send_github_notification(release: &GithubReleaseInfo, token: &str, gotify_url: &str) { + let client = reqwest::Client::new(); + let app_name = release.repo.split('/').last().unwrap_or(&release.repo); + + let url = format!("{}/message?token={}", gotify_url, token); + + let message = format!( + "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n```{}```\n\n🔗 *Release Url*:{}", + release.tag_name, + app_name, + release.published_at.replace("T", " ").replace("Z", ""), + release.changelog, + release.html_url + ); + + let content = json!({ + "title": format!("New version for {}", app_name), + "message": message, + "priority": "2" + }); + + match client.post(&url) + .json(&content) + .send() + .await + { + Ok(response) if response.status().is_success() => { + info!("Message sent to Gotify for {}", app_name); + }, + Ok(response) => { + error!("Failed to send message to Gotify. Status code: {}", response.status()); + }, + Err(e) => { + error!("Error sending to Gotify: {}", e); + } + } +} + +pub async fn send_docker_notification(release: &DockerReleaseInfo, token: &str, gotify_url: &str) { + let client = reqwest::Client::new(); + let app_name = release.repo.split('/').last().unwrap_or(&release.repo); + + let url = format!("{}/message?token={}", gotify_url, token); + + let message = format!( + "🐳 *Docker Image Updated!*\n\n🔐 *New Digest*: `{}`\n\n📦 *App*: {}\n\n📢 *Published*: {}\n\n🔗 *Release Url*:{}", + release.digest, + app_name, + release.published_at.replace("T", " ").replace("Z", ""), + release.html_url + ); + + let content = json!({ + "title": format!("New version for {}", app_name), + "message": message, + "priority": "2" + }); + + match client.post(&url) + .json(&content) + .send() + .await + { + Ok(response) if response.status().is_success() => { + info!("Message sent to Gotify for {}", app_name); + }, + Ok(response) => { + error!("Failed to send message to Gotify. Status code: {}", response.status()); + }, + Err(e) => { + error!("Error sending to Gotify: {}", e); + } + } +} \ No newline at end of file diff --git a/src/notifications/mod.rs b/src/notifications/mod.rs new file mode 100644 index 0000000..5cc53a6 --- /dev/null +++ b/src/notifications/mod.rs @@ -0,0 +1,97 @@ +pub mod ntfy; +pub mod gotify; +pub mod discord; +pub mod slack; +pub mod github; +pub mod docker; + +use tokio::task; +use crate::models::{GithubReleaseInfo, DockerReleaseInfo}; +use crate::config::Config; + +pub async fn send_notifications( + github_releases: Vec, + docker_releases: Vec, + config: &Config, +) { + let mut tasks = Vec::new(); + + // Create tasks for GitHub notifications + for release in &github_releases { + if let Some(url) = &config.ntfy_url { + let release = release.clone(); + let auth = config.auth.clone(); + let url = url.clone(); + tasks.push(task::spawn(async move { + github::send_to_ntfy(release, &auth, &url).await; + })); + } + + if let (Some(gotify_url), Some(gotify_token)) = (&config.gotify_url, &config.gotify_token) { + let release = release.clone(); + let url = gotify_url.clone(); + let token = gotify_token.clone(); + tasks.push(task::spawn(async move { + github::send_to_gotify(release, &token, &url).await; + })); + } + + if let Some(discord_url) = &config.discord_webhook_url { + let release = release.clone(); + let url = discord_url.clone(); + tasks.push(task::spawn(async move { + github::send_to_discord(release, &url).await; + })); + } + + if let Some(slack_url) = &config.slack_webhook_url { + let release = release.clone(); + let url = slack_url.clone(); + tasks.push(task::spawn(async move { + github::send_to_slack(release, &url).await; + })); + } + } + + // Create tasks for Docker notifications + for release in &docker_releases { + if let Some(url) = &config.ntfy_url { + let release = release.clone(); + let auth = config.auth.clone(); + let url = url.clone(); + tasks.push(task::spawn(async move { + docker::send_to_ntfy(release, &auth, &url).await; + })); + } + + if let (Some(gotify_url), Some(gotify_token)) = (&config.gotify_url, &config.gotify_token) { + let release = release.clone(); + let url = gotify_url.clone(); + let token = gotify_token.clone(); + tasks.push(task::spawn(async move { + docker::send_to_gotify(release, &token, &url).await; + })); + } + + if let Some(discord_url) = &config.discord_webhook_url { + let release = release.clone(); + let url = discord_url.clone(); + tasks.push(task::spawn(async move { + docker::send_to_discord(release, &url).await; + })); + } + + if let Some(slack_url) = &config.slack_webhook_url { + let release = release.clone(); + let url = slack_url.clone(); + tasks.push(task::spawn(async move { + docker::send_to_slack(release, &url).await; + })); + } + } + + // Wait for all tasks to complete + for task in tasks { + let _ = task.await; + } +} \ No newline at end of file diff --git a/src/notifications/ntfy.rs b/src/notifications/ntfy.rs new file mode 100644 index 0000000..ec0bf49 --- /dev/null +++ b/src/notifications/ntfy.rs @@ -0,0 +1,84 @@ +use log::{error, info}; +use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; +use crate::models::{GithubReleaseInfo, DockerReleaseInfo}; + +pub async fn send_github_notification(release: &GithubReleaseInfo, auth: &str, ntfy_url: &str) { + let client = reqwest::Client::new(); + let app_name = release.repo.split('/').last().unwrap_or(&release.repo); + + let mut headers = HeaderMap::new(); + headers.insert("Authorization", HeaderValue::from_str(&format!("Basic {}", auth)) + .unwrap_or_else(|_| HeaderValue::from_static(""))); + headers.insert("Title", HeaderValue::from_str(&format!("New version for {}", app_name)) + .unwrap_or_else(|_| HeaderValue::from_static(""))); + headers.insert("Priority", HeaderValue::from_static("urgent")); + headers.insert("Markdown", HeaderValue::from_static("yes")); + headers.insert("Actions", HeaderValue::from_str(&format!("view, Update {}, {}, clear=true", app_name, release.html_url)) + .unwrap_or_else(|_| HeaderValue::from_static(""))); + + let message = format!( + "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n```{}```\n\n 🔗 *Release Url*: {}", + release.tag_name, + app_name, + release.published_at.replace("T", " ").replace("Z", ""), + release.changelog, + release.html_url + ); + + match client.post(ntfy_url) + .headers(headers) + .body(message) + .send() + .await + { + Ok(response) if response.status().is_success() => { + info!("Message sent to Ntfy for {}", app_name); + }, + Ok(response) => { + error!("Failed to send message to Ntfy. Status code: {}", response.status()); + }, + Err(e) => { + error!("Error sending to Ntfy: {}", e); + } + } +} + +pub async fn send_docker_notification(release: &DockerReleaseInfo, auth: &str, ntfy_url: &str) { + let client = reqwest::Client::new(); + let app_name = release.repo.split('/').last().unwrap_or(&release.repo); + + let mut headers = HeaderMap::new(); + headers.insert("Authorization", HeaderValue::from_str(&format!("Basic {}", auth)) + .unwrap_or_else(|_| HeaderValue::from_static(""))); + headers.insert("Title", HeaderValue::from_str(&format!("🆕 New version for {}", app_name)) + .unwrap_or_else(|_| HeaderValue::from_static(""))); + headers.insert("Priority", HeaderValue::from_static("urgent")); + headers.insert("Markdown", HeaderValue::from_static("yes")); + headers.insert("Actions", HeaderValue::from_str(&format!("View, Update {}, {}, clear=true", app_name, release.html_url)) + .unwrap_or_else(|_| HeaderValue::from_static(""))); + + let message = format!( + "🐳 *Docker Image Updated!*\n\n🔐 *New Digest*: `{}`\n\n📦 *App*: {}\n\n📢 *Published*: {}\n\n 🔗 *Release Url*: {}", + release.digest, + app_name, + release.published_at.replace("T", " ").replace("Z", ""), + release.html_url + ); + + match client.post(ntfy_url) + .headers(headers) + .body(message) + .send() + .await + { + Ok(response) if response.status().is_success() => { + info!("Message sent to Ntfy for {}", app_name); + }, + Ok(response) => { + error!("Failed to send message to Ntfy. Status code: {}", response.status()); + }, + Err(e) => { + error!("Error sending to Ntfy: {}", e); + } + } +} \ No newline at end of file diff --git a/src/notifications/slack.rs b/src/notifications/slack.rs new file mode 100644 index 0000000..9066720 --- /dev/null +++ b/src/notifications/slack.rs @@ -0,0 +1,131 @@ +use log::{error, info}; +use serde_json::json; +use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; +use std::iter::FromIterator; +use crate::models::{GithubReleaseInfo, DockerReleaseInfo}; + +pub async fn send_github_notification(release: &GithubReleaseInfo, webhook_url: &str) { + let client = reqwest::Client::new(); + let app_name = release.repo.split('/').last().unwrap_or(&release.repo); + + let mut message = format!( + "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n```{}```", + release.tag_name, + app_name, + release.published_at.replace("T", " ").replace("Z", ""), + release.changelog + ); + + if message.len() > 2000 { + message = format!( + "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n `truncated..` use 🔗 instead", + release.tag_name, + app_name, + release.published_at.replace("T", " ").replace("Z", "") + ); + } + + let data = json!({ + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": message + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "View Release" + }, + "url": release.html_url, + "action_id": "button-action" + } + }, + { + "type": "divider" + } + ] + }); + + let headers = HeaderMap::from_iter([( + CONTENT_TYPE, + HeaderValue::from_static("application/json") + )]); + + match client.post(webhook_url) + .headers(headers) + .json(&data) + .send() + .await + { + Ok(response) if response.status().is_success() => { + info!("Message sent to Slack for {}", app_name); + }, + Ok(response) => { + error!("Failed to send message to Slack. Status code: {}", response.status()); + }, + Err(e) => { + error!("Error sending to Slack: {}", e); + } + } +} + +pub async fn send_docker_notification(release: &DockerReleaseInfo, webhook_url: &str) { + let client = reqwest::Client::new(); + let app_name = release.repo.split('/').last().unwrap_or(&release.repo); + + let message = format!( + "🐳 *Docker Image Updated!*\n\n🔐 *New Digest*: `{}`\n\n📦 *App*: {}\n\n📢*Published*: {}", + release.digest, + app_name, + release.published_at.replace("T", " ").replace("Z", "") + ); + + let data = json!({ + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": message + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "View Image" + }, + "url": release.html_url, + "action_id": "button-action" + } + }, + { + "type": "divider" + } + ] + }); + + let headers = HeaderMap::from_iter([( + CONTENT_TYPE, + HeaderValue::from_static("application/json") + )]); + + match client.post(webhook_url) + .headers(headers) + .json(&data) + .send() + .await + { + Ok(response) if response.status().is_success() => { + info!("Message sent to Slack for {}", app_name); + }, + Ok(response) => { + error!("Failed to send message to Slack. Status code: {}", response.status()); + }, + Err(e) => { + error!("Error sending to Slack: {}", e); + } + } +} \ No newline at end of file From 57ea0ef54b43bb25c26f1941a0648b3452170ce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Thu, 12 Jun 2025 20:05:44 +0200 Subject: [PATCH 04/23] refactor(rust): update notification functions and improve database handling --- Cargo.lock | 2 +- src/database.rs | 16 +++- src/notifications/discord.rs | 65 +++++++------ src/notifications/docker.rs | 24 ++++- src/notifications/github.rs | 176 +++++++++++------------------------ 5 files changed, 125 insertions(+), 158 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c164e1..0ecc891 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -399,7 +399,7 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "github-ntfy" -version = "0.1.0" +version = "2.0.0" dependencies = [ "chrono", "dotenv", diff --git a/src/database.rs b/src/database.rs index 2dd2507..5669d2b 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,15 +1,21 @@ use log::info; -use rusqlite::{Connection, Result as SqliteResult}; +use rusqlite::{Connection, Result as SqliteResult, OpenFlags}; use std::env; +use std::path::Path; pub fn init_databases() -> SqliteResult<(Connection, Connection)> { let db_path = env::var("DB_PATH").unwrap_or_else(|_| "/github-ntfy".to_string()); - std::fs::create_dir_all(&db_path).ok(); + + if let Err(e) = std::fs::create_dir_all(&db_path) { + info!("Error while creating directory {}: {}", db_path, e); + } let versions_path = format!("{}/ghntfy_versions.db", db_path); let repos_path = format!("{}/watched_repos.db", db_path); - let conn = Connection::open(&versions_path)?; + let conn = Connection::open_with_flags(&versions_path, OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_URI)?; + + info!("Database open at {}", versions_path); conn.execute( "CREATE TABLE IF NOT EXISTS versions ( @@ -28,7 +34,9 @@ pub fn init_databases() -> SqliteResult<(Connection, Connection)> { [], )?; - let conn2 = Connection::open(&repos_path)?; + let conn2 = Connection::open_with_flags(&repos_path, OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_URI)?; + + info!("Database open at {}", repos_path); conn2.execute( "CREATE TABLE IF NOT EXISTS watched_repos ( diff --git a/src/notifications/discord.rs b/src/notifications/discord.rs index 195cc2d..94f5be1 100644 --- a/src/notifications/discord.rs +++ b/src/notifications/discord.rs @@ -1,78 +1,85 @@ use log::{error, info}; use serde_json::json; +use reqwest::header::HeaderMap; use crate::models::{GithubReleaseInfo, DockerReleaseInfo}; -pub async fn send_github_notification(release: &GithubReleaseInfo, token: &str, gotify_url: &str) { +pub async fn send_github_notification(release: &GithubReleaseInfo, webhook_url: &str) { let client = reqwest::Client::new(); let app_name = release.repo.split('/').last().unwrap_or(&release.repo); - let url = format!("{}/message?token={}", gotify_url, token); - - let message = format!( - "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n```{}```\n\n🔗 *Release Url*:{}", + let mut message = format!( + "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n```{}```", release.tag_name, app_name, release.published_at.replace("T", " ").replace("Z", ""), - release.changelog, - release.html_url + release.changelog ); - let content = json!({ - "title": format!("New version for {}", app_name), - "message": message, - "priority": "2" + if message.len() > 2000 { + message = format!( + "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n🔗 *Release Link*: {}", + release.tag_name, + app_name, + release.published_at.replace("T", " ").replace("Z", ""), + release.html_url + ); + } + + let data = json!({ + "content": message, + "username": "GitHub Ntfy" }); - match client.post(&url) - .json(&content) + let headers = HeaderMap::new(); + + match client.post(webhook_url) + .headers(headers) + .json(&data) .send() .await { Ok(response) if response.status().is_success() => { - info!("Message sent to Gotify for {}", app_name); + info!("Message sent to Discord for {}", app_name); }, Ok(response) => { - error!("Failed to send message to Gotify. Status code: {}", response.status()); + error!("Failed to send message to Discord. Status code: {}", response.status()); }, Err(e) => { - error!("Error sending to Gotify: {}", e); + error!("Error sending to Discord: {}", e); } } } -pub async fn send_docker_notification(release: &DockerReleaseInfo, token: &str, gotify_url: &str) { +pub async fn send_docker_notification(release: &DockerReleaseInfo, webhook_url: &str) { let client = reqwest::Client::new(); let app_name = release.repo.split('/').last().unwrap_or(&release.repo); - let url = format!("{}/message?token={}", gotify_url, token); - let message = format!( - "🐳 *Docker Image Updated!*\n\n🔐 *New Digest*: `{}`\n\n📦 *App*: {}\n\n📢 *Published*: {}\n\n🔗 *Release Url*:{}", + "🐳 *Docker Image Updated!*\n\n🔐 *New Digest*: `{}`\n\n📦 *App*: {}\n\n📢 *Published*: {}\n\n🔗 *Link*: {}", release.digest, app_name, release.published_at.replace("T", " ").replace("Z", ""), release.html_url ); - let content = json!({ - "title": format!("New version for {}", app_name), - "message": message, - "priority": "2" + let data = json!({ + "content": message, + "username": "GitHub Ntfy" }); - match client.post(&url) - .json(&content) + match client.post(webhook_url) + .json(&data) .send() .await { Ok(response) if response.status().is_success() => { - info!("Message sent to Gotify for {}", app_name); + info!("Message sent to Discord for {}", app_name); }, Ok(response) => { - error!("Failed to send message to Gotify. Status code: {}", response.status()); + error!("Failed to send message to Discord. Status code: {}", response.status()); }, Err(e) => { - error!("Error sending to Gotify: {}", e); + error!("Error sending to Discord: {}", e); } } } \ No newline at end of file diff --git a/src/notifications/docker.rs b/src/notifications/docker.rs index f689608..0777447 100644 --- a/src/notifications/docker.rs +++ b/src/notifications/docker.rs @@ -3,6 +3,22 @@ use crate::models::DockerReleaseInfo; use crate::config::Config; use crate::notifications::{ntfy, gotify, discord, slack}; +pub async fn send_to_ntfy(release: DockerReleaseInfo, auth: &str, ntfy_url: &str) { + ntfy::send_docker_notification(&release, auth, ntfy_url).await; +} + +pub async fn send_to_gotify(release: DockerReleaseInfo, token: &str, gotify_url: &str) { + gotify::send_docker_notification(&release, token, gotify_url).await; +} + +pub async fn send_to_discord(release: DockerReleaseInfo, webhook_url: &str) { + discord::send_docker_notification(&release, webhook_url).await; +} + +pub async fn send_to_slack(release: DockerReleaseInfo, webhook_url: &str) { + slack::send_docker_notification(&release, webhook_url).await; +} + pub async fn send_notifications(releases: &[DockerReleaseInfo], config: &Config) { let mut tasks = Vec::new(); @@ -13,7 +29,7 @@ pub async fn send_notifications(releases: &[DockerReleaseInfo], config: &Config) let auth = config.auth.clone(); let url_clone = url.clone(); tasks.push(task::spawn(async move { - ntfy::send_docker_notification(&release_clone, &auth, &url_clone).await; + send_to_ntfy(release_clone, &auth, &url_clone).await; })); } @@ -23,7 +39,7 @@ pub async fn send_notifications(releases: &[DockerReleaseInfo], config: &Config) let token = gotify_token.clone(); let url = gotify_url.clone(); tasks.push(task::spawn(async move { - gotify::send_docker_notification(&release_clone, &token, &url).await; + send_to_gotify(release_clone, &token, &url).await; })); } @@ -32,7 +48,7 @@ pub async fn send_notifications(releases: &[DockerReleaseInfo], config: &Config) let release_clone = release.clone(); let url = discord_url.clone(); tasks.push(task::spawn(async move { - discord::send_docker_notification(&release_clone, &url).await; + send_to_discord(release_clone, &url).await; })); } @@ -41,7 +57,7 @@ pub async fn send_notifications(releases: &[DockerReleaseInfo], config: &Config) let release_clone = release.clone(); let url = slack_url.clone(); tasks.push(task::spawn(async move { - slack::send_docker_notification(&release_clone, &url).await; + send_to_slack(release_clone, &url).await; })); } } diff --git a/src/notifications/github.rs b/src/notifications/github.rs index 5c747ac..3911fe9 100644 --- a/src/notifications/github.rs +++ b/src/notifications/github.rs @@ -1,133 +1,69 @@ -use log::{error, info}; -use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; -use serde_json::json; +use tokio::task; use crate::models::GithubReleaseInfo; +use crate::config::Config; +use crate::notifications::{ntfy, gotify, discord, slack}; pub async fn send_to_ntfy(release: GithubReleaseInfo, auth: &str, ntfy_url: &str) { - let client = reqwest::Client::new(); - let app_name = release.repo.split('/').last().unwrap_or(&release.repo); - - let mut headers = HeaderMap::new(); - headers.insert("Title", HeaderValue::from_str(&format!("New version for {}", app_name)) - .unwrap_or_else(|_| HeaderValue::from_static(""))); - headers.insert("Priority", HeaderValue::from_static("urgent")); - headers.insert("Markdown", HeaderValue::from_static("yes")); - headers.insert("Actions", HeaderValue::from_str(&format!("view, Update {}, {}, clear=true", app_name, release.html_url)) - .unwrap_or_else(|_| HeaderValue::from_static(""))); - - let message = format!( - "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n```{}```\n\n 🔗 *Release Url*: {}", - release.tag_name, - app_name, - release.published_at.replace("T", " ").replace("Z", ""), - release.changelog, - release.html_url - ); - - match client.post(ntfy_url) - .headers(headers) - .body(message) - .send() - .await - { - Ok(response) if response.status().is_success() => { - info!("Message sent to Ntfy for {}", app_name); - }, - Ok(response) => { - error!("Failed to send message to Ntfy. Status code: {}", response.status()); - }, - Err(e) => { - error!("Error sending to Ntfy: {}", e); - } - } + ntfy::send_github_notification(&release, auth, ntfy_url).await; } pub async fn send_to_gotify(release: GithubReleaseInfo, token: &str, gotify_url: &str) { - let client = reqwest::Client::new(); - let app_name = release.repo.split('/').last().unwrap_or(&release.repo); - - let url = format!("{}/message?token={}", gotify_url, token); - - let message = format!( - "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n```{}```\n\n🔗 *Release Url*:{}", - release.tag_name, - app_name, - release.published_at.replace("T", " ").replace("Z", ""), - release.changelog, - release.html_url - ); - - let content = json!({ - "title": format!("New version for {}", app_name), - "message": message, - "priority": "2" - }); - - match client.post(&url) - .json(&content) - .send() - .await - { - Ok(response) if response.status().is_success() => { - info!("Message sent to Gotify for {}", app_name); - }, - Ok(response) => { - error!("Failed to send message to Gotify. Status code: {}", response.status()); - }, - Err(e) => { - error!("Error sending to Gotify: {}", e); - } - } + gotify::send_github_notification(&release, token, gotify_url).await; } pub async fn send_to_discord(release: GithubReleaseInfo, webhook_url: &str) { - let client = reqwest::Client::new(); - let app_name = release.repo.split('/').last().unwrap_or(&release.repo); - - let mut message = format!( - "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n📝 *Changelog*:\n\n```{}```", - release.tag_name, - app_name, - release.published_at.replace("T", " ").replace("Z", ""), - release.changelog - ); - - if message.len() > 2000 { - message = format!( - "📌 *New version*: {}\n\n📦*For*: {}\n\n📅 *Published on*: {}\n\n🔗 *Release Link*: {}", - release.tag_name, - app_name, - release.published_at.replace("T", " ").replace("Z", ""), - release.html_url - ); - } - - let data = json!({ - "content": message, - "username": "GitHub Ntfy" - }); - - let headers = HeaderMap::new(); - - match client.post(webhook_url) - .headers(headers) - .json(&data) - .send() - .await - { - Ok(response) if response.status().is_success() => { - info!("Message sent to Discord for {}", app_name); - }, - Ok(response) => { - error!("Failed to send message to Discord. Status code: {}", response.status()); - }, - Err(e) => { - error!("Error sending to Discord: {}", e); - } - } + discord::send_github_notification(&release, webhook_url).await; } pub async fn send_to_slack(release: GithubReleaseInfo, webhook_url: &str) { - // Implémentation pour Slack similaire à celle pour Discord - // ... + slack::send_github_notification(&release, webhook_url).await; +} + +pub async fn send_notifications(releases: &[GithubReleaseInfo], config: &Config) { + let mut tasks = Vec::new(); + + for release in releases { + // Send to Ntfy + if let Some(url) = &config.ntfy_url { + let release_clone = release.clone(); + let auth = config.auth.clone(); + let url_clone = url.clone(); + tasks.push(task::spawn(async move { + send_to_ntfy(release_clone, &auth, &url_clone).await; + })); + } + + // Send to Gotify + if let (Some(gotify_url), Some(gotify_token)) = (&config.gotify_url, &config.gotify_token) { + let release_clone = release.clone(); + let token = gotify_token.clone(); + let url = gotify_url.clone(); + tasks.push(task::spawn(async move { + send_to_gotify(release_clone, &token, &url).await; + })); + } + + // Send to Discord + if let Some(discord_url) = &config.discord_webhook_url { + let release_clone = release.clone(); + let url = discord_url.clone(); + tasks.push(task::spawn(async move { + send_to_discord(release_clone, &url).await; + })); + } + + // Send to Slack + if let Some(slack_url) = &config.slack_webhook_url { + let release_clone = release.clone(); + let url = slack_url.clone(); + tasks.push(task::spawn(async move { + send_to_slack(release_clone, &url).await; + })); + } + } + + // Wait for all tasks to complete + for task in tasks { + let _ = task.await; + } } \ No newline at end of file From 39f0d6aa8bd35f7d414d1df46df8f2e49d82badf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Thu, 12 Jun 2025 20:09:10 +0200 Subject: [PATCH 05/23] refactor(rust): upgrade GitHub Actions to use version 4 of upload and download artifacts --- .github/workflows/create_dev.yml | 4 ++-- .github/workflows/create_release.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/create_dev.yml b/.github/workflows/create_dev.yml index 58f261a..799b9ae 100644 --- a/.github/workflows/create_dev.yml +++ b/.github/workflows/create_dev.yml @@ -45,7 +45,7 @@ jobs: cp target/${{ matrix.target }}/release/github-ntfy release/${{ matrix.name }} - name: Upload binaire comme artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ matrix.name }} path: release/${{ matrix.name }} @@ -72,7 +72,7 @@ jobs: password: ${{ secrets.DOCKER_PASSWORD }} - name: Télécharger tous les binaires - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: binaries diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 330ccf5..9919695 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -77,7 +77,7 @@ jobs: cp target/${{ matrix.target }}/release/github-ntfy release/${{ matrix.name }} - name: Upload binaire comme artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ matrix.name }} path: release/${{ matrix.name }} @@ -104,7 +104,7 @@ jobs: password: ${{ secrets.DOCKER_PASSWORD }} - name: Télécharger tous les binaires - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: binaries @@ -137,7 +137,7 @@ jobs: uses: actions/checkout@v4 - name: Télécharger tous les binaires - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: binaries From 5caa2b56ce6b85a9bc3d1047d1beeb921e326827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Thu, 12 Jun 2025 20:18:30 +0200 Subject: [PATCH 06/23] refactor(rust): update CI configuration to support static OpenSSL with cross --- .github/dependabot.yaml | 2 +- .github/workflows/create_dev.yml | 14 +++++++++++++- .github/workflows/create_release.yml | 13 +++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index b95566b..1638788 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -5,7 +5,7 @@ version: 2 updates: - - package-ecosystem: "pip" # See documentation for possible values + - package-ecosystem: "cargo" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" diff --git a/.github/workflows/create_dev.yml b/.github/workflows/create_dev.yml index 799b9ae..dab2c0f 100644 --- a/.github/workflows/create_dev.yml +++ b/.github/workflows/create_dev.yml @@ -35,7 +35,20 @@ jobs: - name: Installer cross run: cargo install cross + - name: Créer Cross.toml pour spécifier OpenSSL statique + run: | + cat > Cross.toml << 'EOF' + [build.env] + passthrough = [ + "RUSTFLAGS", + "OPENSSL_STATIC", + ] + EOF + - name: Construire avec cross + env: + OPENSSL_STATIC: 1 + RUSTFLAGS: "-C target-feature=+crt-static" run: | cross build --release --target ${{ matrix.target }} @@ -85,7 +98,6 @@ jobs: chmod +x binaries-docker/* ls -la binaries-docker/ - # Construire et pousser l'image Docker dev - name: Construire et pousser l'image Docker dev uses: docker/build-push-action@v6 with: diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 9919695..11b05a6 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -67,7 +67,20 @@ jobs: - name: Installer cross run: cargo install cross + - name: Créer Cross.toml pour spécifier OpenSSL statique + run: | + cat > Cross.toml << 'EOF' + [build.env] + passthrough = [ + "RUSTFLAGS", + "OPENSSL_STATIC", + ] + EOF + - name: Construire avec cross + env: + OPENSSL_STATIC: 1 + RUSTFLAGS: "-C target-feature=+crt-static" run: | cross build --release --target ${{ matrix.target }} From b28f70b6596974a11d60c261b91b3bf43a9cd973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Thu, 12 Jun 2025 20:23:06 +0200 Subject: [PATCH 07/23] refactor(rust): add support for vendored OpenSSL in CI configuration --- .github/workflows/create_dev.yml | 8 +++++--- .github/workflows/create_release.yml | 4 +++- Cargo.toml | 6 +++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/create_dev.yml b/.github/workflows/create_dev.yml index dab2c0f..f51312b 100644 --- a/.github/workflows/create_dev.yml +++ b/.github/workflows/create_dev.yml @@ -35,22 +35,24 @@ jobs: - name: Installer cross run: cargo install cross - - name: Créer Cross.toml pour spécifier OpenSSL statique + - name: Créer Cross.toml pour spécifier OpenSSL vendored run: | cat > Cross.toml << 'EOF' [build.env] passthrough = [ "RUSTFLAGS", "OPENSSL_STATIC", + "OPENSSL_NO_VENDOR" ] EOF - - name: Construire avec cross + - name: Construire avec cross et OpenSSL vendored env: OPENSSL_STATIC: 1 RUSTFLAGS: "-C target-feature=+crt-static" + OPENSSL_NO_VENDOR: 0 run: | - cross build --release --target ${{ matrix.target }} + cross build --release --target ${{ matrix.target }} --features vendored-openssl - name: Préparer le binaire run: | diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 11b05a6..e7bf7d6 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -74,6 +74,7 @@ jobs: passthrough = [ "RUSTFLAGS", "OPENSSL_STATIC", + "OPENSSL_NO_VENDOR" ] EOF @@ -81,8 +82,9 @@ jobs: env: OPENSSL_STATIC: 1 RUSTFLAGS: "-C target-feature=+crt-static" + OPENSSL_NO_VENDOR: 0 run: | - cross build --release --target ${{ matrix.target }} + cross build --release --target ${{ matrix.target }} --features vendored-openssl - name: Préparer le binaire run: | diff --git a/Cargo.toml b/Cargo.toml index 854ce9d..b3444de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,9 @@ edition = "2021" name = "github-ntfy" path = "src/main.rs" +[features] +vendored-openssl = ["openssl/vendored"] + [dependencies] tokio = { version = "1", features = ["full"] } reqwest = { version = "0.11", features = ["json", "blocking"] } @@ -17,4 +20,5 @@ log = "0.4" env_logger = "0.10" dotenv = "0.15" chrono = "0.4" -warp = "0.3" \ No newline at end of file +warp = "0.3" +openssl = { version = "0.10", features = ["vendored"] } \ No newline at end of file From 622e3d4334ce4f1a9bd53bcf365b0b664fa21c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= Date: Thu, 12 Jun 2025 21:32:20 +0200 Subject: [PATCH 08/23] refactor(rust): update CI configuration for multi-architecture Docker builds --- .github/workflows/create_dev.yml | 51 ++++++++++++++++++++++++---- .github/workflows/create_release.yml | 3 -- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/.github/workflows/create_dev.yml b/.github/workflows/create_dev.yml index f51312b..4e6ecc7 100644 --- a/.github/workflows/create_dev.yml +++ b/.github/workflows/create_dev.yml @@ -17,9 +17,6 @@ jobs: - target: aarch64-unknown-linux-musl platform: linux/arm64 name: github-ntfy-arm64 - - target: armv7-unknown-linux-musleabihf - platform: linux/arm/v7 - name: github-ntfy-armv7 steps: - name: Checkout code @@ -66,7 +63,7 @@ jobs: path: release/${{ matrix.name }} docker-build-push: - needs: [build-binaries] + needs: [ version, build-binaries ] runs-on: ubuntu-latest steps: - name: Checkout code @@ -98,13 +95,53 @@ jobs: cp binaries/github-ntfy-arm64/github-ntfy-arm64 binaries-docker/ cp binaries/github-ntfy-armv7/github-ntfy-armv7 binaries-docker/ chmod +x binaries-docker/* - ls -la binaries-docker/ - - name: Construire et pousser l'image Docker dev + - name: Créer Dockerfile spécifique pour chaque architecture + run: | + # Créer un Dockerfile séparé pour chaque architecture + cat > Dockerfile.multi << 'EOF' + FROM --platform=${TARGETPLATFORM} alpine:3.22 + + ARG TARGETPLATFORM + ARG TARGETARCH + ARG TARGETVARIANT + + # Copier le binaire approprié selon l'architecture + COPY binaries-docker/github-ntfy-amd64 /usr/local/bin/github-ntfy-amd64 + COPY binaries-docker/github-ntfy-arm64 /usr/local/bin/github-ntfy-arm64 + COPY binaries-docker/github-ntfy-armv7 /usr/local/bin/github-ntfy-armv7 + + # Installer des outils pour débogage et déplacer le bon binaire + RUN apk add --no-cache sqlite-libs openssl nginx && \ + if [ "$TARGETARCH" = "amd64" ]; then \ + cp /usr/local/bin/github-ntfy-amd64 /usr/local/bin/github-ntfy; \ + elif [ "$TARGETARCH" = "arm64" ]; then \ + cp /usr/local/bin/github-ntfy-arm64 /usr/local/bin/github-ntfy; \ + elif [ "$TARGETARCH" = "arm" ] && [ "$TARGETVARIANT" = "v7" ]; then \ + cp /usr/local/bin/github-ntfy-armv7 /usr/local/bin/github-ntfy; \ + fi && \ + chmod +x /usr/local/bin/github-ntfy && \ + rm /usr/local/bin/github-ntfy-* + + WORKDIR /app + COPY web /app/web + COPY config.toml /app/config.toml + + ENV DB_PATH=/data + VOLUME /data + EXPOSE 8080 + + ENTRYPOINT ["/usr/local/bin/github-ntfy"] + CMD ["--config", "/app/config.toml"] + EOF + + - name: Construire et pousser l'image Docker multi-architecture uses: docker/build-push-action@v6 with: context: . push: true platforms: linux/amd64,linux/arm64,linux/arm/v7 - tags: ${{ secrets.DOCKER_USERNAME }}/github-ntfy:dev + tags: | + ${{ secrets.DOCKER_USERNAME }}/github-ntfy:dev + ${{ secrets.DOCKER_USERNAME }}/github-ntfy:${{ needs.version.outputs.version }} file: Dockerfile.multi \ No newline at end of file diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index e7bf7d6..4df3cdf 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -49,9 +49,6 @@ jobs: - target: aarch64-unknown-linux-musl platform: linux/arm64 name: github-ntfy-arm64 - - target: armv7-unknown-linux-musleabihf - platform: linux/arm/v7 - name: github-ntfy-armv7 steps: - name: Checkout code From 38918f0bb8b078d4f63437c6e54556ac9b8a3947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= Date: Thu, 12 Jun 2025 21:33:46 +0200 Subject: [PATCH 09/23] refactor(rust): simplify CI dependencies by removing unnecessary job requirement --- .github/workflows/create_dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/create_dev.yml b/.github/workflows/create_dev.yml index 4e6ecc7..f6b13e7 100644 --- a/.github/workflows/create_dev.yml +++ b/.github/workflows/create_dev.yml @@ -63,7 +63,7 @@ jobs: path: release/${{ matrix.name }} docker-build-push: - needs: [ version, build-binaries ] + needs: [ build-binaries ] runs-on: ubuntu-latest steps: - name: Checkout code From 8c97043b2f9d4ad8f022a1c417d8d548665d6458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= Date: Thu, 12 Jun 2025 21:39:19 +0200 Subject: [PATCH 10/23] refactor(rust): remove support for armv7 architecture in CI configuration --- .github/workflows/create_dev.yml | 8 ++------ .github/workflows/create_release.yml | 6 ++---- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/workflows/create_dev.yml b/.github/workflows/create_dev.yml index f6b13e7..9270c50 100644 --- a/.github/workflows/create_dev.yml +++ b/.github/workflows/create_dev.yml @@ -72,7 +72,7 @@ jobs: - name: Configurer QEMU uses: docker/setup-qemu-action@v3 with: - platforms: arm64,arm/v7 + platforms: arm64 - name: Configurer Docker Buildx uses: docker/setup-buildx-action@v3 @@ -93,7 +93,6 @@ jobs: mkdir -p binaries-docker cp binaries/github-ntfy-amd64/github-ntfy-amd64 binaries-docker/ cp binaries/github-ntfy-arm64/github-ntfy-arm64 binaries-docker/ - cp binaries/github-ntfy-armv7/github-ntfy-armv7 binaries-docker/ chmod +x binaries-docker/* - name: Créer Dockerfile spécifique pour chaque architecture @@ -109,7 +108,6 @@ jobs: # Copier le binaire approprié selon l'architecture COPY binaries-docker/github-ntfy-amd64 /usr/local/bin/github-ntfy-amd64 COPY binaries-docker/github-ntfy-arm64 /usr/local/bin/github-ntfy-arm64 - COPY binaries-docker/github-ntfy-armv7 /usr/local/bin/github-ntfy-armv7 # Installer des outils pour débogage et déplacer le bon binaire RUN apk add --no-cache sqlite-libs openssl nginx && \ @@ -117,8 +115,6 @@ jobs: cp /usr/local/bin/github-ntfy-amd64 /usr/local/bin/github-ntfy; \ elif [ "$TARGETARCH" = "arm64" ]; then \ cp /usr/local/bin/github-ntfy-arm64 /usr/local/bin/github-ntfy; \ - elif [ "$TARGETARCH" = "arm" ] && [ "$TARGETVARIANT" = "v7" ]; then \ - cp /usr/local/bin/github-ntfy-armv7 /usr/local/bin/github-ntfy; \ fi && \ chmod +x /usr/local/bin/github-ntfy && \ rm /usr/local/bin/github-ntfy-* @@ -140,7 +136,7 @@ jobs: with: context: . push: true - platforms: linux/amd64,linux/arm64,linux/arm/v7 + platforms: linux/amd64,linux/arm64 tags: | ${{ secrets.DOCKER_USERNAME }}/github-ntfy:dev ${{ secrets.DOCKER_USERNAME }}/github-ntfy:${{ needs.version.outputs.version }} diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 4df3cdf..f5b2f5e 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -104,7 +104,7 @@ jobs: - name: Configurer QEMU uses: docker/setup-qemu-action@v3 with: - platforms: arm64,arm/v7 + platforms: arm64 - name: Configurer Docker Buildx uses: docker/setup-buildx-action@v3 @@ -125,7 +125,6 @@ jobs: mkdir -p binaries-docker cp binaries/github-ntfy-amd64/github-ntfy-amd64 binaries-docker/ cp binaries/github-ntfy-arm64/github-ntfy-arm64 binaries-docker/ - cp binaries/github-ntfy-armv7/github-ntfy-armv7 binaries-docker/ chmod +x binaries-docker/* ls -la binaries-docker/ @@ -135,7 +134,7 @@ jobs: with: context: . push: true - platforms: linux/amd64,linux/arm64,linux/arm/v7 + platforms: linux/amd64,linux/arm64 tags: | ${{ secrets.DOCKER_USERNAME }}/github-ntfy:latest ${{ secrets.DOCKER_USERNAME }}/github-ntfy:${{ needs.version.outputs.version }} @@ -161,7 +160,6 @@ jobs: files: | binaries/github-ntfy-amd64/github-ntfy-amd64 binaries/github-ntfy-arm64/github-ntfy-arm64 - binaries/github-ntfy-armv7/github-ntfy-armv7 draft: false prerelease: false generate_release_notes: true From acbd6ccc0000b06bc941a5ac2c08aa96da34dab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= Date: Thu, 12 Jun 2025 21:44:39 +0200 Subject: [PATCH 11/23] refactor(ci): update Docker image tags to use breizhhardware namespace --- .github/workflows/create_dev.yml | 4 ++-- .github/workflows/create_release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/create_dev.yml b/.github/workflows/create_dev.yml index 9270c50..1f98ad3 100644 --- a/.github/workflows/create_dev.yml +++ b/.github/workflows/create_dev.yml @@ -138,6 +138,6 @@ jobs: push: true platforms: linux/amd64,linux/arm64 tags: | - ${{ secrets.DOCKER_USERNAME }}/github-ntfy:dev - ${{ secrets.DOCKER_USERNAME }}/github-ntfy:${{ needs.version.outputs.version }} + breizhhardware/github-ntfy:dev + breizhhardware/github-ntfy:${{ needs.version.outputs.version }} file: Dockerfile.multi \ No newline at end of file diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index f5b2f5e..a77f726 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -136,8 +136,8 @@ jobs: push: true platforms: linux/amd64,linux/arm64 tags: | - ${{ secrets.DOCKER_USERNAME }}/github-ntfy:latest - ${{ secrets.DOCKER_USERNAME }}/github-ntfy:${{ needs.version.outputs.version }} + breizhhardware/github-ntfy:latest + breizhhardware/github-ntfy:${{ needs.version.outputs.version }} file: Dockerfile.multi create-release: From 2b9eb943376e4373097de2bc4f61a5d9dfe661c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= Date: Thu, 12 Jun 2025 21:54:33 +0200 Subject: [PATCH 12/23] refactor(ci): remove dynamic version tag from Docker image in create_dev.yml --- .github/workflows/create_dev.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/create_dev.yml b/.github/workflows/create_dev.yml index 1f98ad3..ce4651d 100644 --- a/.github/workflows/create_dev.yml +++ b/.github/workflows/create_dev.yml @@ -139,5 +139,4 @@ jobs: platforms: linux/amd64,linux/arm64 tags: | breizhhardware/github-ntfy:dev - breizhhardware/github-ntfy:${{ needs.version.outputs.version }} file: Dockerfile.multi \ No newline at end of file From 60db3550c0616b3fb0c6933c5643cb18b8ed91e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Fri, 13 Jun 2025 08:50:42 +0200 Subject: [PATCH 13/23] refactor(docker): update Dockerfile for architecture-specific binary handling and add OpenSSL dependency --- Cargo.lock | 11 +++++++++++ Dockerfile.multi | 45 +++++++++++++++++++++------------------------ 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0ecc891..2af8ddc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -405,6 +405,7 @@ dependencies = [ "dotenv", "env_logger", "log", + "openssl", "reqwest", "rusqlite", "serde", @@ -927,6 +928,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-src" +version = "300.5.0+3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8ce546f549326b0e6052b649198487d91320875da901e7bd11a06d1ee3f9c2f" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.109" @@ -935,6 +945,7 @@ checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] diff --git a/Dockerfile.multi b/Dockerfile.multi index 612f5a1..dad67c3 100644 --- a/Dockerfile.multi +++ b/Dockerfile.multi @@ -1,33 +1,33 @@ -FROM alpine:3.22 +FROM alpine:3.22 AS base -# Installer les dépendances nécessaires -RUN apk add --no-cache sqlite-libs openssl nginx +# Copier les binaires précompilés selon l'architecture +COPY binaries-docker/github-ntfy-amd64 /usr/local/bin/github-ntfy-amd64 +COPY binaries-docker/github-ntfy-arm64 /usr/local/bin/github-ntfy-arm64 -# Copier les fichiers web statiques +# Installation des dépendances et configuration du binaire selon l'architecture +RUN apk add --no-cache sqlite-libs openssl nginx && \ + if [ "$(uname -m)" = "x86_64" ]; then \ + cp /usr/local/bin/github-ntfy-amd64 /usr/local/bin/github-ntfy; \ + elif [ "$(uname -m)" = "aarch64" ]; then \ + cp /usr/local/bin/github-ntfy-arm64 /usr/local/bin/github-ntfy; \ + fi && \ + chmod +x /usr/local/bin/github-ntfy && \ + rm /usr/local/bin/github-ntfy-* + +WORKDIR /app + +# Copier les fichiers statiques COPY index.html /var/www/html/index.html COPY script.js /var/www/html/script.js -# Copier les binaires compilés en fonction de l'architecture -ARG TARGETARCH -ARG TARGETVARIANT -COPY binaries-docker/github-ntfy-${TARGETARCH}${TARGETVARIANT} /usr/local/bin/github-ntfy - -# Corriger pour arm/v7 -RUN if [ -f /usr/local/bin/github-ntfyv7 ]; then \ - mv /usr/local/bin/github-ntfyv7 /usr/local/bin/github-ntfy; \ - fi - -# Rendre le binaire exécutable -RUN chmod +x /usr/local/bin/github-ntfy - # Copier la configuration nginx COPY nginx.conf /etc/nginx/nginx.conf -# Copier et rendre exécutable le script d'entrée +# Copier le script d'entrée COPY entrypoint.sh / RUN chmod 700 /entrypoint.sh -# Définir les variables d'environnement +# Variables d'environnement ENV USERNAME="" \ PASSWORD="" \ NTFY_URL="" \ @@ -38,13 +38,10 @@ ENV USERNAME="" \ GOTIFY_URL="" \ GOTIFY_TOKEN="" \ DISCORD_WEBHOOK_URL="" \ - SLACK_WEBHOOK_URL="" \ - DB_PATH="/data" + SLACK_WEBHOOK_URL="" -# Créer le répertoire des données -RUN mkdir -p /data && chmod 755 /data +RUN mkdir -p /github-ntfy && chmod 755 /github-ntfy -# Exposer les ports pour l'API et le serveur web EXPOSE 8080 80 ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file From fe33377fa012bb09907a10988c4845af2f671f39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Fri, 13 Jun 2025 13:55:10 +0200 Subject: [PATCH 14/23] refactor(ci): simplify CI configuration by consolidating binary build steps and updating Dockerfile --- .github/workflows/create_dev.yml | 85 +++++----------------------- .github/workflows/create_release.yml | 46 +++++---------- .gitignore | 3 + Dockerfile | 66 +++++---------------- Dockerfile.multi | 47 --------------- index.html => web/index.html | 2 +- script.js => web/script.js | 0 7 files changed, 47 insertions(+), 202 deletions(-) delete mode 100644 Dockerfile.multi rename index.html => web/index.html (98%) rename script.js => web/script.js (100%) diff --git a/.github/workflows/create_dev.yml b/.github/workflows/create_dev.yml index ce4651d..cdfe850 100644 --- a/.github/workflows/create_dev.yml +++ b/.github/workflows/create_dev.yml @@ -6,18 +6,8 @@ on: - dev jobs: - build-binaries: + build-binary: runs-on: ubuntu-latest - strategy: - matrix: - include: - - target: x86_64-unknown-linux-musl - platform: linux/amd64 - name: github-ntfy-amd64 - - target: aarch64-unknown-linux-musl - platform: linux/arm64 - name: github-ntfy-arm64 - steps: - name: Checkout code uses: actions/checkout@v4 @@ -26,7 +16,7 @@ jobs: uses: actions-rs/toolchain@v1 with: toolchain: stable - target: ${{ matrix.target }} + target: x86_64-unknown-linux-musl override: true - name: Installer cross @@ -49,32 +39,27 @@ jobs: RUSTFLAGS: "-C target-feature=+crt-static" OPENSSL_NO_VENDOR: 0 run: | - cross build --release --target ${{ matrix.target }} --features vendored-openssl + cross build --release --target x86_64-unknown-linux-musl --features vendored-openssl - name: Préparer le binaire run: | mkdir -p release - cp target/${{ matrix.target }}/release/github-ntfy release/${{ matrix.name }} + cp target/x86_64-unknown-linux-musl/release/github-ntfy release/github-ntfy - name: Upload binaire comme artifact uses: actions/upload-artifact@v4 with: - name: ${{ matrix.name }} - path: release/${{ matrix.name }} + name: github-ntfy + path: release/github-ntfy docker-build-push: - needs: [ build-binaries ] + needs: [build-binary] runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - - name: Configurer QEMU - uses: docker/setup-qemu-action@v3 - with: - platforms: arm64 - - - name: Configurer Docker Buildx + - name: Configurer Docker uses: docker/setup-buildx-action@v3 - name: Login Docker Hub @@ -83,60 +68,20 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Télécharger tous les binaires + - name: Télécharger le binaire uses: actions/download-artifact@v4 with: + name: github-ntfy path: binaries - - name: Préparer les binaires pour Docker + - name: Préparer le binaire pour Docker run: | - mkdir -p binaries-docker - cp binaries/github-ntfy-amd64/github-ntfy-amd64 binaries-docker/ - cp binaries/github-ntfy-arm64/github-ntfy-arm64 binaries-docker/ - chmod +x binaries-docker/* + chmod +x binaries/github-ntfy - - name: Créer Dockerfile spécifique pour chaque architecture - run: | - # Créer un Dockerfile séparé pour chaque architecture - cat > Dockerfile.multi << 'EOF' - FROM --platform=${TARGETPLATFORM} alpine:3.22 - - ARG TARGETPLATFORM - ARG TARGETARCH - ARG TARGETVARIANT - - # Copier le binaire approprié selon l'architecture - COPY binaries-docker/github-ntfy-amd64 /usr/local/bin/github-ntfy-amd64 - COPY binaries-docker/github-ntfy-arm64 /usr/local/bin/github-ntfy-arm64 - - # Installer des outils pour débogage et déplacer le bon binaire - RUN apk add --no-cache sqlite-libs openssl nginx && \ - if [ "$TARGETARCH" = "amd64" ]; then \ - cp /usr/local/bin/github-ntfy-amd64 /usr/local/bin/github-ntfy; \ - elif [ "$TARGETARCH" = "arm64" ]; then \ - cp /usr/local/bin/github-ntfy-arm64 /usr/local/bin/github-ntfy; \ - fi && \ - chmod +x /usr/local/bin/github-ntfy && \ - rm /usr/local/bin/github-ntfy-* - - WORKDIR /app - COPY web /app/web - COPY config.toml /app/config.toml - - ENV DB_PATH=/data - VOLUME /data - EXPOSE 8080 - - ENTRYPOINT ["/usr/local/bin/github-ntfy"] - CMD ["--config", "/app/config.toml"] - EOF - - - name: Construire et pousser l'image Docker multi-architecture + - name: Construire et pousser l'image Docker uses: docker/build-push-action@v6 with: context: . push: true - platforms: linux/amd64,linux/arm64 - tags: | - breizhhardware/github-ntfy:dev - file: Dockerfile.multi \ No newline at end of file + tags: breizhhardware/github-ntfy:dev + file: Dockerfile \ No newline at end of file diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index a77f726..c0dcc37 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -40,15 +40,6 @@ jobs: build-binaries: needs: version runs-on: ubuntu-latest - strategy: - matrix: - include: - - target: x86_64-unknown-linux-musl - platform: linux/amd64 - name: github-ntfy-amd64 - - target: aarch64-unknown-linux-musl - platform: linux/arm64 - name: github-ntfy-arm64 steps: - name: Checkout code @@ -58,13 +49,13 @@ jobs: uses: actions-rs/toolchain@v1 with: toolchain: stable - target: ${{ matrix.target }} + target: x86_64-unknown-linux-musl override: true - name: Installer cross run: cargo install cross - - name: Créer Cross.toml pour spécifier OpenSSL statique + - name: Créer Cross.toml pour spécifier OpenSSL vendored run: | cat > Cross.toml << 'EOF' [build.env] @@ -75,24 +66,24 @@ jobs: ] EOF - - name: Construire avec cross + - name: Construire avec cross et OpenSSL vendored env: OPENSSL_STATIC: 1 RUSTFLAGS: "-C target-feature=+crt-static" OPENSSL_NO_VENDOR: 0 run: | - cross build --release --target ${{ matrix.target }} --features vendored-openssl + cross build --release --target x86_64-unknown-linux-musl --features vendored-openssl - name: Préparer le binaire run: | mkdir -p release - cp target/${{ matrix.target }}/release/github-ntfy release/${{ matrix.name }} + cp target/x86_64-unknown-linux-musl/release/github-ntfy release/github-ntfy - name: Upload binaire comme artifact uses: actions/upload-artifact@v4 with: - name: ${{ matrix.name }} - path: release/${{ matrix.name }} + name: github-ntfy + path: release/github-ntfy docker-build-push: needs: [version, build-binaries] @@ -101,11 +92,6 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Configurer QEMU - uses: docker/setup-qemu-action@v3 - with: - platforms: arm64 - - name: Configurer Docker Buildx uses: docker/setup-buildx-action@v3 @@ -118,27 +104,23 @@ jobs: - name: Télécharger tous les binaires uses: actions/download-artifact@v4 with: + name: github-ntfy path: binaries - - name: Préparer les binaires pour Docker + - name: Préparer le binaire pour Docker run: | - mkdir -p binaries-docker - cp binaries/github-ntfy-amd64/github-ntfy-amd64 binaries-docker/ - cp binaries/github-ntfy-arm64/github-ntfy-arm64 binaries-docker/ - chmod +x binaries-docker/* - ls -la binaries-docker/ + chmod +x binaries/github-ntfy # Construire et pousser l'image multi-architecture - - name: Construire et pousser l'image Docker multi-architecture + - name: Construire et pousser l'image Docker uses: docker/build-push-action@v6 with: context: . push: true - platforms: linux/amd64,linux/arm64 tags: | breizhhardware/github-ntfy:latest breizhhardware/github-ntfy:${{ needs.version.outputs.version }} - file: Dockerfile.multi + file: Dockerfile create-release: needs: [version, build-binaries] @@ -150,6 +132,7 @@ jobs: - name: Télécharger tous les binaires uses: actions/download-artifact@v4 with: + name: github-ntfy path: binaries - name: Créer une release GitHub @@ -158,8 +141,7 @@ jobs: tag_name: ${{ needs.version.outputs.version }} name: Release ${{ needs.version.outputs.version }} files: | - binaries/github-ntfy-amd64/github-ntfy-amd64 - binaries/github-ntfy-arm64/github-ntfy-arm64 + binaries/github-ntfy draft: false prerelease: false generate_release_notes: true diff --git a/.gitignore b/.gitignore index d345671..36ddc92 100644 --- a/.gitignore +++ b/.gitignore @@ -410,3 +410,6 @@ github-ntfy/* # Rust target target/* + +binaries +binaries/* diff --git a/Dockerfile b/Dockerfile index 42bb1c4..5f5d4d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,63 +1,25 @@ -FROM 1.87.0-alpine3.22 as builder +FROM alpine:3.22 -LABEL maintainer="BreizhHardware" -LABEL version_number="1.4" +# Copier le binaire +COPY binaries/github-ntfy /usr/local/bin/github-ntfy + +# Installer les dépendances +RUN apk add --no-cache sqlite-libs openssl nginx && \ + chmod +x /usr/local/bin/github-ntfy WORKDIR /app -# Installation of dependencies -RUN apk add --no-cache sqlite-dev musl-dev openssl-dev pkgconfig - -# Copy of the source files -COPY Cargo.toml Cargo.lock ./ - -# Create a temp source file to pre download dependencies -RUN mkdir src && \ - echo "fn main() {}" > src/main.rs && \ - cargo build --release && \ - rm -rf src - -# Copy real file -COPY src/ ./src/ - -# Build the application -RUN cargo build --release - -# Final image -FROM alpine:3.22 - -# Install of runtime dependencies -RUN apk add --no-cache sqlite-libs openssl nginx - -# Copy the static files -COPY index.html /var/www/html/index.html -COPY script.js /var/www/html/script.js - -# Copy the built application from the builder stage -COPY --from=builder /app/target/release/github-ntfy /usr/local/bin/github-ntfy - -# Configure Nginx +# Copier les fichiers web dans le répertoire attendu par nginx +COPY web/* /var/www/html/ COPY nginx.conf /etc/nginx/nginx.conf -# Copy the entrypoint script -COPY entrypoint.sh / -RUN chmod 700 /entrypoint.sh - -# Define the working directory -ENV USERNAME="" \ - PASSWORD="" \ - NTFY_URL="" \ - GHNTFY_TIMEOUT="3600" \ - GHNTFY_TOKEN="" \ - DOCKER_USERNAME="" \ - DOCKER_PASSWORD="" \ - GOTIFY_URL="" \ - GOTIFY_TOKEN="" \ - DISCORD_WEBHOOK_URL="" \ - SLACK_WEBHOOK_URL="" +# Copier le script d'entrée +COPY entrypoint.sh /app/entrypoint.sh +RUN chmod +x /app/entrypoint.sh +# Créer le répertoire de données RUN mkdir -p /github-ntfy && chmod 755 /github-ntfy EXPOSE 5000 80 -ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/Dockerfile.multi b/Dockerfile.multi deleted file mode 100644 index dad67c3..0000000 --- a/Dockerfile.multi +++ /dev/null @@ -1,47 +0,0 @@ -FROM alpine:3.22 AS base - -# Copier les binaires précompilés selon l'architecture -COPY binaries-docker/github-ntfy-amd64 /usr/local/bin/github-ntfy-amd64 -COPY binaries-docker/github-ntfy-arm64 /usr/local/bin/github-ntfy-arm64 - -# Installation des dépendances et configuration du binaire selon l'architecture -RUN apk add --no-cache sqlite-libs openssl nginx && \ - if [ "$(uname -m)" = "x86_64" ]; then \ - cp /usr/local/bin/github-ntfy-amd64 /usr/local/bin/github-ntfy; \ - elif [ "$(uname -m)" = "aarch64" ]; then \ - cp /usr/local/bin/github-ntfy-arm64 /usr/local/bin/github-ntfy; \ - fi && \ - chmod +x /usr/local/bin/github-ntfy && \ - rm /usr/local/bin/github-ntfy-* - -WORKDIR /app - -# Copier les fichiers statiques -COPY index.html /var/www/html/index.html -COPY script.js /var/www/html/script.js - -# Copier la configuration nginx -COPY nginx.conf /etc/nginx/nginx.conf - -# Copier le script d'entrée -COPY entrypoint.sh / -RUN chmod 700 /entrypoint.sh - -# Variables d'environnement -ENV USERNAME="" \ - PASSWORD="" \ - NTFY_URL="" \ - GHNTFY_TIMEOUT="3600" \ - GHNTFY_TOKEN="" \ - DOCKER_USERNAME="" \ - DOCKER_PASSWORD="" \ - GOTIFY_URL="" \ - GOTIFY_TOKEN="" \ - DISCORD_WEBHOOK_URL="" \ - SLACK_WEBHOOK_URL="" - -RUN mkdir -p /github-ntfy && chmod 755 /github-ntfy - -EXPOSE 8080 80 - -ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/index.html b/web/index.html similarity index 98% rename from index.html rename to web/index.html index 2fd8c9b..6bed7ae 100644 --- a/index.html +++ b/web/index.html @@ -5,7 +5,7 @@ Github-Ntfy Add a Repo - +
diff --git a/script.js b/web/script.js similarity index 100% rename from script.js rename to web/script.js From 43275d1fd96777def41ee599d33fe701256343ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Sat, 14 Jun 2025 11:45:13 +0200 Subject: [PATCH 15/23] refactor(github): enhance error handling and add User-Agent header in get_latest_releases function --- src/github.rs | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/github.rs b/src/github.rs index 77aa2b3..6d576d2 100644 --- a/src/github.rs +++ b/src/github.rs @@ -1,16 +1,26 @@ -use log::error; +use log::{error, info}; use reqwest::header::HeaderMap; use crate::models::{GithubRelease, GithubReleaseInfo}; pub async fn get_latest_releases( repos: &[String], client: &reqwest::Client, - headers: HeaderMap, + mut headers: HeaderMap ) -> Vec { let mut releases = Vec::new(); + if !headers.contains_key("User-Agent") { + headers.insert("User-Agent", "github-ntfy/1.0".parse().unwrap()); + } + + let has_auth = headers.contains_key("Authorization"); + if !has_auth { + info!("Aucun token GitHub configuré, les requêtes seront limitées"); + } + for repo in repos { let url = format!("https://api.github.com/repos/{}/releases/latest", repo); + match client.get(&url).headers(headers.clone()).send().await { Ok(response) => { if response.status().is_success() { @@ -27,11 +37,14 @@ pub async fn get_latest_releases( }); } } else { - error!("Error fetching GitHub release for {}: {}", repo, response.status()); + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + error!("Erreur lors de la récupération de la release GitHub pour {}: {} - {}", + repo, status, body); } - } + }, Err(e) => { - error!("Error fetching GitHub release for {}: {}", repo, e); + error!("Erreur de connexion pour {}: {}", repo, e); } } } From 79e48391cb5f881172d5daf78146ce85dc31b025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Sat, 14 Jun 2025 13:58:36 +0200 Subject: [PATCH 16/23] refactor(database): add version tracking functions and update notification logic --- src/database.rs | 24 ++++++- src/main.rs | 4 +- src/models.rs | 6 ++ src/notifications/mod.rs | 132 +++++++++++++++++++++------------------ 4 files changed, 103 insertions(+), 63 deletions(-) diff --git a/src/database.rs b/src/database.rs index 5669d2b..7594026 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,5 +1,5 @@ use log::info; -use rusqlite::{Connection, Result as SqliteResult, OpenFlags}; +pub(crate) use rusqlite::{Connection, Result as SqliteResult, OpenFlags}; use std::env; use std::path::Path; @@ -78,4 +78,26 @@ pub fn get_docker_watched_repos(conn: &Connection) -> SqliteResult> repos.push(repo?); } Ok(repos) +} + +pub fn is_new_version(conn: &Connection, repo: &str, version: &str) -> SqliteResult { + let mut stmt = conn.prepare("SELECT version FROM versions WHERE repo = ?")?; + let result = stmt.query_map([repo], |row| row.get::<_, String>(0))?; + + for stored_version in result { + if let Ok(v) = stored_version { + return Ok(v != version); + } + } + + Ok(true) +} + +pub fn update_version(conn: &Connection, repo: &str, version: &str, changelog: Option<&str>) -> SqliteResult<()> { + conn.execute( + "REPLACE INTO versions (repo, version, changelog) VALUES (?, ?, ?)", + [repo, version, changelog.unwrap_or("")], + )?; + + Ok(()) } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 9918a10..e68c773 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,7 +29,7 @@ async fn main() -> Result<(), Box> { env_logger::init(); let config = config::Config::from_env(); - let (_conn_versions, conn_repos) = database::init_databases()?; + let (conn_versions, conn_repos) = database::init_databases()?; start_api(); @@ -57,7 +57,7 @@ async fn main() -> Result<(), Box> { let github_releases = github::get_latest_releases(&github_repos, &client, config.github_headers()).await; let docker_releases = docker::get_latest_docker_releases(&docker_repos, &client, config.docker_headers()).await; - notifications::send_notifications(github_releases, docker_releases, &config).await; + notifications::send_notifications(github_releases, docker_releases, &config, &conn_versions).await; tokio::time::sleep(Duration::from_secs_f64(config.timeout)).await; } diff --git a/src/models.rs b/src/models.rs index fc75249..bfaec27 100644 --- a/src/models.rs +++ b/src/models.rs @@ -33,4 +33,10 @@ pub struct DockerReleaseInfo { pub digest: String, pub html_url: String, pub published_at: String, +} + +pub struct NotifiedRelease { + pub repo: String, + pub tag_name: String, + pub notified_at: chrono::DateTime, } \ No newline at end of file diff --git a/src/notifications/mod.rs b/src/notifications/mod.rs index 5cc53a6..e45e38b 100644 --- a/src/notifications/mod.rs +++ b/src/notifications/mod.rs @@ -8,85 +8,95 @@ pub mod docker; use tokio::task; use crate::models::{GithubReleaseInfo, DockerReleaseInfo}; use crate::config::Config; +use crate::database::{Connection, is_new_version, update_version}; +use rusqlite::Result as SqliteResult; pub async fn send_notifications( github_releases: Vec, docker_releases: Vec, config: &Config, -) { + db_conn: &Connection, +) -> SqliteResult<()> { let mut tasks = Vec::new(); // Create tasks for GitHub notifications for release in &github_releases { - if let Some(url) = &config.ntfy_url { - let release = release.clone(); - let auth = config.auth.clone(); - let url = url.clone(); - tasks.push(task::spawn(async move { - github::send_to_ntfy(release, &auth, &url).await; - })); - } + if is_new_version(db_conn, &release.repo, &release.tag_name)? { + if let Some(url) = &config.ntfy_url { + let release = release.clone(); + let auth = config.auth.clone(); + let url = url.clone(); + tasks.push(task::spawn(async move { + github::send_to_ntfy(release, &auth, &url).await; + })); + } - if let (Some(gotify_url), Some(gotify_token)) = (&config.gotify_url, &config.gotify_token) { - let release = release.clone(); - let url = gotify_url.clone(); - let token = gotify_token.clone(); - tasks.push(task::spawn(async move { - github::send_to_gotify(release, &token, &url).await; - })); - } + if let (Some(gotify_url), Some(gotify_token)) = (&config.gotify_url, &config.gotify_token) { + let release = release.clone(); + let url = gotify_url.clone(); + let token = gotify_token.clone(); + tasks.push(task::spawn(async move { + github::send_to_gotify(release, &token, &url).await; + })); + } - if let Some(discord_url) = &config.discord_webhook_url { - let release = release.clone(); - let url = discord_url.clone(); - tasks.push(task::spawn(async move { - github::send_to_discord(release, &url).await; - })); - } + if let Some(discord_url) = &config.discord_webhook_url { + let release = release.clone(); + let url = discord_url.clone(); + tasks.push(task::spawn(async move { + github::send_to_discord(release, &url).await; + })); + } - if let Some(slack_url) = &config.slack_webhook_url { - let release = release.clone(); - let url = slack_url.clone(); - tasks.push(task::spawn(async move { - github::send_to_slack(release, &url).await; - })); + if let Some(slack_url) = &config.slack_webhook_url { + let release = release.clone(); + let url = slack_url.clone(); + tasks.push(task::spawn(async move { + github::send_to_slack(release, &url).await; + })); + } + + update_version(db_conn, &release.repo, &release.tag_name, Some(release.changelog.as_str()))?; } } - // Create tasks for Docker notifications for release in &docker_releases { - if let Some(url) = &config.ntfy_url { - let release = release.clone(); - let auth = config.auth.clone(); - let url = url.clone(); - tasks.push(task::spawn(async move { - docker::send_to_ntfy(release, &auth, &url).await; - })); - } + if is_new_version(db_conn, &release.repo, &release.digest)? { + if let Some(url) = &config.ntfy_url { + let release = release.clone(); + let auth = config.auth.clone(); + let url = url.clone(); + tasks.push(task::spawn(async move { + docker::send_to_ntfy(release, &auth, &url).await; + })); + } - if let (Some(gotify_url), Some(gotify_token)) = (&config.gotify_url, &config.gotify_token) { - let release = release.clone(); - let url = gotify_url.clone(); - let token = gotify_token.clone(); - tasks.push(task::spawn(async move { - docker::send_to_gotify(release, &token, &url).await; - })); - } + if let (Some(gotify_url), Some(gotify_token)) = (&config.gotify_url, &config.gotify_token) { + let release = release.clone(); + let url = gotify_url.clone(); + let token = gotify_token.clone(); + tasks.push(task::spawn(async move { + docker::send_to_gotify(release, &token, &url).await; + })); + } - if let Some(discord_url) = &config.discord_webhook_url { - let release = release.clone(); - let url = discord_url.clone(); - tasks.push(task::spawn(async move { - docker::send_to_discord(release, &url).await; - })); - } + if let Some(discord_url) = &config.discord_webhook_url { + let release = release.clone(); + let url = discord_url.clone(); + tasks.push(task::spawn(async move { + docker::send_to_discord(release, &url).await; + })); + } - if let Some(slack_url) = &config.slack_webhook_url { - let release = release.clone(); - let url = slack_url.clone(); - tasks.push(task::spawn(async move { - docker::send_to_slack(release, &url).await; - })); + if let Some(slack_url) = &config.slack_webhook_url { + let release = release.clone(); + let url = slack_url.clone(); + tasks.push(task::spawn(async move { + docker::send_to_slack(release, &url).await; + })); + } + + update_version(db_conn, &release.repo, &release.digest, None)?; } } @@ -94,4 +104,6 @@ pub async fn send_notifications( for task in tasks { let _ = task.await; } + + Ok(()) } \ No newline at end of file From 6a0031ac5d74044cdee7f57b5aee6f1a145cc43a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Sun, 15 Jun 2025 17:42:47 +0200 Subject: [PATCH 17/23] Update src/database.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/database.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/database.rs b/src/database.rs index 7594026..a8eb9d6 100644 --- a/src/database.rs +++ b/src/database.rs @@ -59,8 +59,8 @@ pub fn init_databases() -> SqliteResult<(Connection, Connection)> { // Functions to retrieve watched repositories pub fn get_watched_repos(conn: &Connection) -> SqliteResult> { - let mut stmt = conn.prepare("SELECT * FROM watched_repos")?; - let repos_iter = stmt.query_map([], |row| Ok(row.get::<_, String>(1)?))?; + let mut stmt = conn.prepare("SELECT repo FROM watched_repos")?; + let repos_iter = stmt.query_map([], |row| Ok(row.get::<_, String>(0)?))?; let mut repos = Vec::new(); for repo in repos_iter { From 21b51766bbf0089466792c409b95203927b05093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Sun, 15 Jun 2025 17:43:00 +0200 Subject: [PATCH 18/23] Update src/api.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/api.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api.rs b/src/api.rs index 7028dab..ca781d2 100644 --- a/src/api.rs +++ b/src/api.rs @@ -118,7 +118,7 @@ async fn add_github_repo(body: RepoRequest, db: Arc>) -> Resul Err(e) => { error!("Error while checking repository: {}", e); return Ok(warp::reply::with_status( - warp::reply::json(&json!({"error": format!("Database error: {}", e)})), + warp::reply::json(&json!({"error": "An internal server error occurred."})), StatusCode::INTERNAL_SERVER_ERROR )); }, From 34729a7edd19e18d09f59098e8fb5340464e47d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= Date: Sun, 15 Jun 2025 17:43:36 +0200 Subject: [PATCH 19/23] fix(api): update default database path to '/github-ntfy' --- src/api.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api.rs b/src/api.rs index ca781d2..026afcf 100644 --- a/src/api.rs +++ b/src/api.rs @@ -16,7 +16,7 @@ struct RepoRequest { pub async fn start_api() -> Result<(), Box> { // Open the database - let db_path = env::var("DB_PATH").unwrap_or_else(|_| "./data".to_string()); + let db_path = env::var("DB_PATH").unwrap_or_else(|_| "/github-ntfy".to_string()); std::fs::create_dir_all(&db_path).ok(); let repos_path = format!("{}/watched_repos.db", db_path); From e4d2bc303f07e65f633be68430849df178a64ab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Sun, 15 Jun 2025 17:51:39 +0200 Subject: [PATCH 20/23] Update src/config.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index 6d2b82b..cf3675f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,5 @@ use dotenv::dotenv; -use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE}; +use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; use std::env; use std::fs::File; use std::io::Read; From 0a5945e7b3f2d5bed611d38816e72a48c262640b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Sun, 15 Jun 2025 17:53:47 +0200 Subject: [PATCH 21/23] Update src/database.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/database.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/database.rs b/src/database.rs index a8eb9d6..2742a85 100644 --- a/src/database.rs +++ b/src/database.rs @@ -70,8 +70,8 @@ pub fn get_watched_repos(conn: &Connection) -> SqliteResult> { } pub fn get_docker_watched_repos(conn: &Connection) -> SqliteResult> { - let mut stmt = conn.prepare("SELECT * FROM docker_watched_repos")?; - let repos_iter = stmt.query_map([], |row| Ok(row.get::<_, String>(1)?))?; + let mut stmt = conn.prepare("SELECT repo FROM docker_watched_repos")?; + let repos_iter = stmt.query_map([], |row| Ok(row.get::<_, String>(0)?))?; let mut repos = Vec::new(); for repo in repos_iter { From 82f5f594136b038f529a19ae0e594e4624df344b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Mon, 16 Jun 2025 09:18:06 +0200 Subject: [PATCH 22/23] update(create_release): modify workflow name and add dev tag for Docker image --- .github/workflows/create_release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index c0dcc37..4defbf8 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -1,4 +1,4 @@ -name: Build et Release Multi-Architectures +name: Build et Release on: push: @@ -119,6 +119,7 @@ jobs: push: true tags: | breizhhardware/github-ntfy:latest + breizhhardware/github-ntfy:dev breizhhardware/github-ntfy:${{ needs.version.outputs.version }} file: Dockerfile From f52f505e389df06883fd97c7451b2789c0c0bb01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20MARQUET?= <72651575+BreizhHardware@users.noreply.github.com> Date: Mon, 16 Jun 2025 09:28:45 +0200 Subject: [PATCH 23/23] update(README): simplify Docker image description and remove unused DB_PATH entry --- README.md | 37 ++++++++----------------------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 0c31754..e569c43 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ ### Docker (recommended) -Use our multi-architecture Docker image, which automatically supports amd64, arm64 and armv7: +Use our Docker image, which automatically supports amd64, arm64 and armv7: ```yaml services: @@ -34,7 +34,6 @@ services: - GOTIFY_TOKEN= # Required if gotify is used - DISCORD_WEBHOOK_URL= # Required if discord is used - SLACK_WEBHOOK_URL= # Required if Slack is used - - DB_PATH=/data # Database path volumes: - /path/to/data:/data ports: @@ -64,39 +63,19 @@ Run ./target/release/github-ntfy ``` -## REST API -The application exposes a REST API on port 8080 to manage watched repositories: - - -Endpoint -Method -Description -/app_repo -POST -Add a GitHub repository to watch -/app_docker_repo -POST -Add a Docker repository to watch -/watched_repos -GET -List all watched GitHub repositories -/watched_docker_repos -GET -List all watched Docker repositories -/delete_repo -POST -Delete a GitHub repository -/delete_docker_repo -POST -Delete a Docker repository - ## Version Notes - v2.0: Complete rewrite in Rust for better performance and reduced resource consumption -- v1.5: Stable Python version +- [v1.7.1](https://github.com/BreizhHardware/ntfy_alerts/tree/v1.7.2): Stable Python version ## Configuration The GitHub token (GHNTFY_TOKEN) needs to have the following permissions: repo, read:org and read:user. +## TODO +- [ ] Add support for multi achitecture Docker images +- [ ] Rework web interface +- [ ] Add support for more notification services (Telegram, Matrix, etc.) +- [ ] Add web oneboarding instead of using environment variables + ## Author 👤 BreizhHardware