foobot.py

· ficd's pastes · raw

expires: 2026-02-13

  1import os
  2import random
  3import re
  4import string
  5from collections import deque
  6
  7import discord
  8import dotenv
  9from discord import Message
 10from rapidfuzz import fuzz
 11
 12_DISCORD_MARKUP = re.compile(
 13    (
 14        r"<(?:[@#&!]\d+|a?:\w+:\d+)>|"  # mentions, custom emoji
 15        r"[\U0001f600-\U0001f64f"  # emoticons
 16        r"\U0001f300-\U0001f5ff"  # symbols & pictographs
 17        r"\U0001f680-\U0001f6ff"  # transport & map
 18        r"\U0001f1e0-\U0001f1ff]"  # flags
 19    ),
 20    flags=re.UNICODE,
 21)
 22_PUNCT = string.punctuation.replace("?", "")
 23
 24RESPONSES = [
 25    "Yes",
 26    "Yeah",
 27    "Yep",
 28    "Mhm",
 29    "Yea",
 30    "Absolutely",
 31    "Indeed",
 32    "Without a doubt",
 33    "Obviously",
 34]
 35
 36KEY_PHRASES = [
 37    "is this true",
 38    "is that true",
 39    "is it true",
 40    "are you sure",
 41    "is this correct",
 42    "is that correct",
 43    "is this real",
 44    "can you verify",
 45    "is that accurate",
 46]
 47
 48PATTERNS = [
 49    r"\bis (this|that|it) true\b",
 50    r"\bare you sure\b",
 51    r"\btrue\??$",
 52]
 53
 54_last_replies: deque[str] = deque(maxlen=3)
 55
 56intents = discord.Intents.default()
 57intents.message_content = True
 58
 59client = discord.Client(intents=intents)
 60
 61
 62@client.event
 63async def on_ready():
 64    print(f"We have logged in as {client.user}")
 65
 66
 67def strip_discord_markup(text: str) -> str:
 68    return _DISCORD_MARKUP.sub(" ", text)
 69
 70
 71def normalize(text: str) -> str:
 72    text = text.lower()
 73    # remove punctuation besides ? marks
 74    for p in _PUNCT:
 75        text = text.replace(p, " ")
 76    return " ".join(text.split())
 77
 78
 79FLEX_PATTERN = re.compile(r"is\s+(?:this|that|it)\s+\w*\s*true")
 80ALT_PATTERN = re.compile(r"(?:this|that|it)\s+is\s+true")
 81
 82
 83def contains_truth_question(text: str) -> bool:
 84    text = normalize(strip_discord_markup(text))
 85
 86    # check direct patterns (strict)
 87    for p in PATTERNS:
 88        if re.search(p, text):
 89            return True
 90
 91    # flexible phrasing
 92    if re.search(FLEX_PATTERN, text):
 93        return True
 94
 95    # reversed phrasing
 96    if re.search(ALT_PATTERN, text):
 97        return True
 98
 99    # fuzzy "contains" match
100    for p in KEY_PHRASES:
101        if fuzz.partial_ratio(text, p) >= 80:
102            return True
103
104    return False
105
106
107def pick_reply_simple() -> str:
108    candidates: list[str] = [
109        r for r in RESPONSES if r not in _last_replies
110    ] or RESPONSES
111    reply: str = random.choice(candidates)
112    _last_replies.append(reply)
113    return reply
114
115
116def reply_dispatch(text: str) -> str | None:
117    if contains_truth_question(text):
118        return pick_reply_simple()
119
120
121@client.event
122async def on_message(message: Message):
123    assert client.user is not None
124    if message.author == client.user:
125        return
126    if client.user.mentioned_in(message):
127        reply = reply_dispatch(message.content)
128        if reply:
129            _ = await message.reply(reply)
130
131
132def main():
133    if not dotenv.load_dotenv():
134        print("Missing .env file!")
135        exit(1)
136    TOKEN = os.getenv("DISCORD_TOKEN")
137    if not TOKEN:
138        print("Missing token.")
139        exit(1)
140    assert isinstance(TOKEN, str)
141    client.run(TOKEN)
142
143
144if __name__ == "__main__":
145    main()