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
new file mode 100644
index 0000000..cdfe850
--- /dev/null
+++ b/.github/workflows/create_dev.yml
@@ -0,0 +1,87 @@
+name: Build et Push Docker Dev Image
+
+on:
+ push:
+ branches:
+ - dev
+
+jobs:
+ build-binary:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Installer Rust
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ target: x86_64-unknown-linux-musl
+ override: true
+
+ - name: Installer cross
+ run: cargo install cross
+
+ - 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 et OpenSSL vendored
+ env:
+ OPENSSL_STATIC: 1
+ RUSTFLAGS: "-C target-feature=+crt-static"
+ OPENSSL_NO_VENDOR: 0
+ run: |
+ cross build --release --target x86_64-unknown-linux-musl --features vendored-openssl
+
+ - name: Préparer le binaire
+ run: |
+ mkdir -p release
+ 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: github-ntfy
+ path: release/github-ntfy
+
+ docker-build-push:
+ needs: [build-binary]
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Configurer Docker
+ 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 le binaire
+ uses: actions/download-artifact@v4
+ with:
+ name: github-ntfy
+ path: binaries
+
+ - name: Préparer le binaire pour Docker
+ run: |
+ chmod +x binaries/github-ntfy
+
+ - name: Construire et pousser l'image Docker
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ push: true
+ 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 d8d60e3..4defbf8 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
on:
push:
@@ -6,25 +6,145 @@ 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
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: x86_64-unknown-linux-musl
+ override: true
+
+ - name: Installer cross
+ run: cargo install cross
+
+ - 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 et OpenSSL vendored
+ env:
+ OPENSSL_STATIC: 1
+ RUSTFLAGS: "-C target-feature=+crt-static"
+ OPENSSL_NO_VENDOR: 0
+ run: |
+ cross build --release --target x86_64-unknown-linux-musl --features vendored-openssl
+
+ - name: Préparer le binaire
+ run: |
+ mkdir -p release
+ 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: github-ntfy
+ path: release/github-ntfy
+
+ docker-build-push:
+ needs: [version, build-binaries]
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - 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@v4
+ with:
+ name: github-ntfy
+ path: binaries
+
+ - name: Préparer le binaire pour Docker
+ run: |
+ chmod +x binaries/github-ntfy
+
+ # Construire et pousser l'image multi-architecture
+ - name: Construire et pousser l'image Docker
uses: docker/build-push-action@v6
with:
context: .
push: true
- tags: ${{ secrets.DOCKER_USERNAME }}/github-ntfy:latest
\ No newline at end of file
+ tags: |
+ breizhhardware/github-ntfy:latest
+ breizhhardware/github-ntfy:dev
+ breizhhardware/github-ntfy:${{ needs.version.outputs.version }}
+ file: Dockerfile
+
+ 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@v4
+ with:
+ name: github-ntfy
+ 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
+ draft: false
+ prerelease: false
+ generate_release_notes: true
+ env:
+ GITHUB_TOKEN: ${{ secrets.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..36ddc92 100644
--- a/.gitignore
+++ b/.gitignore
@@ -405,4 +405,11 @@ docker-compose.yml
github-ntfy/
github-ntfy/*
-*.db
\ No newline at end of file
+*.db
+
+# Rust
+target
+target/*
+
+binaries
+binaries/*
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..2af8ddc
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,2115 @@
+# 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 = "2.0.0"
+dependencies = [
+ "chrono",
+ "dotenv",
+ "env_logger",
+ "log",
+ "openssl",
+ "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-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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
+dependencies = [
+ "cc",
+ "libc",
+ "openssl-src",
+ "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",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..b3444de
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "github-ntfy"
+version = "2.0.0"
+edition = "2021"
+
+[[bin]]
+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"] }
+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"
+openssl = { version = "0.10", features = ["vendored"] }
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index f8e3313..5f5d4d2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,39 +1,25 @@
-FROM python:3.11.8-alpine3.19
+FROM alpine:3.22
-LABEL maintainer="BreizhHardware"
-LABEL version_number="1.4"
+# Copier le binaire
+COPY binaries/github-ntfy /usr/local/bin/github-ntfy
-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
-RUN chmod 700 /entrypoint.sh
+# Installer les dépendances
+RUN apk add --no-cache sqlite-libs openssl nginx && \
+ chmod +x /usr/local/bin/github-ntfy
-# Définir les variables d'environnement pour username et password
-ENV USERNAME="" \
- PASSWORD="" \
- NTFY_URL="" \
- GHNTFY_TIMEOUT="3600" \
- GHNTFY_TOKEN="" \
- DOCKER_USERNAME="" \
- DOCKER_PASSWORD="" \
- GOTIFY_URL="" \
- GOTIFY_TOKEN="" \
- DISCORD_WEBHOOK_URL="" \
- SLACK_WEBHOOK_URL="" \
- FLASK_ENV=production
-
-# Exposer le port 5000 pour l'API et le port 80 pour le serveur web
-EXPOSE 5000 80
+WORKDIR /app
+# Copier les fichiers web dans le répertoire attendu par nginx
+COPY web/* /var/www/html/
COPY nginx.conf /etc/nginx/nginx.conf
-ENTRYPOINT ["/entrypoint.sh"]
+# 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 ["/app/entrypoint.sh"]
diff --git a/README.md b/README.md
index 22c488f..e569c43 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
Welcome to ntfy_alerts 👋
-
+
@@ -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 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
@@ -39,85 +35,58 @@ services:
- DISCORD_WEBHOOK_URL= # Required if discord is used
- SLACK_WEBHOOK_URL= # Required if Slack is used
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
+```
+
+## Version Notes
+- v2.0: Complete rewrite in Rust for better performance and reduced resource consumption
+- [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
-👤 **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/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_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()
-
diff --git a/src/api.rs b/src/api.rs
new file mode 100644
index 0000000..026afcf
--- /dev/null
+++ b/src/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(|_| "/github-ntfy".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": "An internal server error occurred."})),
+ 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/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..cf3675f
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,81 @@
+use dotenv::dotenv;
+use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
+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..2742a85
--- /dev/null
+++ b/src/database.rs
@@ -0,0 +1,103 @@
+use log::info;
+pub(crate) 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());
+
+ 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_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 (
+ 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_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 (
+ 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 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 {
+ repos.push(repo?);
+ }
+ Ok(repos)
+}
+
+pub fn get_docker_watched_repos(conn: &Connection) -> SqliteResult> {
+ 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 {
+ 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/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..6d576d2
--- /dev/null
+++ b/src/github.rs
@@ -0,0 +1,80 @@
+use log::{error, info};
+use reqwest::header::HeaderMap;
+use crate::models::{GithubRelease, GithubReleaseInfo};
+
+pub async fn get_latest_releases(
+ repos: &[String],
+ client: &reqwest::Client,
+ 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() {
+ 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 {
+ 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!("Erreur de connexion pour {}: {}", 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..e68c773
--- /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, &conn_versions).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..bfaec27
--- /dev/null
+++ b/src/models.rs
@@ -0,0 +1,42 @@
+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,
+}
+
+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/discord.rs b/src/notifications/discord.rs
new file mode 100644
index 0000000..94f5be1
--- /dev/null
+++ b/src/notifications/discord.rs
@@ -0,0 +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, 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_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*: {}\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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/notifications/docker.rs b/src/notifications/docker.rs
new file mode 100644
index 0000000..0777447
--- /dev/null
+++ b/src/notifications/docker.rs
@@ -0,0 +1,69 @@
+use tokio::task;
+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();
+
+ 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
diff --git a/src/notifications/github.rs b/src/notifications/github.rs
new file mode 100644
index 0000000..3911fe9
--- /dev/null
+++ b/src/notifications/github.rs
@@ -0,0 +1,69 @@
+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) {
+ ntfy::send_github_notification(&release, auth, ntfy_url).await;
+}
+
+pub async fn send_to_gotify(release: GithubReleaseInfo, token: &str, gotify_url: &str) {
+ gotify::send_github_notification(&release, token, gotify_url).await;
+}
+
+pub async fn send_to_discord(release: GithubReleaseInfo, webhook_url: &str) {
+ discord::send_github_notification(&release, webhook_url).await;
+}
+
+pub async fn send_to_slack(release: GithubReleaseInfo, webhook_url: &str) {
+ 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
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..e45e38b
--- /dev/null
+++ b/src/notifications/mod.rs
@@ -0,0 +1,109 @@
+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;
+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 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(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;
+ }));
+ }
+
+ update_version(db_conn, &release.repo, &release.tag_name, Some(release.changelog.as_str()))?;
+ }
+ }
+
+ for release in &docker_releases {
+ 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(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;
+ }));
+ }
+
+ update_version(db_conn, &release.repo, &release.digest, None)?;
+ }
+ }
+
+ // Wait for all tasks to complete
+ for task in tasks {
+ let _ = task.await;
+ }
+
+ Ok(())
+}
\ 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
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