95 lines
2.7 KiB
Python
95 lines
2.7 KiB
Python
from __future__ import annotations
|
||
|
||
import re
|
||
import unicodedata
|
||
from typing import Any
|
||
from typing import Iterable
|
||
from typing import Tuple
|
||
|
||
_SLASH_CHARS = "//⁄∕╱⧸"
|
||
_COMMAND_RE = re.compile(
|
||
rf"^[\s\u00A0]*[{re.escape(_SLASH_CHARS)}]\s*([A-Za-z0-9_]+)(?:@([A-Za-z0-9_]+))?",
|
||
flags=re.ASCII,
|
||
)
|
||
|
||
|
||
def _clean_text(text: str | None) -> str:
|
||
raw = str(text or "")
|
||
if not raw:
|
||
return ""
|
||
cleaned: list[str] = []
|
||
for ch in raw:
|
||
code = ord(ch)
|
||
if 0xFE00 <= code <= 0xFE0F:
|
||
continue
|
||
if 0xE0100 <= code <= 0xE01EF:
|
||
continue
|
||
if unicodedata.category(ch) == "Cf":
|
||
continue
|
||
cleaned.append(ch)
|
||
normalized = "".join(cleaned).strip()
|
||
if not normalized:
|
||
return ""
|
||
for slash in ("/", "⁄", "∕", "╱", "⧸"):
|
||
normalized = normalized.replace(slash, "/")
|
||
return normalized
|
||
|
||
|
||
def _parse_command_token(text: str | None) -> Tuple[str, str]:
|
||
normalized = _clean_text(text)
|
||
if not normalized:
|
||
return ("", "")
|
||
match = _COMMAND_RE.match(normalized)
|
||
if not match:
|
||
return ("", "")
|
||
command = str(match.group(1) or "").strip().lower()
|
||
username = str(match.group(2) or "").strip().lower()
|
||
return (command, username)
|
||
|
||
|
||
def extract_command_token(
|
||
text: str | None,
|
||
entities: Iterable[Any] | None = None,
|
||
) -> Tuple[str, str]:
|
||
raw = str(text or "")
|
||
if entities:
|
||
for entity in entities:
|
||
if str(getattr(entity, "type", "") or "").strip() != "bot_command":
|
||
continue
|
||
try:
|
||
offset = int(getattr(entity, "offset", 0) or 0)
|
||
length = int(getattr(entity, "length", 0) or 0)
|
||
except Exception:
|
||
continue
|
||
if length <= 0:
|
||
continue
|
||
fragment = raw[offset : offset + length]
|
||
command, username = _parse_command_token(fragment)
|
||
if command:
|
||
return (command, username)
|
||
return _parse_command_token(raw)
|
||
|
||
|
||
def extract_command_name(text: str | None, entities: Iterable[Any] | None = None) -> str:
|
||
return extract_command_token(text, entities)[0]
|
||
|
||
|
||
def looks_like_slash_command(text: str | None) -> bool:
|
||
normalized = _clean_text(text)
|
||
if not normalized:
|
||
return False
|
||
return normalized[:1] == "/"
|
||
|
||
|
||
def split_command_and_args(text: str | None) -> Tuple[str, str]:
|
||
normalized = _clean_text(text)
|
||
if not normalized:
|
||
return ("", "")
|
||
match = _COMMAND_RE.match(normalized)
|
||
if not match:
|
||
return ("", "")
|
||
command = str(match.group(1) or "").strip().lower()
|
||
rest = normalized[match.end() :].lstrip()
|
||
return (command, rest)
|
||
|