Sign In

slack-selfbot

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

slack-selfbot - npm Package Compare versions

Comparing version
0.1.0
to
0.1.1
.prettierignore

Sorry, the diff of this file is not supported yet

+3
{
"tabWidth": 4
}
+216
-70
#!/usr/bin/env bun
import { SlackClient } from 'slack-undoc-client';
import { SlackClient } from "slack-undoc-client";
const SLACK_COOKIE = process.env.SLACK_COOKIE;
const SLACK_XOXP = process.env.SLACK_XOXP;
const SLACK_WORKSPACE = process.env.SLACK_WORKSPACE || "hackclub";
if (!SLACK_COOKIE) {
throw new Error('SLACK_COOKIE environment variable is not set.');
throw new Error("SLACK_COOKIE environment variable is not set.");
} else if (!SLACK_XOXP) {
throw new Error('SLACK_XOXP environment variable is not set.');
throw new Error("SLACK_XOXP environment variable is not set.");
}

@@ -16,11 +17,13 @@

cookie: SLACK_COOKIE,
workspace: 'hackclub',
})
workspace: SLACK_WORKSPACE,
});
const userInfo = await client.authTest();
const userChannelId: string = (await client.callUnknown("conversations.open", {
users: userInfo.user_id,
}) as any).channel.id;
const selfUserInfo = await client.authTest();
const userChannelId: string = (
(await client.callUnknown("conversations.open", {
users: selfUserInfo.user_id,
})) as any
).channel.id;
console.log(`Logged in as ${userInfo.user} (${userInfo.user_id})`);
console.log(`Logged in as ${selfUserInfo.user} (${selfUserInfo.user_id})`);

@@ -32,8 +35,14 @@ let websocketUrl: string = `wss://wss-primary.slack.com/?token=${client.token}&sync_desync=1&slack_client=desktop&start_args=?agent=client&org_wide_aware=true&eac_cache_ts=true&cache_ts=0&name_tagging=true&only_self_subteams=true&connect_only=true&ms_latest=true&no_query_on_subscribe=1&flannel=3&lazy_channels=1&gateway_server=T09V59WQY1E-1&enterprise_id=E09V59WQY1E&batch_presence_aware=1`;

function chatPostEphemeral(channel: string, text: string, thread_ts?: string) {
async function chatPostEphemeral(
channel: string,
text: string,
thread_ts?: string,
blocks?: any[],
user?: string,
) {
fetch("https://slack.com/api/chat.postEphemeral", {
method: "POST",
headers: {
"Authorization": `Bearer ${SLACK_XOXP}`,
"Content-Type": "application/json",
Authorization: `Bearer ${SLACK_XOXP}`,
"Content-Type": "application/json;charset=utf-8",
},

@@ -44,13 +53,51 @@ body: JSON.stringify({

thread_ts,
user: userInfo.user_id,
blocks,
user: user ?? selfUserInfo.user_id,
}),
});
})
.then((res) => res.json())
.then((data) => {
if (!(data as any).ok) {
console.error("Error posting ephemeral message:", data);
}
})
.catch((err) => {
console.error("Error posting ephemeral message:", err);
});
}
function getChannelName(channelId: string): Promise<string> {
type UserInfo = {
type: "user";
id: string;
userId: string;
displayName: string;
pronouns: string;
imageUrl: string;
};
async function getUserInfo(userId: string): Promise<UserInfo> {
return fetch(`https://cachet.dunkirk.sh/users/${userId}`)
.then((res) => res.json())
.then((data) => data as UserInfo);
}
async function getChannelName(channelId: string): Promise<string> {
return fetch(`https://flaron.halceon.dev/cid/${channelId}`)
.then(res => res.json())
.then(data => (data as any).name ?? "unknown :(");
.then((res) => res.json())
.then((data) => (data as any).name ?? "unknown :(");
}
async function getChannelNames(channelIds: string[]): Promise<string[]> {
const dedupedIds = [...new Set(channelIds)];
const namesById = new Map(
await Promise.all(
dedupedIds.map(
async (id) => [id, await getChannelName(id)] as const,
),
),
);
return channelIds.map((id) => namesById.get(id) ?? "unknown :(");
}
type MessageMetadata = {

@@ -63,3 +110,3 @@ text: string;

thread_ts?: string;
}
};

@@ -71,3 +118,3 @@ type ShallowMessageMetadata = {

thread_ts?: string;
}
};

@@ -78,3 +125,3 @@ type Command = {

handler: (msg: MessageMetadata, args: string[]) => Promise<void>;
}
};

@@ -86,3 +133,3 @@ type ReactionTrigger = {

any?: (msg: ShallowMessageMetadata, reaction: string) => Promise<void>;
}
};

@@ -92,3 +139,5 @@ function formatHelpText() {

.map(([name, command]) => {
const usage = command.args ? `*${name}* ${command.args}` : `*${name}*`;
const usage = command.args
? `*${name}* ${command.args}`
: `*${name}*`;
return `- ${usage}: ${command.description}`;

@@ -105,7 +154,7 @@ })

// @ts-ignore
text: `Waiter, waiter! One more <https://hackclub.slack.com/archives/${msg.channel}/p${msg.ts.replace(".","")}|OOC> please!`,
text: `Waiter, waiter! One more <https://${SLACK_WORKSPACE}.slack.com/archives/${msg.channel}/p${msg.ts.replace(".", "")}|OOC> please!`,
});
},
},
"private": {
private: {
me: async (msg: ShallowMessageMetadata) => {

@@ -130,16 +179,29 @@ const message = await client.conversationsHistory({

const channels = [...messageText.matchAll(/<#(\w+)(?:\|[^>]*)?>/g)];
const channelIds = channels.flatMap((channel) =>
channel[1] ? [channel[1]] : [],
);
const channelNames = await Promise.all(channels.map(channel => getChannelName(channel[1] ?? "")));
const channelNames = await getChannelNames(channelIds);
const constructedMessage = channels.length > 0
? channels.map((channel, index) => `\`${channel[1]}\`: ${channelNames[index]}`).join("\n")
: "No channels found in message";
const constructedMessage =
channelIds.length > 0
? channelIds
.map(
(channelId, index) =>
`\`${channelId}\`: ${channelNames[index]}`,
)
.join("\n")
: "No channels found in message";
chatPostEphemeral(msg.channel, constructedMessage, msg.thread_ts);
await chatPostEphemeral(
msg.channel,
constructedMessage,
msg.thread_ts,
);
},
},
}
};
const COMMANDS: Record<string, Command> = {
"help": {
help: {
description: "Show this help message.",

@@ -151,17 +213,27 @@ args: "[command]",

const command = COMMANDS[commandName];
const usage = command.args ? `${commandName} ${command.args}` : commandName;
chatPostEphemeral(msg.channel, `${usage}: ${command.description}`, msg.thread_ts);
const usage = command.args
? `${commandName} ${command.args}`
: commandName;
await chatPostEphemeral(
msg.channel,
`${usage}: ${command.description}`,
msg.thread_ts,
);
return;
}
chatPostEphemeral(msg.channel, `Available commands:\n${formatHelpText()}`, msg.thread_ts);
await chatPostEphemeral(
msg.channel,
`Available commands:\n${formatHelpText()}`,
msg.thread_ts,
);
},
},
"ping": {
ping: {
description: "Check whether the selfbot is running.",
handler: async (msg: MessageMetadata, args: string[]) => {
chatPostEphemeral(msg.channel, `Pong!`, msg.thread_ts);
await chatPostEphemeral(msg.channel, `Pong!`, msg.thread_ts);
},
},
"echo": {
echo: {
description: "Send a message as yourself.",

@@ -180,3 +252,3 @@ args: "<text>",

},
"id": {
id: {
description: "Get the ID of a user, channel or usergroup.",

@@ -187,3 +259,7 @@ args: "<@user|#channel|@usergroup>",

if (!target) {
chatPostEphemeral(msg.channel, "Please provide a user, channel or usergroup.", msg.thread_ts);
await chatPostEphemeral(
msg.channel,
"Please provide a user, channel or usergroup.",
msg.thread_ts,
);
return;

@@ -199,3 +275,6 @@ }

id = target.slice(2, -1);
} else if (target.startsWith("<!subteam^") && target.endsWith(">")) {
} else if (
target.startsWith("<!subteam^") &&
target.endsWith(">")
) {
// Usergroup

@@ -206,21 +285,28 @@ id = target.slice(10, -1);

if (!id) {
chatPostEphemeral(msg.channel, "Invalid user, channel or usergroup.", msg.thread_ts);
await chatPostEphemeral(
msg.channel,
"Invalid user, channel or usergroup.",
msg.thread_ts,
);
return;
}
chatPostEphemeral(msg.channel, id, msg.thread_ts);
await chatPostEphemeral(msg.channel, id, msg.thread_ts);
},
},
}
};
function connect() {
if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
if (
ws &&
(ws.readyState === WebSocket.CONNECTING ||
ws.readyState === WebSocket.OPEN)
) {
return;
}
ws = new WebSocket(
websocketUrl, {
ws = new WebSocket(websocketUrl, {
headers: {
Cookie: `d=${SLACK_COOKIE}`
}
Cookie: `d=${SLACK_COOKIE}`,
},
});

@@ -248,30 +334,90 @@

await trigger.any(msg, data.reaction);
} else if (data.user === userInfo.user_id && trigger.me) {
} else if (
data.user === selfUserInfo.user_id &&
trigger.me
) {
await trigger.me(msg, data.reaction);
}
}
break;
case "message":
if (data.subtype !== "me_message" || data.user !== userInfo.user_id) return;
client.chatDelete({
channel: data.channel,
ts: data.ts,
});
const messageText: string = data.text.trim();
const [command, ...args] = messageText.split(/\s+/);
if (!command) return;
if (
data.subtype === "me_message" &&
data.user === selfUserInfo.user_id
) {
client.chatDelete({
channel: data.channel,
ts: data.ts,
});
const messageText: string = data.text.trim();
const [command, ...args] = messageText.split(/\s+/);
if (!command) return;
const msg: MessageMetadata = {
text: messageText,
blocks: data.blocks,
ts: data.ts,
channel: data.channel,
user: data.user,
thread_ts: data.thread_ts,
};
const msg: MessageMetadata = {
text: messageText,
blocks: data.blocks,
ts: data.ts,
channel: data.channel,
user: data.user,
thread_ts: data.thread_ts,
};
const commandDef = COMMANDS[command];
if (commandDef) {
await commandDef.handler(msg, args);
const commandDef = COMMANDS[command];
if (commandDef) {
await commandDef.handler(msg, args);
}
} else if (
data.subtype === "message_changed" ||
data.subtype === "message_deleted"
) {
const oldMessage = data.previous_message;
const subtype = data.subtype;
if (selfUserInfo.user_id === oldMessage.user) return; // skip self
if (oldMessage.metadata?.event_type === "anchor") return; // skip anchors
if (oldMessage.app_id) return; // skip bots
if (data.subtype === "message_changed" && (oldMessage.attachments?.length !== data.message?.attachments?.length)) return; // skip url unfurls
if (
data.ts === "1780106006.044689" &&
data.channel === "C0266FRGT"
)
return; // thanks manitej!
const userInfo = await getUserInfo(oldMessage.user);
const action =
subtype === "message_changed" ? "edited" : "deleted";
//console.log(`User ${userInfo.displayName} (${userInfo.userId}) ${action} a message in channel ${data.channel}: ${oldMessage.text}`);
let blocks: any[] = oldMessage.blocks;
const editedTs =
subtype === "message_changed"
? data.message.ts.split(".")[0]
: Math.round(new Date().getTime() / 1000);
const fallbackTsText = new Date(
editedTs * 1000,
).toLocaleString();
const messageHardLink = `https://${SLACK_WORKSPACE}.slack.com/archives/${data.channel}/p${oldMessage.ts.replace(".", "")}`;
if (blocks[0] && blocks[0].type === "rich_text") {
blocks[0].elements[0].elements.unshift({
type: "text",
text: ": ",
});
blocks[0].elements[0].elements.unshift({
type: "user",
user_id: oldMessage.user,
});
}
blocks.push({
type: "context",
elements: [
{
type: "mrkdwn",
text: `<${messageHardLink}|${action}> at <!date^${editedTs}^{time_secs} {date_short_pretty}|${fallbackTsText}>`,
},
],
});
await chatPostEphemeral(
data.channel,
`${userInfo.displayName}: ${oldMessage.text} (${action})`,
oldMessage.thread_ts,
blocks,
undefined,
);
}

@@ -278,0 +424,0 @@ break;

@@ -6,7 +6,14 @@ {

"readme": "README.md",
"version": "0.1.0",
"version": "0.1.1",
"bin": "./index.ts",
"scripts": {
"format": "prettier -w ."
},
"engines": {
"node": ">=20"
},
"devDependencies": {
"@types/bun": "^1.3.14",
"@types/node": "^26.1.0"
"@types/node": "^26.1.0",
"prettier": "^3.9.4"
},

@@ -13,0 +20,0 @@ "peerDependencies": {

@@ -5,2 +5,14 @@ # slack-selfbot

## What does it do?
- Lets you run selfbot commands by using `/me` messages
- Supports these commands:
- `help [command]` - show available commands or details for one command
- `ping` - check whether the selfbot is alive
- `echo <text>` - send a normal message as yourself with mrkdwn support
- `id <@user|#channel|@usergroup>` - gets the SLack ID from the provided parameter
- Watches for a couple reaction shortcuts:
- `:i-would-ooc-this-but-i-cant:` - DMs you every time someone reacts to a message with it
- `:private:` - gives you the name of the private channel
## Setup

@@ -7,0 +19,0 @@