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