Skip to content

Instantly share code, notes, and snippets.

@jmschrack
Last active July 11, 2024 04:59
Show Gist options
  • Select an option

  • Save jmschrack/013d949e779b83000652510c98eac2b5 to your computer and use it in GitHub Desktop.

Select an option

Save jmschrack/013d949e779b83000652510c98eac2b5 to your computer and use it in GitHub Desktop.
SlashCreate setup for custom AWS Lambda Runtime

Setup

Cloudformation Template

The key difference here is the Lambda Runtime, and the HttpApi. Everything else can be modified to suit your needs.

Project setup

Drop the Makefile, bootstrap, and AWSRuntimeServer.js files into the src/handler folder. These must be in the folder specified by the CodeUri parameter of the Lambda Function

Download and extract the Bun runtime into the src/handler folder as well. https://github.com/oven-sh/bun/releases/latest/download/bun-linux-x64.zip

Usage with SlashCreate

Index.js is provided as an example. Everything should work as normal, but keep in mind that with Lambdas you probably need to defer your command responses more often than not.

Deployment

  • sam build
  • sam deploy --guided
const { Server } = require('slash-create');
class AWSRuntimeServer extends Server {
/**
* @param opts The server options
*/
constructor(opts) {
super(opts);
}
/**
*
* @param {string} path This is not used in AWS Lambda
* @param {ServerRequestHandler} handler The callback that incoming events are passed to
*/
createEndpoint(path, handler) {
this.handler = handler;
}
/** @private */
async listen(port = 8030, host = 'localhost') {
while (true) {
try {
//const res = await axios.get(`http://${process.env.AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next`, { timeout: 30000 })
const res= (await fetch(`http://${process.env.AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next`));
const invocationID=res.headers.get('lambda-runtime-aws-request-id');
const data = await res.json();
console.log("Received message", data.body);
const treq = {
headers: data.headers,
body: JSON.parse(data.body),
rawBody:data.body,
request: {},
response: {},
}
await this.handler(treq, lambdaResponder(invocationID));
} catch (e) {
console.error("Error in listen", e);
}
}
}
}
/**
* Creates a callback function that sends a response to the AWS Lambda Runtime API
* @param {string} invocationID
* @returns
*/
function lambdaResponder(invocationID) {
return async (response) => {
let d;
if (response.status && response.status !== 200) {
response.body = JSON.stringify(response.body);
response.statusCode = response.status ?? 200;
d=JSON.stringify(response);
}else{
d=JSON.stringify(response.body);
}
console.log(invocationID + " ::Sending response", d);
await fetch(`http://${process.env.AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/${invocationID}/response`, { method: "POST", body: d })
}
}
module.exports = AWSRuntimeServer;
/**
* @callback ServerRequestHandler
* @param {TransformedRequest} req The incoming request
* @param {ResponseFunction} res The callback to send the response back to the initial request
* @returns {Promise<void>}
* @callback ResponseFunction
* @param {Response} response
* @returns {Promise<void>}
* @typedef {Object} Response
* @property {number} [status]
* @property {Object.<string, string | string[] | undefined>} [headers]
* @property {any} [body]
* @typedef {Object} TransformedRequest
* @property {Object.<string, string | string[] | undefined>} headers
* @property {any} body
* @property {*} request
* @property {*} response
* @property {string} [rawBody]
*/
#!/bin/sh
set -euo pipefail
exec ./bun index.js
const { SlashCreator } = require('slash-create');
const AWSRuntimeServer = require('./AWSRuntimeServer')
const COMMANDS_DIR = '/opt/nodejs/commands';
const creator = new SlashCreator({
applicationID: process.env.DISCORD_APP_ID,
publicKey: process.env.DISCORD_PUBLIC_KEY,
token: process.env.DISCORD_BOT_TOKEN,
});
creator.on('debug', console.log);
creator.on('warn', console.log);
creator.on('error', console.log);
creator.on('rawREST', (request) => {
console.log("Request:", JSON.stringify(request.body));
});
async function main() {
await (creator.withServer(new AWSRuntimeServer())).registerCommandsIn(COMMANDS_DIR);
await creator.startServer();
}
main();
build-DiscordHandlerFunction:
cp -r bootstrap index.js AWSRuntimeServer.js bun node_modules/ package.json $(ARTIFACTS_DIR)
run-DiscordHandlerFunction:
exec $(ARTIFACTS_DIR)/bun index.js
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
serverless-discord-bot
A Serverless Discord Bot template built with Discord Slash Commands and AWS SAM.
Globals:
Function:
Timeout: 30
Resources:
CommandsLayer:
Type: AWS::Serverless::LayerVersion
Properties:
ContentUri: ./src/commands_layer
#CreateCommandsFunction:
# Type: AWS::Serverless::Function
# Properties:
# CodeUri: ./src/create_commands
# Description: "Function to create Discord Slash Commands"
# Handler: index.lambdaHandler
# Environment:
# Variables:
# DISCORD_APP_ID: '{{resolve:secretsmanager:/dev/serverless_discord_bot/discord:SecretString:app_id}}'
# DISCORD_PUBLIC_KEY: '{{resolve:secretsmanager:/dev/serverless_discord_bot/discord:SecretString:public_key}}'
# DISCORD_BOT_TOKEN: '{{resolve:secretsmanager:/dev/serverless_discord_bot/discord:SecretString:bot_token}}'
# Layers:
# - !Ref CommandsLayer
# CreateCommandsInvoker:
# Type: Custom::CreateCommandsInvoker
# Properties:
# ServiceToken: !GetAtt CreateCommandsFunction.Arn
# Passing the CommandsLayer ARN will cause a custom resource update every time the commands are updated.
# (note that the ARN of a LayerVersion Resource ends with an incrementing layer version number)
# CommandsLayerVersion: !Ref CommandsLayer
DiscordInteractionApi:
Type: AWS::Serverless::HttpApi
DiscordHandlerFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/handler
Description: "Serverless Function to handle incoming Discord requests"
Handler: run.handler
Role: !GetAtt LambdaExecutionRole.Arn
Runtime: provided.al2023
Architectures:
- x86_64
Events:
HttpEvent:
Type: HttpApi
Properties:
ApiId: !Ref DiscordInteractionApi
Environment:
Variables:
DISCORD_APP_ID: '{{resolve:secretsmanager:/dev/serverless_discord_bot/discord:SecretString:app_id}}'
DISCORD_PUBLIC_KEY: '{{resolve:secretsmanager:/dev/serverless_discord_bot/discord:SecretString:public_key}}'
DISCORD_BOT_TOKEN: '{{resolve:secretsmanager:/dev/serverless_discord_bot/discord:SecretString:bot_token}}'
Layers:
- !Ref CommandsLayer
LambdaExecutionRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Principal:
Service: lambda.amazonaws.com
Action: 'sts:AssumeRole'
Policies:
- PolicyName: LambdaDynamoDBPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Action:
- "logs:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Resource: '*'
Outputs:
DiscordInteractionApi:
Description: "Interactions endpoint URL"
Value: !Sub "https://${DiscordInteractionApi}.execute-api.${AWS::Region}.amazonaws.com/"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment