Skip to content

Commit b78f484

Browse files
committed
release
1 parent 1149683 commit b78f484

10 files changed

Lines changed: 421 additions & 0 deletions

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
GITHUB_TOKEN=ghp_****

.gitignore

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Dependencies
2+
node_modules/
3+
package-lock.json
4+
yarn.lock
5+
pnpm-lock.yaml
6+
7+
# Session files (WhatsApp authentication)
8+
session/
9+
*.data.json
10+
auth_info_baileys/
11+
12+
# Commit tracking
13+
last_commit.txt
14+
15+
# Environment variables
16+
.env
17+
.env.local
18+
.env.development
19+
.env.test
20+
.env.production
21+
22+
# Logs
23+
logs/
24+
*.log
25+
npm-debug.log*
26+
yarn-debug.log*
27+
yarn-error.log*
28+
pino-*.log
29+
30+
# Operating System
31+
.DS_Store
32+
.DS_Store?
33+
._*
34+
.Spotlight-V100
35+
.Trashes
36+
ehthumbs.db
37+
Thumbs.db
38+
desktop.ini
39+
40+
# IDE
41+
.vscode/
42+
.idea/
43+
*.swp
44+
*.swo
45+
*~
46+
.project
47+
.classpath
48+
.settings/
49+
*.sublime-project
50+
*.sublime-workspace
51+
52+
# Build
53+
dist/
54+
build/
55+
*.tsbuildinfo
56+
57+
# Testing
58+
coverage/
59+
.nyc_output/
60+
61+
# Temporary files
62+
tmp/
63+
temp/
64+
*.tmp
65+
66+
# Backup files
67+
*.bak
68+
*.backup
69+
*~

GitHubPoller.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import fs from "fs/promises";
2+
import { CONFIG } from "./config.js";
3+
import { GitHubAPI } from "./githubApi.js";
4+
import { MessageFormatter } from "./messageFormatter.js";
5+
import { getUptime } from "./utils.js";
6+
7+
export class GitHubPoller {
8+
constructor(sock) {
9+
this.sock = sock;
10+
this.lastCommit = null;
11+
this.isPolling = false;
12+
this.retryCount = 0;
13+
this.stats = {
14+
totalChecks: 0,
15+
newCommits: 0,
16+
errors: 0,
17+
startTime: Date.now(),
18+
};
19+
}
20+
21+
async initialize() {
22+
try {
23+
const data = await fs.readFile(CONFIG.LAST_FILE, "utf8");
24+
this.lastCommit = data.trim();
25+
console.log(`[INIT] Loaded SHA: ${this.lastCommit?.slice(0, 7) || "none"}`);
26+
} catch {
27+
console.log("[INIT] Starting fresh - no previous commit");
28+
}
29+
}
30+
31+
async notifyNewCommit(commit) {
32+
const details = await GitHubAPI.getCommitDetails(commit.sha);
33+
const message = MessageFormatter.formatCommitNotification(commit, details);
34+
35+
await this.sock.sendMessage(CONFIG.TARGET_JID, { text: message });
36+
37+
this.stats.newCommits++;
38+
console.log(`[✓] Commit sent: ${commit.sha.slice(0, 7)} | Total: ${this.stats.newCommits}`);
39+
}
40+
41+
async check() {
42+
if (this.isPolling) return;
43+
this.isPolling = true;
44+
this.stats.totalChecks++;
45+
46+
try {
47+
const commit = await GitHubAPI.fetchLatestCommit();
48+
49+
if (!commit) {
50+
console.log("[CHECK] No commits found");
51+
return;
52+
}
53+
54+
if (commit.sha !== this.lastCommit) {
55+
this.lastCommit = commit.sha;
56+
await fs.writeFile(CONFIG.LAST_FILE, commit.sha);
57+
await this.notifyNewCommit(commit);
58+
this.retryCount = 0;
59+
} else {
60+
process.stdout.write(`\r[CHECK] No new commits | Checks: ${this.stats.totalChecks} | Uptime: ${getUptime(this.stats.startTime)}`);
61+
}
62+
} catch (err) {
63+
this.retryCount++;
64+
this.stats.errors++;
65+
console.error(`\n[ERROR] Poll failed (${this.retryCount}/${CONFIG.MAX_RETRIES}): ${err.message}`);
66+
67+
if (this.retryCount >= CONFIG.MAX_RETRIES) {
68+
console.warn("[WARN] Max retries reached, resetting counter");
69+
this.retryCount = 0;
70+
}
71+
} finally {
72+
this.isPolling = false;
73+
const delay = this.retryCount > 0 ? CONFIG.RETRY_DELAY : CONFIG.POLL_INTERVAL;
74+
setTimeout(() => this.check(), delay);
75+
}
76+
}
77+
78+
start() {
79+
console.log(`[POLLER] Started polling every ${CONFIG.POLL_INTERVAL / 1000}s`);
80+
console.log(`[POLLER] Watching: ${CONFIG.OWNER}/${CONFIG.REPO}@${CONFIG.BRANCH}`);
81+
this.check();
82+
}
83+
84+
printStats() {
85+
console.log("\n╭─────────────────────────╮");
86+
console.log("│ POLLING STATISTICS │");
87+
console.log("├─────────────────────────┤");
88+
console.log(`│ Total Checks: ${this.stats.totalChecks}`);
89+
console.log(`│ New Commits: ${this.stats.newCommits}`);
90+
console.log(`│ Errors: ${this.stats.errors}`);
91+
console.log(`│ Uptime: ${getUptime(this.stats.startTime)}`);
92+
console.log("╰─────────────────────────╯\n");
93+
}
94+
}

config.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const CONFIG = {
2+
PAIRING_NUMBER: "62882003353414",
3+
OWNER: "VryptLab",
4+
REPO: "EternityBot",
5+
BRANCH: "main",
6+
TARGET_JID: "62882005514880@s.whatsapp.net",
7+
POLL_INTERVAL: 1_000,
8+
LAST_FILE: "./last_commit.txt",
9+
SESSION_DIR: "./session",
10+
MAX_RETRIES: 3,
11+
RETRY_DELAY: 5_000,
12+
GITHUB_TOKEN: process.env.GITHUB_TOKEN || null,
13+
};

githubApi.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import fetch from "node-fetch";
2+
import { CONFIG } from "./config.js";
3+
4+
export class GitHubAPI {
5+
static getHeaders() {
6+
const headers = {
7+
"User-Agent": "GitHub-Polling-Bot",
8+
"Accept": "application/vnd.github.v3+json",
9+
};
10+
11+
if (CONFIG.GITHUB_TOKEN) {
12+
headers["Authorization"] = `token ${CONFIG.GITHUB_TOKEN}`;
13+
}
14+
15+
return headers;
16+
}
17+
18+
static async fetchLatestCommit() {
19+
const res = await fetch(
20+
`https://api.github.com/repos/${CONFIG.OWNER}/${CONFIG.REPO}/commits?sha=${CONFIG.BRANCH}&per_page=1`,
21+
{ headers: this.getHeaders(), timeout: 15_000 }
22+
);
23+
24+
if (!res.ok) {
25+
const remaining = res.headers.get("x-ratelimit-remaining");
26+
const reset = res.headers.get("x-ratelimit-reset");
27+
28+
if (res.status === 403 && remaining === "0") {
29+
const resetDate = new Date(reset * 1000);
30+
throw new Error(`Rate limit exceeded. Reset at ${resetDate.toLocaleTimeString()}`);
31+
}
32+
33+
throw new Error(`GitHub API error: ${res.status} ${res.statusText}`);
34+
}
35+
36+
const commits = await res.json();
37+
return commits[0];
38+
}
39+
40+
static async getCommitDetails(sha) {
41+
try {
42+
const res = await fetch(
43+
`https://api.github.com/repos/${CONFIG.OWNER}/${CONFIG.REPO}/commits/${sha}`,
44+
{ headers: this.getHeaders(), timeout: 10_000 }
45+
);
46+
47+
if (res.ok) {
48+
return await res.json();
49+
}
50+
} catch (err) {
51+
console.warn("[WARN] Failed to fetch commit details:", err.message);
52+
}
53+
return null;
54+
}
55+
}

index.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { connectToWhatsApp } from "./whatsappConnection.js";
2+
3+
process.on("SIGINT", () => {
4+
console.log("\n[EXIT] Shutting down gracefully...");
5+
process.exit(0);
6+
});
7+
8+
process.on("SIGTERM", () => {
9+
console.log("\n[EXIT] Received SIGTERM, shutting down...");
10+
process.exit(0);
11+
});
12+
13+
process.on("unhandledRejection", (err) => {
14+
console.error("[FATAL] Unhandled rejection:", err);
15+
});
16+
17+
process.on("uncaughtException", (err) => {
18+
console.error("[FATAL] Uncaught exception:", err);
19+
process.exit(1);
20+
});
21+
22+
console.log("╭─────────────────────────╮");
23+
console.log("│ GitHub Polling Bot v2 │");
24+
console.log("╰─────────────────────────╯\n");
25+
26+
connectToWhatsApp().catch((err) => {
27+
console.error("[FATAL] Failed to start:", err);
28+
process.exit(1);
29+
});

messageFormatter.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { CONFIG } from "./config.js";
2+
import { truncate } from "./utils.js";
3+
4+
export class MessageFormatter {
5+
static formatCommitNotification(commit, details) {
6+
const { sha, commit: info, html_url, author: githubAuthor } = commit;
7+
const date = new Date(info.author.date).toLocaleString("id-ID", {
8+
timeZone: "Asia/Jakarta",
9+
dateStyle: "short",
10+
timeStyle: "short",
11+
});
12+
13+
const stats = details?.stats || {};
14+
const additions = stats.additions || 0;
15+
const deletions = stats.deletions || 0;
16+
const totalChanges = additions + deletions;
17+
const filesChanged = stats.total || 0;
18+
19+
const lines = [
20+
`╭─ *NEW COMMIT* ─╮`,
21+
`│`,
22+
`├ 📦 Repo: *${CONFIG.REPO}*`,
23+
`├ 🔖 SHA: \`${sha.slice(0, 7)}\``,
24+
`├ 👤 Author: ${githubAuthor?.login || info.author.name}`,
25+
`├ 🕐 ${date}`,
26+
];
27+
28+
if (totalChanges > 0) {
29+
lines.push(`│`);
30+
lines.push(`├ 📊 Stats:`);
31+
lines.push(`│ • ${filesChanged} file${filesChanged !== 1 ? 's' : ''} changed`);
32+
lines.push(`│ • ${additions} additions (+)`);
33+
lines.push(`│ • ${deletions} deletions (-)`);
34+
}
35+
36+
lines.push(`│`);
37+
lines.push(`├ 💬 Message:`);
38+
lines.push(`│ ${truncate(info.message, 80)}`);
39+
lines.push(`│`);
40+
lines.push(`╰─ 🔗 ${html_url}`);
41+
42+
return lines.join("\n");
43+
}
44+
}

package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "github-commit-bot",
3+
"version": "1.0.0",
4+
"description": "Real-time GitHub commit notifications delivered straight to your WhatsApp",
5+
"main": "index.js",
6+
"scripts": {
7+
"start": "node index.js"
8+
},
9+
"repository": {
10+
"type": "git",
11+
"url": "git+https://github.com/VryptLab/github-commit-bot.git"
12+
},
13+
"keywords": [],
14+
"author": "Vryptt",
15+
"license": "ISC",
16+
"type": "module",
17+
"bugs": {
18+
"url": "https://github.com/VryptLab/github-commit-bot/issues"
19+
},
20+
"homepage": "https://github.com/VryptLab/github-commit-bot#readme",
21+
"dependencies": {
22+
"baileys": "^7.0.0-rc.5",
23+
"node-fetch": "^3.3.2"
24+
}
25+
}

utils.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export const formatBytes = (bytes) => {
2+
if (bytes === 0) return "0 B";
3+
const k = 1024;
4+
const sizes = ["B", "KB", "MB", "GB"];
5+
const i = Math.floor(Math.log(bytes) / Math.log(k));
6+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
7+
};
8+
9+
export const truncate = (str, max = 100) =>
10+
str.length > max ? `${str.substring(0, max)}...` : str;
11+
12+
export const getUptime = (startTime) => {
13+
const uptime = Date.now() - startTime;
14+
const hours = Math.floor(uptime / 3600000);
15+
const minutes = Math.floor((uptime % 3600000) / 60000);
16+
return `${hours}h ${minutes}m`;
17+
};

0 commit comments

Comments
 (0)