const express = require('express'); const { WebSocketServer } = require('ws'); const { Octokit } = require('@octokit/rest'); const http = require('http'); const CONFIG = { GITHUB_TOKEN: process.env.GITHUB_TOKEN || 'ghp_your_github_token_here', REPOS_TO_WATCH: [ { owner: process.env.REPO_OWNER || 'herzonly', repo: process.env.REPO_NAME || 'DHX' }, ], POLL_INTERVAL: 30000, BUFFER_TIME: 5 * 60 * 1000, PORT: process.env.PORT || 7860 }; const octokit = new Octokit({ auth: CONFIG.GITHUB_TOKEN }); const app = express(); const server = http.createServer(app); const wss = new WebSocketServer({ server }); app.use((req, res, next) => { const allowedMethods = ['GET', 'OPTIONS']; if (!allowedMethods.includes(req.method)) { console.log(`[BLOCKED] ${req.method} request from ${req.ip} to ${req.path}`); return res.status(405).json({ error: 'Method Not Allowed', message: 'Only GET requests are allowed', allowed_methods: allowedMethods }); } next(); }); const changedFilesBuffer = []; const cleanupTimers = new Map(); const lastCommits = new Map(); const clients = new Set(); wss.on('connection', (ws) => { console.log('New WebSocket client connected'); clients.add(ws); ws.send(JSON.stringify({ type: 'initial', count: changedFilesBuffer.length, data: changedFilesBuffer })); ws.on('close', () => { console.log('Client disconnected'); clients.delete(ws); }); ws.on('error', (error) => { console.error('WebSocket error:', error); clients.delete(ws); }); }); function broadcast(data) { const message = JSON.stringify(data); clients.forEach((client) => { if (client.readyState === 1) { client.send(message); } }); } async function getFileContent(owner, repo, path, ref) { try { const { data } = await octokit.repos.getContent({ owner, repo, path, ref }); if (data.content) { return Buffer.from(data.content, 'base64').toString('utf-8'); } return null; } catch (error) { console.error(`Error getting file content for ${path}:`, error.message); return null; } } function addChangedFile(fileData) { const id = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const item = { id, ...fileData, created_at: new Date().toISOString() }; changedFilesBuffer.push(item); console.log(`[BUFFER] Added file: ${fileData.changed_file.name} (Total: ${changedFilesBuffer.length})`); const timer = setTimeout(() => { const index = changedFilesBuffer.findIndex(f => f.id === id); if (index !== -1) { const removed = changedFilesBuffer.splice(index, 1)[0]; console.log(`[CLEANUP] Removed: ${removed.changed_file.name} after 5 minutes (Remaining: ${changedFilesBuffer.length})`); broadcast({ type: 'cleanup', id: id, filename: removed.changed_file.name, remaining_count: changedFilesBuffer.length }); } cleanupTimers.delete(id); }, CONFIG.BUFFER_TIME); cleanupTimers.set(id, timer); return item; } async function checkRepoChanges(owner, repo) { try { const { data: commits } = await octokit.repos.listCommits({ owner, repo, per_page: 10 }); if (commits.length === 0) return; const repoKey = `${owner}/${repo}`; const lastCommitSha = lastCommits.get(repoKey); if (!lastCommitSha) { lastCommits.set(repoKey, commits[0].sha); console.log(`[INIT] Tracking ${repoKey} from commit: ${commits[0].sha.substring(0, 7)}`); return; } const newCommits = []; for (const commit of commits) { if (commit.sha === lastCommitSha) break; newCommits.push(commit); } if (newCommits.length === 0) return; console.log(`[DETECTED] ${newCommits.length} new commit(s) in ${repoKey}`); for (const commit of newCommits.reverse()) { try { const { data: commitDetail } = await octokit.repos.getCommit({ owner, repo, ref: commit.sha }); console.log(`[COMMIT] ${commit.sha.substring(0, 7)}: "${commit.commit.message}" - ${commitDetail.files.length} file(s)`); for (const file of commitDetail.files) { if (file.status === 'removed') { console.log(` [SKIP] ${file.filename} (removed)`); continue; } console.log(` [PROCESS] ${file.filename} (${file.status})`); const content = await getFileContent(owner, repo, file.filename, commit.sha); const fileChangeData = { repository: repoKey, commit: { sha: commit.sha, short_sha: commit.sha.substring(0, 7), message: commit.commit.message, author: commit.commit.author.name, email: commit.commit.author.email, date: commit.commit.author.date, url: commit.html_url }, changed_file: { name: file.filename, status: file.status, additions: file.additions, deletions: file.deletions, changes: file.changes, isinya: content || 'Unable to fetch content', patch: file.patch || null } }; const bufferedItem = addChangedFile(fileChangeData); broadcast({ type: 'new_change', data: bufferedItem, total_count: changedFilesBuffer.length }); } } catch (error) { console.error(`[ERROR] Processing commit ${commit.sha.substring(0, 7)}:`, error.message); } } lastCommits.set(repoKey, commits[0].sha); } catch (error) { console.error(`[ERROR] Checking repo ${owner}/${repo}:`, error.message); } } app.get('/', (req, res) => { res.json({ name: 'GitHub Repository Watcher', version: '1.0.0', endpoints: { data: '/data', health: '/health' }, websocket: 'Connect to same URL with ws:// or wss://' }); }); app.get('/data', (req, res) => { res.json({ success: true, count: changedFilesBuffer.length, data: changedFilesBuffer, timestamp: new Date().toISOString() }); }); app.get('/health', (req, res) => { res.json({ status: 'running', buffered_files: changedFilesBuffer.length, ws_clients: clients.size, watching_repos: CONFIG.REPOS_TO_WATCH.length, poll_interval_sec: CONFIG.POLL_INTERVAL / 1000 }); }); server.listen(CONFIG.PORT, () => { console.log(`\n${'='.repeat(60)}`); console.log(` GitHub Repository Watcher - Running`); console.log(`${'='.repeat(60)}`); console.log(` Server: http://localhost:${CONFIG.PORT}`); console.log(` WebSocket: ws://localhost:${CONFIG.PORT}`); console.log(` Data Endpoint: http://localhost:${CONFIG.PORT}/data`); console.log(`${'='.repeat(60)}`); console.log(` Watching ${CONFIG.REPOS_TO_WATCH.length} repo(s):`); CONFIG.REPOS_TO_WATCH.forEach(r => { console.log(` - ${r.owner}/${r.repo}`); }); console.log(` Poll Interval: ${CONFIG.POLL_INTERVAL / 1000}s`); console.log(` Buffer Time: ${CONFIG.BUFFER_TIME / 1000 / 60}min`); console.log(`${'='.repeat(60)}\n`); }); async function startWatching() { console.log('[START] Initializing repository watcher...\n'); for (const { owner, repo } of CONFIG.REPOS_TO_WATCH) { await checkRepoChanges(owner, repo); } setInterval(async () => { for (const { owner, repo } of CONFIG.REPOS_TO_WATCH) { await checkRepoChanges(owner, repo); } }, CONFIG.POLL_INTERVAL); } startWatching(); process.on('SIGINT', () => { console.log('\n[SHUTDOWN] Cleaning up...'); cleanupTimers.forEach(timer => clearTimeout(timer)); cleanupTimers.clear(); wss.close(); server.close(); console.log('[SHUTDOWN] Server stopped'); process.exit(0); });