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; } } }