Skip to content

Commit 14ee0d7

Browse files
committed
feat: add /cowsay command
1 parent 405ea59 commit 14ee0d7

File tree

4 files changed

+102
-1
lines changed

4 files changed

+102
-1
lines changed

bun.lockb

13.1 KB
Binary file not shown.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
},
1010
"dependencies": {
1111
"@aws-sdk/client-s3": "^3.717.0",
12+
"@barudakrosul/textwrap": "^0.0.4",
1213
"@octokit/rest": "^21.0.2",
1314
"@t3-oss/env-core": "^0.11.1",
1415
"@types/sharp": "^0.32.0",
1516
"bufferutil": "^4.0.8",
17+
"cowsay": "^1.6.0",
1618
"dayjs": "^1.11.13",
1719
"discord.js": "^14.16.3",
1820
"fastest-levenshtein": "^1.0.16",

src/commands/cowsay.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {
2+
type ChatInputCommandInteraction,
3+
SlashCommandBuilder,
4+
} from "discord.js";
5+
import cowsay from "cowsay";
6+
import textwrap from "@barudakrosul/textwrap";
7+
8+
const DEFAULT_WRAP_WIDTH = 40;
9+
10+
export const data = new SlashCommandBuilder()
11+
.setName("cowsay")
12+
.setDescription("Make a cow say it")
13+
.addStringOption((option) =>
14+
option
15+
.setName("text")
16+
.setDescription("The text to say")
17+
.setRequired(true),
18+
)
19+
.addStringOption((option) =>
20+
option
21+
.setName("cow")
22+
.setDescription("The cow design to use")
23+
.setRequired(false),
24+
)
25+
.addIntegerOption((option) =>
26+
option
27+
.setName("wrap_width")
28+
.setDescription(
29+
"Width at which to wrap the input text (0 to disable)",
30+
)
31+
.setRequired(false),
32+
);
33+
34+
export async function command(interaction: ChatInputCommandInteraction) {
35+
const { options } = interaction;
36+
37+
const text = options.getString("text", true);
38+
const cow = options.getString("cow");
39+
const wrapWidth = options.getInteger("wrap_width") ?? DEFAULT_WRAP_WIDTH;
40+
41+
const isChannel = !!interaction.channel;
42+
43+
if (!isChannel) {
44+
await interaction.reply({
45+
content: "This command can only be used in a channel",
46+
ephemeral: true,
47+
});
48+
return;
49+
}
50+
51+
if (text === "!list") {
52+
for (const chunk of await getCowListResponseContent()) {
53+
await interaction.reply({
54+
content: chunk,
55+
ephemeral: true,
56+
});
57+
}
58+
} else {
59+
await interaction.reply({
60+
content: getCowsayResponseContent(text, cow, wrapWidth),
61+
});
62+
}
63+
}
64+
65+
function getCowsayResponseContent(
66+
text: string,
67+
cow: string | null,
68+
wrapWidth: number,
69+
): string {
70+
const wrappedText =
71+
wrapWidth > 0 ? textwrap.wrap(text, wrapWidth).join("\n") : text;
72+
const cowsaid = cowsay.say({
73+
text: wrappedText,
74+
f: cow ?? undefined,
75+
});
76+
return "```\n" + cowsaid + "\n```";
77+
}
78+
79+
async function getCowListResponseContent(): Promise<string[]> {
80+
try {
81+
const filenames = await cowsay.list(() => {});
82+
const cows = filenames.map((name) => name.replace(/\.cow$/, ""));
83+
const chunks = ["```\n"];
84+
for (const cow of cows) {
85+
if (chunks[chunks.length - 1].length + cow.length + 1 + 3 > 2000) {
86+
chunks[chunks.length - 1] += "```";
87+
chunks.push("```\n");
88+
}
89+
chunks[chunks.length - 1] += cow + "\n";
90+
}
91+
chunks[chunks.length - 1] += "```";
92+
return chunks;
93+
} catch (e) {
94+
const msg = (e as Error).message;
95+
console.error(`Error listing cows: ${msg}`);
96+
return ["Error listing cows"];
97+
}
98+
}

src/commands/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import type {
44
} from "discord.js";
55

66
import * as summarize from "./summarize";
7+
import * as cowsay from "./cowsay";
78

89
type Command = {
910
data: SlashCommandOptionsOnlyBuilder;
1011
command: (interaction: ChatInputCommandInteraction) => Promise<void>;
1112
};
1213

13-
export const commands: Command[] = [summarize];
14+
export const commands: Command[] = [summarize, cowsay];

0 commit comments

Comments
 (0)