//native
// Template for receiving Slack Events API callbacks on a Windmill HTTP route.
//
// Setup:
// 1. Save this script, then add an HTTP route trigger on it: method POST, request type
// Sync (Slack needs the challenge answered in the response), authentication
// "Signature" with the Slack preset and your app's Signing Secret
// (api.slack.com/apps → your app → Basic Information → App Credentials).
// Windmill then rejects any request Slack did not sign; this script never sees it.
// 2. In the Slack app, open Event Subscriptions, turn it on and paste the route URL as
// the Request URL. Slack sends a url_verification challenge, which main() answers.
// 3. Subscribe to bot events and add the scope each one needs, then reinstall the app:
// message.channels → channels:history (the bot must be in the channel)
// reaction_added → reactions:read
// app_mention → app_mentions:read
// member_joined_channel → channels:read
// Saving the events in the Slack UI adds missing scopes for you (it also adds
// groups:read and mpim:read for member_joined_channel). Or paste this into the app manifest (App Manifest → YAML), merged with what is there:
//
// oauth_config:
// scopes:
// bot:
// - app_mentions:read
// - channels:history
// - channels:read
// - reactions:read
// settings:
// event_subscriptions:
// request_url: https://<your-windmill>/api/r/<route_path>
// bot_events:
// - app_mention
// - member_joined_channel
// - message.channels
// - reaction_added
//
// Slack waits 3 seconds for a 2xx and retries up to 3 times otherwise. Keep the handlers
// below short, or hand long work to another job (wmill.runScriptByPathAsync) and return.
// This script is //native so it starts in milliseconds; that needs a native worker,
// which the default deployment includes.
// Runs before main() on trigger calls: keeps the body and the two retry headers.
export async function preprocessor(event: {
kind: string
body: { [key: string]: any }
headers: { [key: string]: string }
}) {
if (event.kind !== "http") {
throw new Error(`Expected an HTTP route trigger, got ${event.kind}`)
}
return {
body: event.body,
retry_num: event.headers["x-slack-retry-num"] ?? "",
retry_reason: event.headers["x-slack-retry-reason"] ?? "",
}
}
/**
* Slack Events API handler (HTTP route template)
* Answers Slack's url_verification challenge, then dispatches message, reaction_added, app_mention and member_joined_channel events, skipping bot echoes and timeout retries.
*/
export async function main(
body: { [key: string]: any },
retry_num: string | undefined,
retry_reason: string | undefined
) {
if (body.type === "url_verification") {
return { challenge: body.challenge }
}
// A timeout retry means the first delivery reached Windmill and its job is queued or
// running, so handling it again would duplicate the work. Other retry reasons
// (connection or HTTP errors) mean the first delivery was lost: let those through.
if (retry_num && retry_reason === "http_timeout") {
return { skipped: "retry", retry_num }
}
if (body.type !== "event_callback") {
return { skipped: body.type }
}
const event = body.event
// Messages posted through any app, this one included (even with a user token):
// without this a bot that replies triggers itself in a loop.
if (event.bot_id) {
return { skipped: "bot" }
}
switch (event.type) {
case "message":
// Edits, deletions and joins arrive as message events with a subtype.
if (event.subtype) {
return { skipped: `message.${event.subtype}` }
}
return {
type: "message",
channel: event.channel,
user: event.user,
text: event.text,
ts: event.ts,
thread_ts: event.thread_ts,
}
case "app_mention":
// With message.channels also subscribed, a mention arrives twice: once as
// app_mention and once as message. React to one of them only.
return {
type: "app_mention",
channel: event.channel,
user: event.user,
text: event.text,
ts: event.ts,
thread_ts: event.thread_ts,
}
case "reaction_added":
return {
type: "reaction_added",
reaction: event.reaction,
user: event.user,
channel: event.item?.channel,
ts: event.item?.ts,
}
case "member_joined_channel":
return {
type: "member_joined_channel",
channel: event.channel,
user: event.user,
inviter: event.inviter,
}
default:
return { skipped: event.type }
}
}
Submitted by hugo989 3 hours ago