init
This commit is contained in:
commit
1fac451ada
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.vercel
|
69
api/formatters.js
Normal file
69
api/formatters.js
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
/**
|
||||||
|
* Format commit message with emoji based on content
|
||||||
|
* @param {string} message - Commit message
|
||||||
|
* @param {Object} stats - Commit stats
|
||||||
|
* @returns {string} - Emoji for the commit
|
||||||
|
*/
|
||||||
|
const getCommitEmoji = (message, stats) => {
|
||||||
|
const msg = message.toLowerCase()
|
||||||
|
if (msg.includes('fix')) return '🔧'
|
||||||
|
if (msg.includes('feat')) return '✨'
|
||||||
|
if (msg.includes('break')) return '💥'
|
||||||
|
if (msg.includes('docs')) return '📚'
|
||||||
|
if (msg.includes('test')) return '🧪'
|
||||||
|
if (msg.includes('refactor')) return '♻️'
|
||||||
|
if (stats.additions > 100 || stats.deletions > 100) return '🔨'
|
||||||
|
return '📝'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format commit stats
|
||||||
|
* @param {Object} stats - Commit stats object
|
||||||
|
* @returns {string} - Formatted stats string
|
||||||
|
*/
|
||||||
|
const formatStats = (stats = { additions: 0, deletions: 0 }) =>
|
||||||
|
`+${stats.additions}/-${stats.deletions}`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format commit message for Telegram
|
||||||
|
* @param {Object} commit - Commit object
|
||||||
|
* @param {string} repoUrl - Repository URL
|
||||||
|
* @returns {string} - Formatted commit message
|
||||||
|
*/
|
||||||
|
const formatCommit = (commit, repoUrl) => {
|
||||||
|
const commitUrl = `${repoUrl}/commit/${commit.id}`
|
||||||
|
const emoji = getCommitEmoji(commit.message, commit.stats || {})
|
||||||
|
const stats = formatStats(commit.stats)
|
||||||
|
return `${emoji} [${commit.id.substring(0, 7)}](${commitUrl}): ${commit.message} \`${stats}\``
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format webhook message for Telegram
|
||||||
|
* @param {Object} data - Webhook payload
|
||||||
|
* @param {Array} commits - Filtered commits
|
||||||
|
* @returns {string} - Formatted message
|
||||||
|
*/
|
||||||
|
const formatMessage = (data, commits) => {
|
||||||
|
const repoUrl = data.repository.html_url || data.repository.url
|
||||||
|
const repoName = data.repository.full_name
|
||||||
|
const branch = data.ref.split('/').pop()
|
||||||
|
const branchUrl = `${repoUrl}/tree/${branch}`
|
||||||
|
|
||||||
|
const totalStats = commits.reduce((acc, commit) => ({
|
||||||
|
additions: acc.additions + (commit.stats?.additions || 0),
|
||||||
|
deletions: acc.deletions + (commit.stats?.deletions || 0)
|
||||||
|
}), { additions: 0, deletions: 0 })
|
||||||
|
|
||||||
|
return [
|
||||||
|
`🔄 [${repoName}](${repoUrl}):[${branch}](${branchUrl}) ${commits.length} new commit${commits.length === 1 ? '' : 's'}`,
|
||||||
|
commits.length > 1 ? `📊 Changes: \`${formatStats(totalStats)}\`` : '',
|
||||||
|
commits.map(commit => formatCommit(commit, repoUrl)).join('\n')
|
||||||
|
].filter(Boolean).join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
formatMessage,
|
||||||
|
formatCommit,
|
||||||
|
formatStats,
|
||||||
|
getCommitEmoji
|
||||||
|
}
|
47
api/gitea.js
Normal file
47
api/gitea.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
const { formatMessage } = require('./formatters')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Gitea webhook
|
||||||
|
* @param {Object} payload - Gitea webhook payload
|
||||||
|
* @returns {Object} - Normalized webhook data
|
||||||
|
*/
|
||||||
|
const normalizeGiteaPayload = (payload) => ({
|
||||||
|
repository: {
|
||||||
|
full_name: payload.repository.full_name,
|
||||||
|
html_url: payload.repository.html_url || payload.repository.url
|
||||||
|
},
|
||||||
|
ref: payload.ref,
|
||||||
|
commits: payload.commits.map(commit => ({
|
||||||
|
id: commit.id,
|
||||||
|
message: commit.message,
|
||||||
|
stats: {
|
||||||
|
additions: commit.added?.length || 0,
|
||||||
|
deletions: commit.removed?.length || 0
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Gitea webhook
|
||||||
|
* @param {Object} payload - Webhook payload
|
||||||
|
* @param {Set} reportedCommits - Set of reported commit hashes
|
||||||
|
* @param {Map} commitTimestamps - Map of commit timestamps
|
||||||
|
* @returns {Object} - Formatted message or null
|
||||||
|
*/
|
||||||
|
const handleGitea = (payload, reportedCommits, commitTimestamps) => {
|
||||||
|
const data = normalizeGiteaPayload(payload)
|
||||||
|
|
||||||
|
// Filter new commits
|
||||||
|
const newCommits = data.commits.filter(commit => {
|
||||||
|
if (reportedCommits.has(commit.id)) return false
|
||||||
|
reportedCommits.add(commit.id)
|
||||||
|
commitTimestamps.set(commit.id, Date.now())
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (newCommits.length === 0) return null
|
||||||
|
|
||||||
|
return formatMessage(data, newCommits)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { handleGitea }
|
47
api/github.js
Normal file
47
api/github.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
const { formatMessage } = require('./formatters')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize GitHub webhook payload to common format
|
||||||
|
* @param {Object} payload - GitHub webhook payload
|
||||||
|
* @returns {Object} - Normalized webhook data
|
||||||
|
*/
|
||||||
|
const normalizeGithubPayload = (payload) => ({
|
||||||
|
repository: {
|
||||||
|
full_name: payload.repository.full_name,
|
||||||
|
html_url: payload.repository.html_url
|
||||||
|
},
|
||||||
|
ref: payload.ref,
|
||||||
|
commits: payload.commits.map(commit => ({
|
||||||
|
id: commit.id,
|
||||||
|
message: commit.message,
|
||||||
|
stats: {
|
||||||
|
additions: commit.stats?.additions || 0,
|
||||||
|
deletions: commit.stats?.deletions || 0
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle GitHub webhook
|
||||||
|
* @param {Object} payload - Webhook payload
|
||||||
|
* @param {Set} reportedCommits - Set of reported commit hashes
|
||||||
|
* @param {Map} commitTimestamps - Map of commit timestamps
|
||||||
|
* @returns {string|null} - Formatted message or null if no new commits
|
||||||
|
*/
|
||||||
|
const handleGithub = (payload, reportedCommits, commitTimestamps) => {
|
||||||
|
const data = normalizeGithubPayload(payload)
|
||||||
|
|
||||||
|
// Filter new commits
|
||||||
|
const newCommits = data.commits.filter(commit => {
|
||||||
|
if (reportedCommits.has(commit.id)) return false
|
||||||
|
reportedCommits.add(commit.id)
|
||||||
|
commitTimestamps.set(commit.id, Date.now())
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (newCommits.length === 0) return null
|
||||||
|
|
||||||
|
return formatMessage(data, newCommits)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { handleGithub }
|
11
api/index.js
Normal file
11
api/index.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
const { webhook, cleanup } = require('./webhook');
|
||||||
|
|
||||||
|
module.exports = async (req, res) => {
|
||||||
|
// Check if it's a cron cleanup request
|
||||||
|
if (req.headers['x-vercel-cron']) {
|
||||||
|
return cleanup(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise handle as webhook
|
||||||
|
return webhook(req, res);
|
||||||
|
};
|
77
api/webhook.js
Normal file
77
api/webhook.js
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
const { formatMessage } = require('./formatters')
|
||||||
|
const { handleGitea } = require('./gitea')
|
||||||
|
const { handleGithub } = require('./github')
|
||||||
|
// Store for already reported commit hashes (in-memory, resets on restart)
|
||||||
|
const reportedCommits = new Set();
|
||||||
|
const commitTimestamps = new Map();
|
||||||
|
|
||||||
|
// TTL constants (used in cleanup)
|
||||||
|
const COMMIT_TTL = 30 * 24 * 60 * 60 * 1000; // 30 days
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup old commits (called by Vercel Cron)
|
||||||
|
* @param {Object} req - Express request object
|
||||||
|
* @param {Object} res - Express response object
|
||||||
|
*/
|
||||||
|
const cleanup = (req, res) => {
|
||||||
|
const now = Date.now();
|
||||||
|
let cleaned = 0;
|
||||||
|
|
||||||
|
for (const [hash, timestamp] of commitTimestamps.entries()) {
|
||||||
|
if (now - timestamp > COMMIT_TTL) {
|
||||||
|
commitTimestamps.delete(hash);
|
||||||
|
reportedCommits.delete(hash);
|
||||||
|
cleaned++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({ cleaned });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Webhook handler that supports both JSON and x-www-form-urlencoded formats
|
||||||
|
* @param {Object} req - Express request object
|
||||||
|
* @param {Object} res - Express response object
|
||||||
|
*/
|
||||||
|
const webhook = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const payload = typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
||||||
|
|
||||||
|
if (!payload || !payload.repository) {
|
||||||
|
return res.status(400).send('Invalid webhook payload')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine webhook type and handle accordingly
|
||||||
|
const message = payload.repository.owner?.username // Gitea specific field
|
||||||
|
? handleGitea(payload, reportedCommits, commitTimestamps)
|
||||||
|
: handleGithub(payload, reportedCommits, commitTimestamps)
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return res.status(200).send('No new commits to report')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send to Telegram
|
||||||
|
const telegramUrl = `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`
|
||||||
|
await fetch(telegramUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
chat_id: `-100${process.env.TELEGRAM_CHAT_ID}`,
|
||||||
|
text: message,
|
||||||
|
parse_mode: 'Markdown',
|
||||||
|
disable_web_page_preview: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).send('ok')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing webhook:', error)
|
||||||
|
res.status(500).send(`Error: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export both handlers
|
||||||
|
module.exports = {
|
||||||
|
webhook,
|
||||||
|
cleanup
|
||||||
|
};
|
15
package.json
Normal file
15
package.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "gh2tg",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"vercel": "^41.1.0"
|
||||||
|
}
|
||||||
|
}
|
12
vercel.json
Normal file
12
vercel.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"crons": [{
|
||||||
|
"path": "/api",
|
||||||
|
"schedule": "0 0 * * *"
|
||||||
|
}],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"src": "/api/webhook",
|
||||||
|
"dest": "/api/index.js"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user