fix: classify choose() emoji keys by unicode properties, not length+ascii
_looks_unicode_emoji used 'len<=4 and not isascii', so a short non-ascii text label (Sí/да/はい/café/确定) was misread as a unicode emoji and sent as a labelless component emoji, which Discord 400s. It now treats a key as an emoji only when every codepoint is emoji-composition material (So/Sk symbols, ZWJ, variation selectors, regional indicators, keycap combiner) with at least one pictographic symbol - so a compound emoji (ZWJ family, flag, skin-tone, keycap) stays one emoji while an i18n label is a label. The terminal Discord 400 remains a documented live-gap (needs a gateway). Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
@@ -10,6 +10,7 @@ treating a timeout as a normal None return.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import unicodedata
|
||||||
from typing import Any, Optional, Union
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
@@ -227,6 +228,43 @@ def _split_key(key: Any) -> "tuple[Optional[str], Optional[Union[str, discord.Pa
|
|||||||
return str(key), None
|
return str(key), None
|
||||||
|
|
||||||
|
|
||||||
|
def _is_emoji_codepoint(ch: str) -> bool:
|
||||||
|
"""whether a single character is emoji-composition material
|
||||||
|
|
||||||
|
covers the pictographic symbols themselves (category So), skin-tone modifiers (Sk),
|
||||||
|
and the glue that binds a compound emoji into one grapheme: ZWJ (U+200D), variation
|
||||||
|
selectors (U+FE00-FE0F), regional-indicator letters (flags, U+1F1E6-1F1FF), and the
|
||||||
|
keycap combiner (U+20E3). a plain letter/digit/space is none of these.
|
||||||
|
"""
|
||||||
|
code = ord(ch)
|
||||||
|
if ch == "" or ch == "⃣":
|
||||||
|
return True
|
||||||
|
if 0xFE00 <= code <= 0xFE0F:
|
||||||
|
return True
|
||||||
|
if 0x1F1E6 <= code <= 0x1F1FF:
|
||||||
|
return True
|
||||||
|
return unicodedata.category(ch) in ("So", "Sk")
|
||||||
|
|
||||||
|
|
||||||
def _looks_unicode_emoji(value: str) -> bool:
|
def _looks_unicode_emoji(value: str) -> bool:
|
||||||
"""heuristic: a short non-ascii token is treated as a unicode emoji key"""
|
"""whether value is a unicode emoji key rather than a text label
|
||||||
return bool(value) and len(value) <= 4 and not value.isascii()
|
|
||||||
|
true only when EVERY codepoint is emoji-composition material (see _is_emoji_codepoint)
|
||||||
|
and at least one is a pictographic symbol - so a compound emoji (ZWJ family, flag,
|
||||||
|
skin-tone) classifies as one emoji, while a short non-ascii text label (Sí/да/はい/確定/
|
||||||
|
café) is a label because its letters are not emoji codepoints. does not cap length:
|
||||||
|
a long emoji-only run is still an emoji, a run mixing letters and emoji is a label.
|
||||||
|
"""
|
||||||
|
if not value:
|
||||||
|
return False
|
||||||
|
# a keycap sequence is [0-9#*] + optional VS16 + U+20E3 - its base is an ascii digit/
|
||||||
|
# symbol (not So), so treat the whole run as an emoji when the keycap combiner is present
|
||||||
|
if value.endswith("⃣") and all(c in "0123456789#*️⃣" for c in value):
|
||||||
|
return True
|
||||||
|
has_symbol = False
|
||||||
|
for ch in value:
|
||||||
|
if not _is_emoji_codepoint(ch):
|
||||||
|
return False
|
||||||
|
if unicodedata.category(ch) == "So":
|
||||||
|
has_symbol = True
|
||||||
|
return has_symbol
|
||||||
|
|||||||
Reference in New Issue
Block a user