Webhooks using `Serverless.com` Lift plugin
serverless.yml
service: github-webhook-service
provider:
name: aws
runtime: nodejs20.x
region: us-east-1
environment:
GITHUB_TOKEN: ${env:GITHUB_TOKEN}
plugins:
- serverless-lift
constructs:
webhook:
type: webhook
path: /webhook
method: POST
eventType: $request.body.type
insecure: true
functions:
handleWebhook:
handler: handler.handleWebhook
events:
- eventBridge:
eventBus: ${construct:webhook.busName}
pattern:
source:
- webhook
detail-type:
- new_comment
package:
patterns:
- '!node_modules/aws-sdk/**'
- '!node_modules/@aws-sdk/**'
handler.mjs
import { Octokit } from "@octokit/rest"; import { Base64 } from "js-base64"; /** * Converts a string to a URL-friendly slug. * @param {string} text - The text to be slugified. * @return {string} The slugified text. */ const slugify = (text) => { return text .toString() .toLowerCase() .trim() .replace(/\s+/g, '-') // Replace spaces with - .replace(/[^\w\-]+/g, '') // Remove all non-word chars .replace(/\-\-+/g, '-') // Replace multiple - with single - .replace(/^-+/, '') // Trim - from start of text .replace(/-+$/, ''); // Trim - from end of text }; /** * Formats a date string to YYYY-MM-DD. * @param {string} date - The date string to format. * @return {string} The formatted date string. */ const formatDate = (date) => { const d = new Date(date); if (isNaN(d.getTime())) { throw new Error('Invalid date provided'); } return d.toISOString().split('T')[0]; }; /** * Creates the content for a blog post. * @param {Object} data - The data for the blog post. * @param {string} data.by_nickname - The author's nickname. * @param {string} data.by_email - The author's email. * @param {string} data.content - The content of the post. * @param {string} data.time - The timestamp of the post. * @return {string} The formatted blog post content. */ const createPostContent = (data) => { const { by_nickname, by_email, content, time } = data; const slugName = slugify(by_nickname); const date = formatDate(time); return `--- tags: - ${by_email} info: aberto. date: ${date} type: post layout: post published: true slug: ${slugName} title: '${by_nickname}' --- ${content}`; }; /** * Handles the webhook event for creating a new blog post. * @param {Object} event - The webhook event object. * @return {Object} The response object. */ export const handleWebhook = async (event) => { try { if (!event || !event.detail || !event.detail.data) { throw new Error('Invalid event structure'); } const { by_nickname, by_email, content } = event.detail.data; const time = event.time || new Date().toISOString(); if (!by_email || !by_nickname || !content) { throw new Error('Missing required fields: email, nickname, or content'); } const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); if (!process.env.GITHUB_TOKEN) { throw new Error('GitHub token is not set'); } const owner = "${REPO_OWNER}"; const repo = "${REPO_NAME}"; const date = formatDate(time); const slugName = slugify(by_nickname); const path = `_posts/${date}-${slugName}.md`; const message = `New post by ${by_nickname}`; const postContent = createPostContent({ by_nickname, by_email, content, time }); const contentEncoded = Base64.encode(postContent); await octokit.repos.createOrUpdateFileContents({ owner, repo, path, message, content: contentEncoded, }); return { statusCode: 200, body: JSON.stringify({ message: "File created/updated successfully", path }), }; } catch (error) { console.error('Error in handleWebhook:', error); return { statusCode: error.status || 500, body: JSON.stringify({ message: error.message || "An unexpected error occurred" }), }; } };