Skip to content

Commit 196aa24

Browse files
committed
Deploy Soplang Webhook to Vercel (serverless)
1 parent ebf3a08 commit 196aa24

File tree

6 files changed

+123
-0
lines changed

6 files changed

+123
-0
lines changed

api/webhook.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { handleWebhook } from '../controllers/webhookController';
2+
3+
export default async function (req, res) {
4+
if (req.method === 'POST') {
5+
await handleWebhook(req, res);
6+
} else {
7+
res.status(405).json({ message: 'Method Not Allowed' });
8+
}
9+
}

controllers/webhookController.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import crypto from 'crypto';
4+
import fetchRepoDetails from '../utils/fetchRepoDetails.js';
5+
import dotenv from 'dotenv';
6+
7+
dotenv.config();
8+
9+
const JSON_FILE = path.join(process.cwd(), 'data/community_projects.json');
10+
const LATEST_PROJECT_FILE = path.join(process.cwd(), 'data/latest_project.txt');
11+
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
12+
13+
// Ensure JSON file exists
14+
const ensureFileExists = () => {
15+
if (!fs.existsSync(JSON_FILE)) fs.writeFileSync(JSON_FILE, '[]');
16+
if (!fs.existsSync(LATEST_PROJECT_FILE)) fs.writeFileSync(LATEST_PROJECT_FILE, '');
17+
};
18+
19+
// Load existing projects
20+
const loadProjects = () => {
21+
ensureFileExists();
22+
return JSON.parse(fs.readFileSync(JSON_FILE, 'utf8'));
23+
};
24+
25+
// Save projects
26+
const saveProjects = (projects, latestProject) => {
27+
ensureFileExists();
28+
fs.writeFileSync(JSON_FILE, JSON.stringify(projects, null, 2));
29+
fs.writeFileSync(LATEST_PROJECT_FILE, latestProject);
30+
};
31+
32+
// Verify GitHub Webhook Signature
33+
const verifySignature = (req) => {
34+
const signature = req.headers['x-hub-signature-256'];
35+
if (!signature) return false;
36+
37+
const payload = JSON.stringify(req.body);
38+
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
39+
hmac.update(payload);
40+
const calculatedSignature = `sha256=${hmac.digest('hex')}`;
41+
42+
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(calculatedSignature));
43+
};
44+
45+
// Handle Webhook
46+
export const handleWebhook = async (req, res) => {
47+
if (!verifySignature(req)) {
48+
return res.status(401).json({ error: 'Invalid signature' });
49+
}
50+
51+
const event = req.headers['x-github-event'];
52+
const payload = req.body;
53+
54+
if (event === 'repository' || event === 'push') {
55+
const { name, owner, html_url, description, topics } = payload.repository;
56+
57+
if ((description && description.toLowerCase().includes('soplang')) ||
58+
(topics && topics.includes('soplang'))) {
59+
60+
let projects = loadProjects();
61+
if (!projects.some(p => p.url === html_url)) {
62+
const repoDetails = await fetchRepoDetails(owner.login, name);
63+
if (repoDetails) {
64+
projects.push(repoDetails);
65+
saveProjects(projects, name);
66+
}
67+
}
68+
}
69+
}
70+
71+
res.status(200).json({ message: 'Webhook received' });
72+
};

routes/webhook.js

Whitespace-only changes.

server.js

Whitespace-only changes.

utils/fetchRepoDetails.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const axios = require('axios');
2+
3+
const fetchRepoDetails = async (owner, name) => {
4+
try {
5+
const token = process.env.GITHUB_TOKEN;
6+
const headers = token ? { Authorization: `token ${token}` } : {};
7+
8+
const [repoData, contributors] = await Promise.all([
9+
axios.get(`https://api.github.com/repos/${owner}/${name}`, { headers }),
10+
axios.get(`https://api.github.com/repos/${owner}/${name}/contributors`, { headers })
11+
]);
12+
13+
return {
14+
name,
15+
owner,
16+
url: repoData.data.html_url,
17+
description: repoData.data.description || "No description",
18+
stars: repoData.data.stargazers_count,
19+
forks: repoData.data.forks_count,
20+
contributors: contributors.data.length,
21+
creator: repoData.data.owner.login,
22+
created_at: repoData.data.created_at,
23+
last_updated: repoData.data.updated_at,
24+
language: repoData.data.language,
25+
};
26+
} catch (error) {
27+
console.error(`❌ Failed to fetch details for ${name}: ${error.message}`);
28+
return null;
29+
}
30+
};
31+
32+
module.exports = fetchRepoDetails;

vercel.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"version": 2,
3+
"builds": [
4+
{ "src": "api/webhook.js", "use": "@vercel/node" }
5+
],
6+
"routes": [
7+
{ "src": "/webhook", "dest": "api/webhook.js" }
8+
]
9+
}
10+

0 commit comments

Comments
 (0)