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" }),
    };
  }
};