2025-12-07 17:15:09 +02:00
|
|
|
|
# utils/core.py
|
2026-03-03 12:42:33 +02:00
|
|
|
|
#
|
|
|
|
|
|
# Detects whether a Discord message is **addressed to** Miku
|
|
|
|
|
|
# (as opposed to merely mentioning her).
|
2025-12-07 17:15:09 +02:00
|
|
|
|
|
|
|
|
|
|
import re
|
feat: Implement comprehensive non-hierarchical logging system
- Created new logging infrastructure with per-component filtering
- Added 6 log levels: DEBUG, INFO, API, WARNING, ERROR, CRITICAL
- Implemented non-hierarchical level control (any combination can be enabled)
- Migrated 917 print() statements across 31 files to structured logging
- Created web UI (system.html) for runtime configuration with dark theme
- Added global level controls to enable/disable levels across all components
- Added timestamp format control (off/time/date/datetime options)
- Implemented log rotation (10MB per file, 5 backups)
- Added API endpoints for dynamic log configuration
- Configured HTTP request logging with filtering via api.requests component
- Intercepted APScheduler logs with proper formatting
- Fixed persistence paths to use /app/memory for Docker volume compatibility
- Fixed checkbox display bug in web UI (enabled_levels now properly shown)
- Changed System Settings button to open in same tab instead of new window
Components: bot, api, api.requests, autonomous, persona, vision, llm,
conversation, mood, dm, scheduled, gpu, media, server, commands,
sentiment, core, apscheduler
All settings persist across container restarts via JSON config.
2026-01-10 20:46:19 +02:00
|
|
|
|
from utils.logger import get_logger
|
|
|
|
|
|
|
|
|
|
|
|
logger = get_logger('core')
|
2025-12-07 17:15:09 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-03-03 12:42:33 +02:00
|
|
|
|
# ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
# 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
|
2025-12-07 17:15:09 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-03-03 12:42:33 +02:00
|
|
|
|
# ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
2025-12-07 17:15:09 +02:00
|
|
|
|
async def is_miku_addressed(message) -> bool:
|
2026-03-03 12:42:33 +02:00
|
|
|
|
"""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
|
2025-12-07 17:15:09 +02:00
|
|
|
|
if message.guild is None:
|
|
|
|
|
|
return True
|
2026-03-03 12:42:33 +02:00
|
|
|
|
|
2025-12-07 17:15:09 +02:00
|
|
|
|
if not message.guild or not message.guild.me:
|
2026-03-03 12:42:33 +02:00
|
|
|
|
logger.warning(f"Invalid guild/guild.me for message from {message.author}")
|
2025-12-07 17:15:09 +02:00
|
|
|
|
return False
|
2026-03-03 12:42:33 +02:00
|
|
|
|
|
|
|
|
|
|
# @mention
|
2025-12-07 17:15:09 +02:00
|
|
|
|
if message.guild.me in message.mentions:
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
2026-03-03 12:42:33 +02:00
|
|
|
|
# Reply to Miku
|
2025-12-07 17:15:09 +02:00
|
|
|
|
if message.reference:
|
|
|
|
|
|
try:
|
2026-03-03 12:42:33 +02:00
|
|
|
|
ref = await message.channel.fetch_message(message.reference.message_id)
|
|
|
|
|
|
if ref.author == message.guild.me:
|
2025-12-07 17:15:09 +02:00
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
feat: Implement comprehensive non-hierarchical logging system
- Created new logging infrastructure with per-component filtering
- Added 6 log levels: DEBUG, INFO, API, WARNING, ERROR, CRITICAL
- Implemented non-hierarchical level control (any combination can be enabled)
- Migrated 917 print() statements across 31 files to structured logging
- Created web UI (system.html) for runtime configuration with dark theme
- Added global level controls to enable/disable levels across all components
- Added timestamp format control (off/time/date/datetime options)
- Implemented log rotation (10MB per file, 5 backups)
- Added API endpoints for dynamic log configuration
- Configured HTTP request logging with filtering via api.requests component
- Intercepted APScheduler logs with proper formatting
- Fixed persistence paths to use /app/memory for Docker volume compatibility
- Fixed checkbox display bug in web UI (enabled_levels now properly shown)
- Changed System Settings button to open in same tab instead of new window
Components: bot, api, api.requests, autonomous, persona, vision, llm,
conversation, mood, dm, scheduled, gpu, media, server, commands,
sentiment, core, apscheduler
All settings persist across container restarts via JSON config.
2026-01-10 20:46:19 +02:00
|
|
|
|
logger.warning(f"Could not fetch referenced message: {e}")
|
2025-12-07 17:15:09 +02:00
|
|
|
|
|
2026-03-03 12:42:33 +02:00
|
|
|
|
# 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)
|
|
|
|
|
|
)
|