319 lines
13 KiB
Python
319 lines
13 KiB
Python
|
|
import random
|
||
|
|
import time
|
||
|
|
import logging
|
||
|
|
|
||
|
|
from nio import AsyncClient
|
||
|
|
|
||
|
|
from utils import send_text, send_html, send_reaction, sanitize_input
|
||
|
|
from config import MAX_DICE_SIDES, MAX_DICE_COUNT, BOT_PREFIX
|
||
|
|
|
||
|
|
logger = logging.getLogger("matrixbot")
|
||
|
|
|
||
|
|
# Registry: name -> (handler, description)
|
||
|
|
COMMANDS = {}
|
||
|
|
|
||
|
|
|
||
|
|
def command(name, description=""):
|
||
|
|
def decorator(func):
|
||
|
|
COMMANDS[name] = (func, description)
|
||
|
|
return func
|
||
|
|
return decorator
|
||
|
|
|
||
|
|
|
||
|
|
# ==================== COMMANDS ====================
|
||
|
|
|
||
|
|
|
||
|
|
@command("help", "Show all available commands")
|
||
|
|
async def cmd_help(client: AsyncClient, room_id: str, sender: str, args: str):
|
||
|
|
lines_plain = ["Commands:"]
|
||
|
|
lines_html = ["<h4>Commands</h4><ul>"]
|
||
|
|
|
||
|
|
for cmd_name, (_, desc) in sorted(COMMANDS.items()):
|
||
|
|
lines_plain.append(f" {BOT_PREFIX}{cmd_name} - {desc}")
|
||
|
|
lines_html.append(f"<li><strong>{BOT_PREFIX}{cmd_name}</strong> — {desc}</li>")
|
||
|
|
|
||
|
|
lines_html.append("</ul>")
|
||
|
|
await send_html(client, room_id, "\n".join(lines_plain), "\n".join(lines_html))
|
||
|
|
|
||
|
|
|
||
|
|
@command("ping", "Check bot latency")
|
||
|
|
async def cmd_ping(client: AsyncClient, room_id: str, sender: str, args: str):
|
||
|
|
start = time.monotonic()
|
||
|
|
resp = await send_text(client, room_id, "Pong!")
|
||
|
|
elapsed = (time.monotonic() - start) * 1000
|
||
|
|
# Edit isn't straightforward in Matrix, so just send a follow-up if slow
|
||
|
|
if elapsed > 500:
|
||
|
|
await send_text(client, room_id, f"(round-trip: {elapsed:.0f}ms)")
|
||
|
|
|
||
|
|
|
||
|
|
@command("8ball", "Ask the magic 8-ball a question")
|
||
|
|
async def cmd_8ball(client: AsyncClient, room_id: str, sender: str, args: str):
|
||
|
|
if not args:
|
||
|
|
await send_text(client, room_id, f"Usage: {BOT_PREFIX}8ball <question>")
|
||
|
|
return
|
||
|
|
|
||
|
|
responses = [
|
||
|
|
"It is certain", "Without a doubt", "You may rely on it",
|
||
|
|
"Yes definitely", "It is decidedly so", "As I see it, yes",
|
||
|
|
"Most likely", "Yes sir!", "Hell yeah my dude", "100% easily",
|
||
|
|
"Reply hazy try again", "Ask again later", "Better not tell you now",
|
||
|
|
"Cannot predict now", "Concentrate and ask again", "Idk bro",
|
||
|
|
"Don't count on it", "My reply is no", "My sources say no",
|
||
|
|
"Outlook not so good", "Very doubtful", "Hell no", "Prolly not",
|
||
|
|
]
|
||
|
|
|
||
|
|
answer = random.choice(responses)
|
||
|
|
plain = f"Question: {args}\nAnswer: {answer}"
|
||
|
|
html = (
|
||
|
|
f"<strong>Magic 8-Ball</strong><br>"
|
||
|
|
f"<em>Q:</em> {args}<br>"
|
||
|
|
f"<em>A:</em> {answer}"
|
||
|
|
)
|
||
|
|
await send_html(client, room_id, plain, html)
|
||
|
|
|
||
|
|
|
||
|
|
@command("fortune", "Get a fortune cookie message")
|
||
|
|
async def cmd_fortune(client: AsyncClient, room_id: str, sender: str, args: str):
|
||
|
|
fortunes = [
|
||
|
|
"If you eat something & nobody sees you eat it, it has no calories",
|
||
|
|
"Your pet is plotting world domination",
|
||
|
|
"Error 404: Fortune not found. Try again after system reboot",
|
||
|
|
"The fortune you seek is in another cookie",
|
||
|
|
"A journey of a thousand miles begins with ordering delivery",
|
||
|
|
"You will find great fortune... in between your couch cushions",
|
||
|
|
"A true friend is someone who tells you when your stream is muted",
|
||
|
|
"Your next competitive match will be legendary",
|
||
|
|
"The cake is still a lie",
|
||
|
|
"Press Alt+F4 for instant success",
|
||
|
|
"You will not encounter any campers today",
|
||
|
|
"Your tank will have a healer",
|
||
|
|
"No one will steal your pentakill",
|
||
|
|
"Your random teammate will have a mic",
|
||
|
|
"You will find diamonds on your first dig",
|
||
|
|
"The boss will drop the rare loot",
|
||
|
|
"Your speedrun will be WR pace",
|
||
|
|
"No lag spikes in your next match",
|
||
|
|
"Your gaming chair will grant you powers",
|
||
|
|
"The RNG gods will bless you",
|
||
|
|
"You will not get third partied",
|
||
|
|
"Your squad will actually stick together",
|
||
|
|
"The enemy team will forfeit at 15",
|
||
|
|
"Your aim will be crispy today",
|
||
|
|
"You will escape the backrooms",
|
||
|
|
"The imposter will not sus you",
|
||
|
|
"Your Minecraft bed will remain unbroken",
|
||
|
|
"You will get Play of the Game",
|
||
|
|
"Your next meme will go viral",
|
||
|
|
"Someone is talking about you in their Discord server",
|
||
|
|
"Your FBI agent thinks you're hilarious",
|
||
|
|
"Your next TikTok will hit the FYP, if the government doesn't ban it first",
|
||
|
|
"Someone will actually read your Twitter thread",
|
||
|
|
"Your DMs will be blessed with quality memes today",
|
||
|
|
"Touch grass (respectfully)",
|
||
|
|
"The algorithm will be in your favor today",
|
||
|
|
"Your next Spotify shuffle will hit different",
|
||
|
|
"Someone saved your Instagram post",
|
||
|
|
"Your Reddit comment will get gold",
|
||
|
|
"POV: You're about to go viral",
|
||
|
|
"Main character energy detected",
|
||
|
|
"No cap, you're gonna have a great day fr fr",
|
||
|
|
"Your rizz levels are increasing",
|
||
|
|
"You will not get ratio'd today",
|
||
|
|
"Someone will actually use your custom emoji",
|
||
|
|
"Your next selfie will be iconic",
|
||
|
|
"Buy a dolphin - your life will have a porpoise",
|
||
|
|
"Stop procrastinating - starting tomorrow",
|
||
|
|
"Catch fire with enthusiasm - people will come for miles to watch you burn",
|
||
|
|
"Your code will compile on the first try today",
|
||
|
|
"A semicolon will save your day",
|
||
|
|
"The bug you've been hunting is just a typo",
|
||
|
|
"Your next Git commit will be perfect",
|
||
|
|
"You will find the solution on the first StackOverflow link",
|
||
|
|
"Your Docker container will build without errors",
|
||
|
|
"The cloud is just someone else's computer",
|
||
|
|
"Your backup strategy will soon prove its worth",
|
||
|
|
"A mechanical keyboard is in your future",
|
||
|
|
"You will finally understand regex... maybe",
|
||
|
|
"Your CSS will align perfectly on the first try",
|
||
|
|
"Someone will star your GitHub repo today",
|
||
|
|
"Your Linux installation will not break after updates",
|
||
|
|
"You will remember to push your changes before shutdown",
|
||
|
|
"Your code comments will actually make sense in 6 months",
|
||
|
|
"The missing curly brace is on line 247",
|
||
|
|
"Have you tried turning it off and on again?",
|
||
|
|
"Your next pull request will be merged without comments",
|
||
|
|
"Your keyboard RGB will sync perfectly today",
|
||
|
|
"You will find that memory leak",
|
||
|
|
"Your next algorithm will have O(1) complexity",
|
||
|
|
"The force quit was strong with this one",
|
||
|
|
"Ctrl+S will save you today",
|
||
|
|
"Your next Python script will need no debugging",
|
||
|
|
"Your next API call will return 200 OK",
|
||
|
|
]
|
||
|
|
|
||
|
|
fortune = random.choice(fortunes)
|
||
|
|
plain = f"Fortune Cookie: {fortune}"
|
||
|
|
html = f"<strong>Fortune Cookie</strong><br>{fortune}"
|
||
|
|
await send_html(client, room_id, plain, html)
|
||
|
|
|
||
|
|
|
||
|
|
@command("flip", "Flip a coin")
|
||
|
|
async def cmd_flip(client: AsyncClient, room_id: str, sender: str, args: str):
|
||
|
|
result = random.choice(["Heads", "Tails"])
|
||
|
|
plain = f"Coin Flip: {result}"
|
||
|
|
html = f"<strong>Coin Flip:</strong> {result}"
|
||
|
|
await send_html(client, room_id, plain, html)
|
||
|
|
|
||
|
|
|
||
|
|
@command("roll", "Roll dice (e.g. !roll 2d6)")
|
||
|
|
async def cmd_roll(client: AsyncClient, room_id: str, sender: str, args: str):
|
||
|
|
dice_str = args.strip() if args.strip() else "1d6"
|
||
|
|
|
||
|
|
try:
|
||
|
|
num, sides = map(int, dice_str.lower().split("d"))
|
||
|
|
except ValueError:
|
||
|
|
await send_text(client, room_id, f"Usage: {BOT_PREFIX}roll NdS (example: 2d6)")
|
||
|
|
return
|
||
|
|
|
||
|
|
if num < 1 or num > MAX_DICE_COUNT:
|
||
|
|
await send_text(client, room_id, f"Number of dice must be 1-{MAX_DICE_COUNT}")
|
||
|
|
return
|
||
|
|
if sides < 2 or sides > MAX_DICE_SIDES:
|
||
|
|
await send_text(client, room_id, f"Sides must be 2-{MAX_DICE_SIDES}")
|
||
|
|
return
|
||
|
|
|
||
|
|
results = [random.randint(1, sides) for _ in range(num)]
|
||
|
|
total = sum(results)
|
||
|
|
plain = f"Dice Roll ({dice_str}): {results} = {total}"
|
||
|
|
html = (
|
||
|
|
f"<strong>Dice Roll</strong> ({dice_str})<br>"
|
||
|
|
f"Rolls: {results}<br>"
|
||
|
|
f"Total: <strong>{total}</strong>"
|
||
|
|
)
|
||
|
|
await send_html(client, room_id, plain, html)
|
||
|
|
|
||
|
|
|
||
|
|
@command("random", "Random number (e.g. !random 1 100)")
|
||
|
|
async def cmd_random(client: AsyncClient, room_id: str, sender: str, args: str):
|
||
|
|
parts = args.split()
|
||
|
|
try:
|
||
|
|
lo = int(parts[0]) if len(parts) >= 1 else 1
|
||
|
|
hi = int(parts[1]) if len(parts) >= 2 else 100
|
||
|
|
except ValueError:
|
||
|
|
await send_text(client, room_id, f"Usage: {BOT_PREFIX}random <min> <max>")
|
||
|
|
return
|
||
|
|
|
||
|
|
if lo > hi:
|
||
|
|
lo, hi = hi, lo
|
||
|
|
|
||
|
|
result = random.randint(lo, hi)
|
||
|
|
plain = f"Random ({lo}-{hi}): {result}"
|
||
|
|
html = f"<strong>Random Number</strong> ({lo}\u2013{hi}): <strong>{result}</strong>"
|
||
|
|
await send_html(client, room_id, plain, html)
|
||
|
|
|
||
|
|
|
||
|
|
@command("rps", "Rock Paper Scissors")
|
||
|
|
async def cmd_rps(client: AsyncClient, room_id: str, sender: str, args: str):
|
||
|
|
choices = ["rock", "paper", "scissors"]
|
||
|
|
choice = args.strip().lower()
|
||
|
|
|
||
|
|
if choice not in choices:
|
||
|
|
await send_text(client, room_id, f"Usage: {BOT_PREFIX}rps <rock|paper|scissors>")
|
||
|
|
return
|
||
|
|
|
||
|
|
bot_choice = random.choice(choices)
|
||
|
|
|
||
|
|
if choice == bot_choice:
|
||
|
|
result = "It's a tie!"
|
||
|
|
elif (
|
||
|
|
(choice == "rock" and bot_choice == "scissors")
|
||
|
|
or (choice == "paper" and bot_choice == "rock")
|
||
|
|
or (choice == "scissors" and bot_choice == "paper")
|
||
|
|
):
|
||
|
|
result = "You win!"
|
||
|
|
else:
|
||
|
|
result = "Bot wins!"
|
||
|
|
|
||
|
|
plain = f"RPS: You={choice}, Bot={bot_choice} -> {result}"
|
||
|
|
html = (
|
||
|
|
f"<strong>Rock Paper Scissors</strong><br>"
|
||
|
|
f"You: {choice.capitalize()} | Bot: {bot_choice.capitalize()}<br>"
|
||
|
|
f"<strong>{result}</strong>"
|
||
|
|
)
|
||
|
|
await send_html(client, room_id, plain, html)
|
||
|
|
|
||
|
|
|
||
|
|
@command("poll", "Create a yes/no poll")
|
||
|
|
async def cmd_poll(client: AsyncClient, room_id: str, sender: str, args: str):
|
||
|
|
if not args:
|
||
|
|
await send_text(client, room_id, f"Usage: {BOT_PREFIX}poll <question>")
|
||
|
|
return
|
||
|
|
|
||
|
|
plain = f"Poll: {args}"
|
||
|
|
html = f"<strong>Poll</strong><br>{args}"
|
||
|
|
resp = await send_html(client, room_id, plain, html)
|
||
|
|
|
||
|
|
if hasattr(resp, "event_id"):
|
||
|
|
await send_reaction(client, room_id, resp.event_id, "\U0001f44d")
|
||
|
|
await send_reaction(client, room_id, resp.event_id, "\U0001f44e")
|
||
|
|
|
||
|
|
|
||
|
|
@command("champion", "Random LoL champion (optional: !champion top)")
|
||
|
|
async def cmd_champion(client: AsyncClient, room_id: str, sender: str, args: str):
|
||
|
|
champions = {
|
||
|
|
"Top": [
|
||
|
|
"Aatrox", "Ambessa", "Aurora", "Camille", "Cho'Gath", "Darius",
|
||
|
|
"Dr. Mundo", "Fiora", "Gangplank", "Garen", "Gnar", "Gragas",
|
||
|
|
"Gwen", "Illaoi", "Irelia", "Jax", "Jayce", "K'Sante", "Kennen",
|
||
|
|
"Kled", "Malphite", "Mordekaiser", "Nasus", "Olaf", "Ornn",
|
||
|
|
"Poppy", "Quinn", "Renekton", "Riven", "Rumble", "Sett", "Shen",
|
||
|
|
"Singed", "Sion", "Teemo", "Trundle", "Tryndamere", "Urgot",
|
||
|
|
"Vladimir", "Volibear", "Wukong", "Yone", "Yorick",
|
||
|
|
],
|
||
|
|
"Jungle": [
|
||
|
|
"Amumu", "Bel'Veth", "Briar", "Diana", "Ekko", "Elise",
|
||
|
|
"Evelynn", "Fiddlesticks", "Graves", "Hecarim", "Ivern",
|
||
|
|
"Jarvan IV", "Kayn", "Kha'Zix", "Kindred", "Lee Sin", "Lillia",
|
||
|
|
"Maokai", "Master Yi", "Nidalee", "Nocturne", "Nunu", "Olaf",
|
||
|
|
"Rek'Sai", "Rengar", "Sejuani", "Shaco", "Skarner", "Taliyah",
|
||
|
|
"Udyr", "Vi", "Viego", "Warwick", "Xin Zhao", "Zac",
|
||
|
|
],
|
||
|
|
"Mid": [
|
||
|
|
"Ahri", "Akali", "Akshan", "Annie", "Aurelion Sol", "Azir",
|
||
|
|
"Cassiopeia", "Corki", "Ekko", "Fizz", "Galio", "Heimerdinger",
|
||
|
|
"Hwei", "Irelia", "Katarina", "LeBlanc", "Lissandra", "Lux",
|
||
|
|
"Malzahar", "Mel", "Naafiri", "Neeko", "Orianna", "Qiyana",
|
||
|
|
"Ryze", "Sylas", "Syndra", "Talon", "Twisted Fate", "Veigar",
|
||
|
|
"Vex", "Viktor", "Vladimir", "Xerath", "Yasuo", "Yone", "Zed",
|
||
|
|
"Zoe",
|
||
|
|
],
|
||
|
|
"Bot": [
|
||
|
|
"Aphelios", "Ashe", "Caitlyn", "Draven", "Ezreal", "Jhin",
|
||
|
|
"Jinx", "Kai'Sa", "Kalista", "Kog'Maw", "Lucian",
|
||
|
|
"Miss Fortune", "Nilah", "Samira", "Sivir", "Smolder",
|
||
|
|
"Tristana", "Twitch", "Varus", "Vayne", "Xayah", "Zeri",
|
||
|
|
],
|
||
|
|
"Support": [
|
||
|
|
"Alistar", "Bard", "Blitzcrank", "Brand", "Braum", "Janna",
|
||
|
|
"Karma", "Leona", "Lulu", "Lux", "Milio", "Morgana", "Nami",
|
||
|
|
"Nautilus", "Pyke", "Rakan", "Rell", "Renata Glasc", "Senna",
|
||
|
|
"Seraphine", "Sona", "Soraka", "Swain", "Taric", "Thresh",
|
||
|
|
"Yuumi", "Zilean", "Zyra",
|
||
|
|
],
|
||
|
|
}
|
||
|
|
|
||
|
|
lane_arg = args.strip().capitalize() if args.strip() else ""
|
||
|
|
if lane_arg and lane_arg in champions:
|
||
|
|
lane = lane_arg
|
||
|
|
else:
|
||
|
|
lane = random.choice(list(champions.keys()))
|
||
|
|
|
||
|
|
champ = random.choice(champions[lane])
|
||
|
|
plain = f"Champion Picker: {champ} ({lane})"
|
||
|
|
html = (
|
||
|
|
f"<strong>League Champion Picker</strong><br>"
|
||
|
|
f"Champion: <strong>{champ}</strong><br>"
|
||
|
|
f"Lane: {lane}"
|
||
|
|
)
|
||
|
|
await send_html(client, room_id, plain, html)
|