"""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 [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 [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")