This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: add basic slackbot

+341 -1
+16
bun.lock
··· 3 3 "workspaces": { 4 4 "": { 5 5 "name": "takes", 6 + "dependencies": { 7 + "bottleneck": "^2.19.5", 8 + "colors": "^1.4.0", 9 + "slack-edge": "^1.3.7", 10 + "yaml": "^2.7.1", 11 + }, 6 12 "devDependencies": { 7 13 "@types/bun": "latest", 8 14 }, ··· 17 23 "@types/node": ["@types/node@22.13.17", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-nAJuQXoyPj04uLgu+obZcSmsfOenUg6DxPKogeUy6yNCFwWaj5sBF8/G/pNo8EtBJjAfSVgfIlugR/BCOleO+g=="], 18 24 19 25 "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], 26 + 27 + "bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="], 20 28 21 29 "bun-types": ["bun-types@1.2.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="], 22 30 31 + "colors": ["colors@1.4.0", "", {}, "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="], 32 + 33 + "slack-edge": ["slack-edge@1.3.7", "", { "dependencies": { "slack-web-api-client": "^1.1.5" } }, "sha512-BI+V8WTlaMQmUkBmyJoJ8PDykf6GoJQiCeExkfJ1H6l8Za4Wuv0sM+oV4sOjLgS06+AvOKvya9FgBpcuAKGoAA=="], 34 + 35 + "slack-web-api-client": ["slack-web-api-client@1.1.5", "", {}, "sha512-YmGGg3uU7tgW8djO2yn+xXgnkq5M1XeWYGODuDCwMbtr6OOJ5ys08Ju68XzadCSZNFqDKKSs31VSZKWJqb4KhA=="], 36 + 23 37 "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], 24 38 25 39 "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], 40 + 41 + "yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], 26 42 } 27 43 }
+31
manifest.yaml
··· 1 + display_information: 2 + name: Smokey 3 + description: Only you can complete your takes 4 + background_color: "#617c68" 5 + features: 6 + app_home: 7 + home_tab_enabled: true 8 + messages_tab_enabled: false 9 + messages_tab_read_only_enabled: true 10 + bot_user: 11 + display_name: smokey 12 + always_online: false 13 + slash_commands: 14 + - command: /takes 15 + url: https://casual-renewing-reptile.ngrok-free.app/slack 16 + description: Start a takes session 17 + should_escape: true 18 + oauth_config: 19 + scopes: 20 + bot: 21 + - commands 22 + - users:read 23 + - chat:write.public 24 + - chat:write 25 + settings: 26 + interactivity: 27 + is_enabled: true 28 + request_url: https://casual-renewing-reptile.ngrok-free.app/slack 29 + org_deploy_enabled: false 30 + socket_mode_enabled: false 31 + token_rotation_enabled: false
+12
package.json
··· 1 1 { 2 2 "name": "takes", 3 + "description": "smokey says hi!", 4 + "version": "0.0.0", 3 5 "module": "src/index.ts", 4 6 "type": "module", 5 7 "private": true, 8 + "scripts": { 9 + "dev": "bun run --watch src/index.ts", 10 + "ngrok": "ngrok http 3000 --domain=casual-renewing-reptile.ngrok-free.app" 11 + }, 6 12 "devDependencies": { 7 13 "@types/bun": "latest" 8 14 }, 9 15 "peerDependencies": { 10 16 "typescript": "^5" 17 + }, 18 + "dependencies": { 19 + "bottleneck": "^2.19.5", 20 + "colors": "^1.4.0", 21 + "slack-edge": "^1.3.7", 22 + "yaml": "^2.7.1" 11 23 } 12 24 }
+9
src/features/example.ts
··· 1 + import { slackApp } from "../index"; 2 + 3 + const example = async () => { 4 + slackApp.action("example_action", async ({ context, payload }) => { 5 + console.log("Example Action", payload); 6 + }); 7 + }; 8 + 9 + export default example;
+1
src/features/index.ts
··· 1 + export { default as example } from "./example";
+82 -1
src/index.ts
··· 1 - console.log("Hello via Bun!"); 1 + import { SlackApp } from "slack-edge"; 2 + 3 + import * as features from "./features/index"; 4 + 5 + import { t, t_fetch } from "./libs/template"; 6 + import { blog } from "./libs/Logger"; 7 + import { version, name } from "../package.json"; 8 + const environment = process.env.NODE_ENV; 9 + 10 + // Check required environment variables 11 + const requiredVars = ["SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET"] as const; 12 + const missingVars = requiredVars.filter((varName) => !process.env[varName]); 13 + 14 + if (missingVars.length > 0) { 15 + throw new Error( 16 + `Missing required environment variables: ${missingVars.join(", ")}`, 17 + ); 18 + } 19 + 20 + console.log( 21 + `----------------------------------\n${name} Server\n----------------------------------\n`, 22 + ); 23 + console.log(`🏗️ Starting ${name}...`); 24 + console.log("📦 Loading Slack App..."); 25 + console.log("🔑 Loading environment variables..."); 26 + 27 + const slackApp = new SlackApp({ 28 + env: { 29 + SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN!, 30 + SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET!, 31 + SLACK_LOGGING_LEVEL: "INFO", 32 + }, 33 + startLazyListenerAfterAck: true, 34 + }); 35 + const slackClient = slackApp.client; 36 + 37 + console.log(`⚒️ Loading ${Object.entries(features).length} features...`); 38 + for (const [feature, handler] of Object.entries(features)) { 39 + console.log(`📦 ${feature} loaded`); 40 + if (typeof handler === "function") { 41 + handler(); 42 + } 43 + } 44 + 45 + export default { 46 + port: process.env.PORT || 3000, 47 + async fetch(request: Request) { 48 + const url = new URL(request.url); 49 + const path = url.pathname; 50 + 51 + switch (path) { 52 + case "/": 53 + return new Response(`Hello World from ${name}@${version}`); 54 + case "/health": 55 + return new Response("OK"); 56 + case "/slack": 57 + return slackApp.run(request); 58 + default: 59 + return new Response("404 Not Found", { status: 404 }); 60 + } 61 + }, 62 + }; 63 + 64 + console.log( 65 + `🚀 Server Started in ${ 66 + Bun.nanoseconds() / 1000000 67 + } milliseconds on version: ${version}!\n\n----------------------------------\n`, 68 + ); 69 + 70 + blog( 71 + t("app.startup", { 72 + environment, 73 + }), 74 + "start", 75 + { 76 + channel: process.env.SLACK_SPAM_CHANNEL || "", 77 + }, 78 + ); 79 + 80 + console.log("\n----------------------------------\n"); 81 + 82 + export { slackApp, slackClient, version, name, environment };
+99
src/libs/Logger.ts
··· 1 + import { slackClient } from "../index"; 2 + 3 + import Bottleneck from "bottleneck"; 4 + import Queue from "./queue"; 5 + 6 + import colors from "colors"; 7 + import type { 8 + ChatPostMessageRequest, 9 + ChatPostMessageResponse, 10 + } from "slack-edge"; 11 + 12 + // Create a rate limiter with Bottleneck 13 + const limiter = new Bottleneck({ 14 + minTime: 1000, // 1 second between each request 15 + }); 16 + 17 + const messageQueue = new Queue(); 18 + 19 + function sendMessage( 20 + message: ChatPostMessageRequest, 21 + ): Promise<ChatPostMessageResponse> { 22 + return limiter.schedule(() => slackClient.chat.postMessage(message)); 23 + } 24 + 25 + async function slog( 26 + logMessage: string, 27 + location?: { 28 + thread_ts?: string; 29 + channel: string; 30 + }, 31 + ): Promise<void> { 32 + const message: ChatPostMessageRequest = { 33 + channel: location?.channel || process.env.SLACK_LOG_CHANNEL || "", 34 + thread_ts: location?.thread_ts, 35 + text: logMessage.substring(0, 2500), 36 + blocks: [ 37 + { 38 + type: "section", 39 + text: { 40 + type: "mrkdwn", 41 + text: logMessage 42 + .split("\n") 43 + .map((a) => `> ${a}`) 44 + .join("\n"), 45 + }, 46 + }, 47 + { 48 + type: "context", 49 + elements: [ 50 + { 51 + type: "mrkdwn", 52 + text: `${new Date().toString()}`, 53 + }, 54 + ], 55 + }, 56 + ], 57 + }; 58 + 59 + messageQueue.enqueue(() => sendMessage(message)); 60 + } 61 + 62 + type LogType = "info" | "start" | "cron" | "error"; 63 + 64 + export async function clog(logMessage: string, type: LogType): Promise<void> { 65 + switch (type) { 66 + case "info": 67 + console.log(colors.blue(logMessage)); 68 + break; 69 + case "start": 70 + console.log(colors.green(logMessage)); 71 + break; 72 + case "cron": 73 + console.log(colors.magenta(`[CRON]: ${logMessage}`)); 74 + break; 75 + case "error": 76 + console.error( 77 + colors.red.bold( 78 + `Yo <@S0790GPRA48> deres an error \n\n [ERROR]: ${logMessage}`, 79 + ), 80 + ); 81 + break; 82 + default: 83 + console.log(logMessage); 84 + } 85 + } 86 + 87 + export async function blog( 88 + logMessage: string, 89 + type: LogType, 90 + location?: { 91 + thread_ts?: string; 92 + channel: string; 93 + }, 94 + ): Promise<void> { 95 + slog(logMessage, location); 96 + clog(logMessage, type); 97 + } 98 + 99 + export { clog as default, slog };
+23
src/libs/queue.ts
··· 1 + export default class Queue { 2 + private jobs: (() => void)[] = []; 3 + private isProcessing = false; 4 + 5 + enqueue(job: () => void) { 6 + this.jobs.push(job); 7 + if (!this.isProcessing) { 8 + this.processQueue(); 9 + } 10 + } 11 + 12 + private processQueue() { 13 + if (this.jobs.length > 0) { 14 + const job = this.jobs.shift(); 15 + if (job) { 16 + this.isProcessing = true; 17 + job(); 18 + this.isProcessing = false; 19 + this.processQueue(); 20 + } 21 + } 22 + } 23 + }
+56
src/libs/template.ts
··· 1 + import { parse } from "yaml"; 2 + 3 + type template = "app.startup"; 4 + 5 + interface data { 6 + environment?: string; 7 + } 8 + 9 + const file = await Bun.file("src/libs/templates.yaml").text(); 10 + const templatesRaw = parse(file); 11 + 12 + function flatten(obj: Record<string, unknown>, prefix = "") { 13 + let result: Record<string, unknown> = {}; 14 + 15 + for (const key in obj) { 16 + if (typeof obj[key] === "object" && Array.isArray(obj[key]) === false) { 17 + result = { 18 + ...result, 19 + ...flatten( 20 + obj[key] as Record<string, unknown>, 21 + `${prefix}${key}.`, 22 + ), 23 + }; 24 + } else { 25 + result[`${prefix}${key}`] = obj[key]; 26 + } 27 + } 28 + 29 + return result; 30 + } 31 + 32 + const templates = flatten(templatesRaw); 33 + 34 + export function t(template: template, data: data) { 35 + return t_format(t_fetch(template), data); 36 + } 37 + 38 + export function t_fetch(template: template) { 39 + return Array.isArray(templates[template]) 40 + ? (randomChoice(templates[template]) as string) 41 + : (templates[template] as string); 42 + } 43 + 44 + export function t_format(template: string, data: data) { 45 + return template.replace( 46 + /\${(.*?)}/g, 47 + (_, key) => data[key as keyof data] ?? "", 48 + ); 49 + } 50 + 51 + export function randomChoice<T>(arr: T[]): T { 52 + if (arr.length === 0) { 53 + throw new Error("Cannot get random choice from empty array"); 54 + } 55 + return arr[Math.floor(Math.random() * arr.length)]!; 56 + }
+12
src/libs/templates.yaml
··· 1 + app: 2 + startup: 3 + - "Remember friends, only YOU can prevent server outages! :bear: The environment *${environment}* is safe and secure! :evergreen_tree:" 4 + - "Howdy campers! Your friendly forest guardian here, keeping watch over environment *${environment}*! :camping:" 5 + - "Time to pitch our tents in *${environment}*! All systems operational! :tent:" 6 + - "Trail status for *${environment}*: Clear skies ahead! :sunny:" 7 + - "Forest ranger checking in! *${environment}* is looking mighty fine today! :mountain:" 8 + - "The campfire is lit and *${environment}* is warming up nicely! :fire:" 9 + - "Happy trails! Your *${environment}* environment is ready for adventure! :hiking_boot:" 10 + - "Welcome to *${environment}* National Park! All systems are go! :national_park:" 11 + - "Ranger station report: *${environment}* is operating at peak performance! :mountain_snow:" 12 + - "Good morning from the wilderness of *${environment}*! Everything's running smoothly! :sunrise_over_mountains:"