# utils/core.py # # Detects whether a Discord message is **addressed to** Miku # (as opposed to merely mentioning her). import re from utils.logger import get_logger logger = get_logger('core') # ──────────────────────────────────────────────────────────────────── # Pre-compiled Miku addressing patterns # Built once at module load; is_miku_addressed() runs only 4 .search() # ──────────────────────────────────────────────────────────────────── def _build_name_variants(bases, honorifics, prefixes, connector, prefix_connector): """Return regex fragments for every name+honorific+prefix combo within a single script family.""" variants = [] for base in bases: be = re.escape(base) variants.append(be) for h in honorifics: he = re.escape(h) variants.append(be + connector + he) for p in prefixes: pe = re.escape(p) variants.append(pe + prefix_connector + be) for h in honorifics: he = re.escape(h) variants.append(pe + prefix_connector + be + connector + he) return variants def _compile_addressing_patterns(): """Compile the four addressing regexes. START – name at the beginning, followed by punctuation "Miku, how are you?" "みく!聞いて" END – comma then name at the end "how are you, Miku?" "教えて、ミク" MIDDLE – name surrounded by commas (vocative) "On the contrary, Miku, I think…" ALONE – name is the entire message "Miku" "みく!" "ミクちゃん" """ latin = _build_name_variants( bases=['miku'], honorifics=[ 'chan', 'san', 'kun', 'nyan', 'hime', 'tan', 'chin', 'heika', 'denka', 'kakka', 'shi', 'chama', 'kyun', 'dono', 'sensei', 'senpai', 'jou', ], prefixes=['o-'], connector=r'[\-\s]?', prefix_connector=r'\s?', ) cyrillic = _build_name_variants( bases=['мику'], honorifics=[ 'чан', 'сан', 'кун', 'нян', 'химе', 'тан', 'чин', 'хейка', 'хеика', 'денка', 'какка', 'си', 'чама', 'кюн', 'доно', 'сенсэй', 'сенсеи', 'сенпай', 'сенпаи', 'джо', ], prefixes=['о-'], connector=r'[\-\s]?', prefix_connector=r'\s?', ) japanese = _build_name_variants( bases=['みく', 'ミク', '未来'], honorifics=[ # Hiragana 'ちゃん', 'さん', 'くん', 'にゃん', 'ひめ', 'たん', 'ちん', 'へいか', 'でんか', 'かっか', 'し', 'ちゃま', 'きゅん', 'どの', 'せんせい', 'せんぱい', 'じょう', # Katakana 'チャン', 'サン', 'クン', 'ニャン', 'ヒメ', 'タン', 'チン', 'ヘイカ', 'デンカ', 'カッカ', 'シ', 'チャマ', 'キュン', 'ドノ', 'センセイ', 'センパイ', 'ジョウ', ], prefixes=['お', 'オ'], connector=r'[-]?', prefix_connector=r'', ) # Longest-first so the regex engine prefers the most specific match all_v = sorted(latin + cyrillic + japanese, key=len, reverse=True) alts = '|'.join(all_v) NAME = rf'\b(?:{alts})\b' PUNCT = r'[,,、::!!??.。]' # addressing punctuation after name COMMA = r'[,,、]' # comma variants (before name / vocative) ETRAIL = r'[!!??.。~~]*' # optional trailing at end ATRAIL = r'[!!??.。~~♪♡❤]*' # optional trailing for name-only messages start_re = re.compile(rf'^\s*{NAME}\s*{PUNCT}', re.IGNORECASE) end_re = re.compile(rf'{COMMA}\s*{NAME}\s*{ETRAIL}\s*$', re.IGNORECASE) middle_re = re.compile(rf'{COMMA}\s*{NAME}\s*{COMMA}', re.IGNORECASE) alone_re = re.compile(rf'^\s*{NAME}\s*{ATRAIL}\s*$', re.IGNORECASE) logger.info(f"Miku addressing: {len(all_v)} name variants compiled into 4 patterns") return start_re, end_re, middle_re, alone_re try: _START_RE, _END_RE, _MIDDLE_RE, _ALONE_RE = _compile_addressing_patterns() except Exception as e: logger.error(f"Failed to compile addressing patterns: {e}") _START_RE = _END_RE = _MIDDLE_RE = _ALONE_RE = None # ──────────────────────────────────────────────────────────────────── async def is_miku_addressed(message) -> bool: """Return True only when the message is directed *at* Miku, not merely mentioning her. Always responds to: DMs, @mentions, replies to Miku's messages. For normal messages checks whether Miku's name (in any supported script / honorific combination) appears in an "addressing" position: • Start – "Miku, how are you?" • End – "how are you, Miku?" • Middle – "On the contrary, Miku, I think…" • Alone – "Miku!" / "ミクちゃん" Does NOT trigger on mere mentions: • "I like Miku" / "Miku is cool" / "told miku about it" """ # DMs – always respond if message.guild is None: return True if not message.guild or not message.guild.me: logger.warning(f"Invalid guild/guild.me for message from {message.author}") return False # @mention if message.guild.me in message.mentions: return True # Reply to Miku if message.reference: try: ref = await message.channel.fetch_message(message.reference.message_id) if ref.author == message.guild.me: return True except Exception as e: logger.warning(f"Could not fetch referenced message: {e}") # Regex addressing (4 pre-compiled patterns) if _START_RE is None: logger.error("Addressing patterns not compiled – skipping pattern check") return False text = message.content.strip() return bool( _START_RE.search(text) or _END_RE.search(text) or _MIDDLE_RE.search(text) or _ALONE_RE.search(text) )