From dcd2e241593c094373fe2996a42abf26aa3bed95 Mon Sep 17 00:00:00 2001 From: Shrev Dev Date: Tue, 21 Oct 2025 09:55:48 -0500 Subject: [PATCH] feat: Docker support and improve connection handling in UptimeKumaService. Added DockerHub instructions to README, implemented automatic reconnection with retry logic, and introduced force reconnect functionality. Updated authentication process to handle retries and added logging for better error tracking. --- .github/workflows/dockerhub.yml | 70 +++++++++++++++++++++++++++++ README.md | 19 ++++++++ src/index.ts | 23 +++++++++- src/services/uptime-kuma.service.ts | 50 +++++++++++++++++++-- 4 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/dockerhub.yml diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml new file mode 100644 index 0000000..f680ec3 --- /dev/null +++ b/.github/workflows/dockerhub.yml @@ -0,0 +1,70 @@ +name: Build and Push to DockerHub + +on: + push: + branches: [ main, develop ] + tags: [ 'v*' ] + pull_request: + branches: [ main ] + +env: + REGISTRY: docker.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to DockerHub + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable={{is_default_branch}} + type=sha,prefix={{branch}}- + + - name: Build and push Docker image + id: build + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Image digest + run: echo ${{ steps.build.outputs.digest }} + + - name: Output image info + if: github.event_name != 'pull_request' + run: | + echo "Image pushed successfully!" + echo "Tags: ${{ steps.meta.outputs.tags }}" + echo "Digest: ${{ steps.build.outputs.digest }}" diff --git a/README.md b/README.md index 309fedd..e59a103 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,25 @@ See **[QUICKSTART.md](QUICKSTART.md)** for detailed setup instructions. 3. Run: `docker-compose up -d` or `npm start` 4. Use `/set-channel` and `/track-all` in Discord +### DockerHub + +Pre-built images are automatically available on DockerHub: + +```bash +# Pull the latest image +docker pull boker02/uptime-kuma-discord-bot:latest + +# Run with environment variables +docker run -d \ + --name uptime-kuma-discord-bot \ + -e DISCORD_BOT_TOKEN=your_token \ + -e UPTIME_KUMA_URL=your_url \ + -e UPTIME_KUMA_USERNAME=your_username \ + -e UPTIME_KUMA_PASSWORD=your_password \ + -v bot-data:/app/data \ + boker02/uptime-kuma-discord-bot:latest +``` + ## Usage ### Basic Setup diff --git a/src/index.ts b/src/index.ts index 8169a0f..176f106 100644 --- a/src/index.ts +++ b/src/index.ts @@ -83,12 +83,33 @@ class UptimeKumaDiscordBot { } private startUpdateInterval(): void { - const updateFn = () => { + let consecutiveDisconnections = 0; + const maxConsecutiveDisconnections = 5; + + const updateFn = async () => { if (!this.uptimeKuma.isConnected()) { this.logger.warn('Uptime Kuma is not connected, skipping update'); + consecutiveDisconnections++; + + // If we've been disconnected for too long, try a force reconnect + if (consecutiveDisconnections >= maxConsecutiveDisconnections) { + this.logger.warn(`Uptime Kuma has been disconnected for ${consecutiveDisconnections} consecutive checks, attempting force reconnect...`); + try { + await this.uptimeKuma.forceReconnect(); + consecutiveDisconnections = 0; + this.logger.info('Force reconnect successful'); + } catch (error: any) { + this.logger.error(`Force reconnect failed: ${error.message}`); + } + } return; } + // Reset counter on successful connection + if (consecutiveDisconnections > 0) { + consecutiveDisconnections = 0; + } + const monitors = this.uptimeKuma.getMonitorStats(); this.discord.updateMonitorStatus(monitors).catch(error => { this.logger.error(`Failed to update Discord status: ${error.message}`); diff --git a/src/services/uptime-kuma.service.ts b/src/services/uptime-kuma.service.ts index c57149b..43f5050 100644 --- a/src/services/uptime-kuma.service.ts +++ b/src/services/uptime-kuma.service.ts @@ -12,6 +12,9 @@ export class UptimeKumaService extends EventEmitter { private isAuthenticated = false; private logger: Logger; private manualReconnectTimeout: NodeJS.Timeout | null = null; + private authRetryAttempts = 0; + private maxAuthRetries = 5; + private authRetryDelay = 10000; constructor() { super(); @@ -68,6 +71,11 @@ export class UptimeKumaService extends EventEmitter { if (!this.isConnected() && this.socket) { this.logger.info('Attempting manual reconnection...'); this.socket.connect(); + } else if (!this.isConnected() && !this.socket) { + this.logger.info('Socket is null, attempting full reconnection...'); + this.connect().catch(err => { + this.logger.error(`Manual reconnection failed: ${err.message}`); + }); } }, 30000); }); @@ -79,11 +87,11 @@ export class UptimeKumaService extends EventEmitter { this.manualReconnectTimeout = null; } - if (this.reconnectAttempts > 0) { - this.logger.info('Reconnected to Uptime Kuma, re-authenticating...'); + if (this.reconnectAttempts > 0 || !this.isAuthenticated) { + this.logger.info('Connected to Uptime Kuma, authenticating...'); this.reconnectAttempts = 0; - this.authenticate().catch(err => { - this.logger.error(`Re-authentication failed: ${err.message}`); + this.authenticateWithRetry().catch(err => { + this.logger.error(`Authentication failed after retries: ${err.message}`); }); } }); @@ -139,6 +147,7 @@ export class UptimeKumaService extends EventEmitter { if (response.ok) { this.isAuthenticated = true; + this.authRetryAttempts = 0; this.logger.info('Successfully authenticated with Uptime Kuma'); resolve(); } else { @@ -151,6 +160,31 @@ export class UptimeKumaService extends EventEmitter { }); } + private async authenticateWithRetry(): Promise { + if (this.authRetryAttempts >= this.maxAuthRetries) { + throw new Error(`Authentication failed after ${this.maxAuthRetries} attempts`); + } + + try { + await this.authenticate(); + } catch (error: any) { + this.authRetryAttempts++; + this.logger.warn(`Authentication attempt ${this.authRetryAttempts}/${this.maxAuthRetries} failed: ${error.message}`); + + if (this.authRetryAttempts < this.maxAuthRetries) { + this.logger.info(`Retrying authentication in ${this.authRetryDelay / 1000} seconds...`); + setTimeout(() => { + this.authenticateWithRetry().catch(err => { + this.logger.error(`Authentication retry failed: ${err.message}`); + }); + }, this.authRetryDelay); + } else { + this.logger.error(`Authentication failed after ${this.maxAuthRetries} attempts. Manual intervention required.`); + throw error; + } + } + } + public getAllMonitors(): Map { const allMonitors = new Map(); for (const [id, stats] of this.monitors.entries()) { @@ -261,6 +295,12 @@ export class UptimeKumaService extends EventEmitter { return this.socket !== null && this.socket.connected && this.isAuthenticated; } + public async forceReconnect(): Promise { + this.logger.info('Force reconnecting to Uptime Kuma...'); + this.disconnect(); + await this.connect(); + } + public disconnect(): void { if (this.manualReconnectTimeout) { clearTimeout(this.manualReconnectTimeout); @@ -272,6 +312,8 @@ export class UptimeKumaService extends EventEmitter { this.socket.disconnect(); this.socket = null; this.isAuthenticated = false; + this.authRetryAttempts = 0; + this.reconnectAttempts = 0; } } }