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