Spaces:
Running
Running
| 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); | |
| }); |