Files
derp/plugins/rand.py
user 530f33be76 feat: add wave 2 plugins and --cprofile CLI flag
Add 7 new pure-stdlib plugins: whois (raw TCP port 43), portcheck
(async TCP connect scan with internal-net guard), httpcheck (HTTP
status/redirects/timing), tlscheck (TLS version/cipher/cert inspect),
blacklist (parallel DNSBL check against 10 RBLs), rand (password/hex/
uuid/bytes/int/coin/dice), and timer (async countdown notifications).

Add --cprofile flag to CLI for profiling bot runtime. Update all docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 01:58:47 +01:00

118 lines
3.9 KiB
Python

"""Plugin: cryptographic random generators (pure stdlib)."""
from __future__ import annotations
import secrets
import string
import uuid
from derp.plugin import command
_MAX_LENGTH = 128
_DEFAULT_LENGTH = 16
_DEFAULT_HEX_LENGTH = 32
_CHARSETS = {
"alnum": string.ascii_letters + string.digits,
"alpha": string.ascii_letters,
"digits": string.digits,
"hex": string.hexdigits[:16],
"upper": string.ascii_uppercase + string.digits,
"lower": string.ascii_lowercase + string.digits,
"safe": string.ascii_letters + string.digits + "!@#$%^&*-_=+",
"all": string.ascii_letters + string.digits + string.punctuation,
}
@command("rand", help="Random gen: !rand <password|hex|uuid|bytes|int> [len]")
async def cmd_rand(bot, message):
"""Generate cryptographically random values.
Usage:
!rand password [len] [charset] -- random password (default 16)
!rand hex [len] -- random hex string (default 32)
!rand uuid -- random UUID4
!rand bytes [len] -- random bytes as hex (default 16)
!rand int [max] -- random integer 0..max (default 1000000)
!rand coin -- coin flip
!rand dice [NdM] -- dice roll (default 1d6)
"""
parts = message.text.split(None, 4)
if len(parts) < 2:
await bot.reply(message, "Usage: !rand <password|hex|uuid|bytes|int|coin|dice> [args]")
return
mode = parts[1].lower()
if mode == "password":
length = _DEFAULT_LENGTH
charset_name = "safe"
if len(parts) > 2:
try:
length = int(parts[2])
except ValueError:
charset_name = parts[2].lower()
if len(parts) > 3:
charset_name = parts[3].lower()
length = max(1, min(length, _MAX_LENGTH))
charset = _CHARSETS.get(charset_name, _CHARSETS["safe"])
pw = "".join(secrets.choice(charset) for _ in range(length))
await bot.reply(message, pw)
elif mode == "hex":
length = _DEFAULT_HEX_LENGTH
if len(parts) > 2:
try:
length = int(parts[2])
except ValueError:
pass
length = max(1, min(length, _MAX_LENGTH))
await bot.reply(message, secrets.token_hex(length // 2 + length % 2)[:length])
elif mode == "uuid":
await bot.reply(message, str(uuid.uuid4()))
elif mode == "bytes":
length = 16
if len(parts) > 2:
try:
length = int(parts[2])
except ValueError:
pass
length = max(1, min(length, _MAX_LENGTH // 2))
await bot.reply(message, secrets.token_bytes(length).hex())
elif mode == "int":
upper = 1000000
if len(parts) > 2:
try:
upper = int(parts[2])
except ValueError:
pass
upper = max(1, min(upper, 2**32))
await bot.reply(message, str(secrets.randbelow(upper)))
elif mode == "coin":
await bot.reply(message, secrets.choice(["heads", "tails"]))
elif mode == "dice":
spec = parts[2] if len(parts) > 2 else "1d6"
try:
num, _, sides = spec.lower().partition("d")
num_dice = int(num) if num else 1
num_sides = int(sides) if sides else 6
if num_dice < 1 or num_dice > 20 or num_sides < 2 or num_sides > 100:
raise ValueError
except ValueError:
await bot.reply(message, "Usage: !rand dice [NdM] (1-20 dice, 2-100 sides)")
return
rolls = [secrets.randbelow(num_sides) + 1 for _ in range(num_dice)]
total = sum(rolls)
if num_dice == 1:
await bot.reply(message, str(total))
else:
await bot.reply(message, f"{' + '.join(map(str, rolls))} = {total}")
else:
await bot.reply(message, "Modes: password, hex, uuid, bytes, int, coin, dice")