aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorArslaan Pathan <[email protected]>2026-07-17 00:03:03 +1200
committerArslaan Pathan <[email protected]>2026-07-17 00:03:03 +1200
commit2eb0e1197b40c07de4a57701504f77345d77dc36 (patch)
tree53e6fe85f9ce6be91ab0cddc9a1dfb44969d35af /src
parentbc715b12204714dc8046f07387210852026b47b9 (diff)
downloadslack-botnet-2eb0e1197b40c07de4a57701504f77345d77dc36.tar.xz
slack-botnet-2eb0e1197b40c07de4a57701504f77345d77dc36.zip
IT WORKS YES YES YES IM SAVED
Diffstat (limited to 'src')
-rw-r--r--src/botnet.py6
-rw-r--r--src/db.py39
-rw-r--r--src/main.py59
-rw-r--r--src/utils.py67
4 files changed, 143 insertions, 28 deletions
diff --git a/src/botnet.py b/src/botnet.py
index 225f7e9..c9c6fee 100644
--- a/src/botnet.py
+++ b/src/botnet.py
@@ -76,7 +76,7 @@ async def deploy_message(user_id: str, message: str):
}
daily_count = await get_daily_user_deployment_count(user_id)
- if daily_count >= DAILY_LIMIT and user_id not in ["U0AKCBZHHMH"]:
+ if daily_count >= DAILY_LIMIT:
return {
"success": False,
"error": f"You've reached your daily limit of {DAILY_LIMIT} messages!"
@@ -122,12 +122,12 @@ async def execute_deployment(deployment_id: int):
for user in users:
try:
client = AsyncWebClient(token=user["oauth_token"])
- channel = await get_random_channel(user["oauth_token"])
+ channel = await get_random_channel(user["oauth_token"], user["slack_id"])
if not channel:
print(f"No channels found for {user['slack_id']}, using #botnet")
channel_id = "C0BGMV1CAQG"
else:
- channel_id = channel["id"]
+ channel_id = channel
response = await client.chat_postMessage(
channel=channel_id, # put it in #botnet for now, make it detect recent thread with CM approval yada yada later
text=message,
diff --git a/src/db.py b/src/db.py
index 3ff226e..f48960d 100644
--- a/src/db.py
+++ b/src/db.py
@@ -47,6 +47,14 @@ async def init_db():
PRIMARY KEY (deployment_id, voter_id)
)
""")
+ await db.execute("""
+ CREATE TABLE IF NOT EXISTS ChannelApprovals (
+ channel_id TEXT PRIMARY KEY,
+ approved_by TEXT NOT NULL,
+ approved_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (approved_by) REFERENCES Users(slack_id)
+ )
+ """)
await db.commit()
async def add_user(slack_id: str, oauth_token: str):
@@ -175,3 +183,34 @@ async def update_voting_message_ts(deployment_id: int, message_ts: str):
(message_ts, deployment_id)
)
await db.commit()
+
+async def approve_channel(channel_id: str, approver_id: str):
+ async with aiosqlite.connect(DB_PATH) as db:
+ await db.execute(
+ "INSERT OR REPLACE INTO ChannelApprovals (channel_id, approved_by) VALUES (?, ?)",
+ (channel_id, approver_id)
+ )
+ await db.commit()
+
+async def is_channel_approved(channel_id: str) -> bool:
+ async with aiosqlite.connect(DB_PATH) as db:
+ cursor = await db.execute(
+ "SELECT channel_id FROM ChannelApprovals WHERE channel_id = ?",
+ (channel_id,)
+ )
+ row = await cursor.fetchone()
+ return row is not None
+
+async def get_approved_channels() -> list[str]:
+ async with aiosqlite.connect(DB_PATH) as db:
+ cursor = await db.execute("SELECT channel_id FROM ChannelApprovals")
+ rows = await cursor.fetchall()
+ return [row[0] for row in rows]
+
+async def revoke_channel_approval(channel_id: str):
+ async with aiosqlite.connect(DB_PATH) as db:
+ await db.execute(
+ "DELETE FROM ChannelApprovals WHERE channel_id = ?",
+ (channel_id,)
+ )
+ await db.commit()
diff --git a/src/main.py b/src/main.py
index 0d63190..c2980a7 100644
--- a/src/main.py
+++ b/src/main.py
@@ -4,8 +4,8 @@ import math
from slack_bolt.app.async_app import AsyncApp
from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler
from dotenv import load_dotenv
-from .db import init_db, get_user, add_vote, get_vote_counts, get_user_vote, get_deployment, update_deployment_status, get_active_users
-from .utils import generate_voting_blocks
+from .db import init_db, get_user, add_vote, get_vote_counts, get_user_vote, get_deployment, update_deployment_status, get_active_users, approve_channel, is_channel_approved, revoke_channel_approval
+from .utils import generate_voting_blocks, is_user_cm
from .auth import app as fastapi_app
from .botnet import deploy_message, execute_deployment
import threading
@@ -31,9 +31,9 @@ async def handle_messages(message, say):
if message.get("user") == selfbot_user_id:
return
- if message.get("user") not in ["U0AKCBZHHMH"]:
- await say("Botnet is temporarily disabled for users other than <@U0AKCBZHHMH> for debugging and the safety of all OAuth-authorized users. Please DM <@U0AKCBZHHMH> if this is not resolved within a few days.")
- return
+ # if message.get("user") not in ["U0AKCBZHHMH"]:
+ # await say("Botnet is temporarily disabled for users other than <@U0AKCBZHHMH> for debugging and the safety of all OAuth-authorized users. Please DM <@U0AKCBZHHMH> if this is not resolved within a few days.")
+ # return
text = message.get("text", "").strip()
@@ -51,7 +51,7 @@ async def handle_messages(message, say):
args = parts[2:]
if command == "help":
- await say("*Botnet Commands*:\n---\n*botnet help*: shows this message\n*botnet deploy <message>*: sends a message into the deploy queue for review/voting\n*botnet approve <channel>*: approve Botnet to function in a channel (must be CM)", mrkdwn=True)
+ await say("*Botnet Commands*:\n---\n*botnet help*: shows this message\n*botnet deploy <message>*: sends a message into the deploy queue for review/voting\n*botnet approve <channel>*: approve Botnet to function in a channel (must be CM)\n*botnet revoke <channel>*: revoke approval for Botnet to unction in a channel (must be CM)", mrkdwn=True)
elif command == "deploy":
if not args:
await say("Please provide a message to deploy!")
@@ -63,7 +63,49 @@ async def handle_messages(message, say):
else:
await say(f"T_T {result['error']}")
elif command == "approve":
- await say("Not implemented yet")
+ user_id = message.get("user")
+ if not args:
+ await say(f"Please provide a channel to approve!")
+ return
+
+ channel_pattern = re.compile(r"^<#([A-Z0-9]+)(?:\|[^>]+)?>$")
+ match = channel_pattern.match(args[0].strip())
+ channel_id = match.group(1)
+
+ if not channel_id:
+ await say(f"Please provide a valid channel.")
+ return
+
+ if not await is_user_cm(channel_id, user_id):
+ await say(f"You must be a CM of that channel to perform this action!")
+ return
+
+ await approve_channel(channel_id, user_id)
+ await say(f"^_^ Approved channel {args[0].strip()}!")
+ elif command == "revoke":
+ user_id = message.get("user")
+ if not args:
+ await say(f"Please provide a channel to revoke approval!")
+ return
+
+ channel_pattern = re.compile(r"^<#([A-Z0-9]+)(?:\|[^>]+)?>$")
+ match = channel_pattern.match(args[0].strip())
+ channel_id = match.group(1)
+
+ if not channel_id:
+ await say(f"Please provide a valid channel.")
+ return
+
+ if not await is_channel_approved(channel_id):
+ await say(f"T_T Channel {args[0].strip()} is not currently approved for Botnet!")
+ return
+
+ if not await is_user_cm(channel_id, user_id):
+ await say(f"You must be a CM of that channel to perform this action!")
+ return
+
+ await revoke_channel_approval(channel_id)
+ await say(f"T_T Channel {args[0].strip()} has been revoked approval for Botnet!")
else:
await say(f"Unknown command: {command}")
except Exception as e:
@@ -100,8 +142,7 @@ async def handle_vote(ack, respond, logger, context, body, client):
active_users = await get_active_users()
total_members = len(active_users)
- # THRESHOLD = math.ceil(max(3, min(15, int(total_members * 0.2)))) # min 3, max 15 - use 20% total members
- THRESHOLD = 2 # Debug hardcoded
+ THRESHOLD = math.ceil(max(3, min(15, int(total_members * 0.2)))) # min 3, max 15 - use 20% total members
print(f"DEBUG: Threshold is {THRESHOLD}")
if approve_count >= THRESHOLD:
diff --git a/src/utils.py b/src/utils.py
index a42539f..ec614d6 100644
--- a/src/utils.py
+++ b/src/utils.py
@@ -1,5 +1,7 @@
import random
+import httpx
from slack_sdk.web.async_client import AsyncWebClient
+from .db import get_approved_channels
def generate_voting_blocks(deployment_id: int, message: str, approve_count: int, reject_count: int, voting_enabled: bool):
blocks = [
@@ -47,24 +49,57 @@ def generate_voting_blocks(deployment_id: int, message: str, approve_count: int,
return blocks
-async def get_random_channel(oauth_token: str):
- client = AsyncWebClient(token=oauth_token)
+async def get_random_channel(user_oauth_token: str, user_id: str) -> str | None:
+ approved = await get_approved_channels()
+ print(f"DEBUG get_random_channel: approved = {approved}")
+
+ if not approved:
+ print("DEBUG get_random_channel: No approved channels")
+ return None
+
+ random.shuffle(approved)
+ print(f"DEBUG get_random_channel: shuffled approved = {approved}")
+
+ for channel_id in approved:
+ print(f"DEBUG get_random_channel: checking channel {channel_id} for user {user_id}")
+ is_member = await user_is_in_channel(user_oauth_token, channel_id, user_id)
+ print(f"DEBUG get_random_channel: is_member = {is_member}")
+ if is_member:
+ print(f"DEBUG get_random_channel: FOUND! returning {channel_id}")
+ return channel_id
+
+ print("DEBUG get_random_channel: No channels found")
+ return None
+async def user_is_in_channel(oauth_token: str, channel_id: str, user_id: str) -> bool:
+ client = AsyncWebClient(token=oauth_token)
+
try:
- response = await client.conversations_list(types="public_channel", limit=200, exclude_archived=True)
-
- channels = [c for c in response.get("channels", []) if c.get("is_member", False)]
+ print(f"DEBUG user_is_in_channel: calling conversations_members for {channel_id}")
+ response = await client.conversations_members(channel=channel_id)
+ print(f"DEBUG user_is_in_channel: response ok = {response.get('ok')}")
+
+ members = response.get("members", [])
+ print(f"DEBUG user_is_in_channel: members count = {len(members)}")
+ print(f"DEBUG user_is_in_channel: looking for {user_id} in members")
+
+ return user_id in members
+ except Exception as e:
+ print(f"DEBUG user_is_in_channel: EXCEPTION - {e}")
+ return False
- if not channels:
- return None
+async def is_user_cm(channel_id: str, user_id: str):
+ try:
+ async with httpx.AsyncClient() as client:
+ response = await client.get(f"https://flaron.halceon.dev/channel/{channel_id}")
+
+ if response.status_code != 200:
+ return False
- channel_approved = False
- while not channel_approved:
- channel = random.choice(channels)
- # FIXME: Check if CM-approved here
- channel_approved = True
+ data = response.json()
- return channel
- except Exception as e:
- print(f"random channel is broken, we might be cooked: {e}")
- return None
+ managers = data.get("managers", [])
+ return user_id in managers
+ except Exception in e:
+ print(f"flaron api failed T_T {e}")
+ return False