Skip to content

Commit 3db2ff4

Browse files
committed
Add utility functions and command converters for working with unicode emojis and emoji shortcodes
1 parent 7b01d67 commit 3db2ff4

File tree

3 files changed

+166
-28
lines changed

3 files changed

+166
-28
lines changed

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ discord.py~=2.1
33
python-dateutil~=2.8
44
python-dotenv~=0.19
55
typing-extensions~=4.4
6+
emoji~=2.2

snakecore/commands/converters.py

+124-24
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,9 @@ class DateTimeConverter(commands.Converter[datetime.datetime]):
104104
105105
Examples
106106
--------
107-
- `<t:{6969...}[:t|T|d|D|f|F|R]> -> datetime(seconds=6969...)`
108-
- `YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]] -> datetime`
109-
- `November 18th, 2069 12:30:30.55 am; -3 -> datetime.datetime(2029, 11, 18, 0, 30, 30, 550000, tzinfo=tzoffset(None, 10800))
107+
- `<t:{6969...}[:t|T|d|D|f|F|R]>` -> `datetime(seconds=6969...)`
108+
- `YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]]` -> `datetime`
109+
- `November 18th, 2069 12:30:30.55 am; -3` -> `datetime.datetime(2029, 11, 18, 0, 30, 30, 550000, tzinfo=tzoffset(None, 10800))`
110110
"""
111111

112112
async def convert(
@@ -142,9 +142,9 @@ class TimeConverter(commands.Converter[datetime.time]):
142142
143143
Examples
144144
--------
145-
- `<t:{6969...}[:t|T|d|D|f|F|R]> -> time`
146-
- `HH[:MM[:SS]][+HH:MM[:SS]] -> time`
147-
- `12:30:30 am; -3 -> datetime.time(0, 30, 30, 550000, tzinfo=tzoffset(None, -10800))
145+
- `<t:{6969...}[:t|T|d|D|f|F|R]>` -> `time`
146+
- `HH[:MM[:SS]][+HH:MM[:SS]]` -> `time`
147+
- `12:30:30 am; -3` -> `datetime.time(0, 30, 30, 550000, tzinfo=tzoffset(None, -10800))`
148148
"""
149149

150150
async def convert(
@@ -232,10 +232,10 @@ class TimeDeltaConverter(commands.Converter[datetime.timedelta]):
232232
233233
Examples
234234
--------
235-
- `<t:{6969...}[:t|T|d|D|f|F|R]> -> datetime(second=6969...) - datetime.now(timezone.utc)`
236-
- `HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]] -> time`
237-
- `300d[ay[s]] 40m[in[ute[s]|s]] -> timedelta(days=30, minutes=40)``
238-
- `6:30:05 -> timedelta(hours=6, minutes=30, seconds=5)`
235+
- `<t:{6969...}[:t|T|d|D|f|F|R]>` -> `datetime(second=6969...) - datetime.now(timezone.utc)`
236+
- `HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]` -> `time`
237+
- `300d[ay[s]] 40m[in[ute[s]|s]]` -> `timedelta(days=30, minutes=40)``
238+
- `6:30:05` -> `timedelta(hours=6, minutes=30, seconds=5)`
239239
"""
240240

241241
async def convert(
@@ -296,13 +296,13 @@ class ClosedRangeConverter(commands.Converter[range]):
296296
297297
Examples
298298
--------
299-
- `start-stop -> range(start, stop+1)`
300-
- `start-stop|[+]step -> range(start, stop+1, +step)` *
301-
- `start-stop|-step -> range(start, stop+1, -step)` *
299+
- `start-stop` -> `range(start, stop+1)`
300+
- `start-stop|[+]step` -> `range(start, stop+1, +step)` *
301+
- `start-stop|-step` -> `range(start, stop+1, -step)` *
302302
303-
- `start>|>=|≥x>|>=|≥stop -> range(start[+1], stop[+1])`
304-
- `start>|>=|≥x>|>=|≥stop|[+]step -> range(start[+1], stop[+1], +step)` *
305-
- `start>|>=|≥x>|>=|≥stop|-step -> range(start[+1], stop[+1], -step)` *
303+
- `start>|>=|≥x>|>=|≥stop` -> `range(start[+1], stop[+1])`
304+
- `start>|>=|≥x>|>=|≥stop|[+]step` -> `range(start[+1], stop[+1], +step)` *
305+
- `start>|>=|≥x>|>=|≥stop|-step` -> `range(start[+1], stop[+1], -step)` *
306306
307307
*The last '|' is considered as part of the syntax.
308308
"""
@@ -621,8 +621,8 @@ class StringConverter(_StringConverter, Generic[_T]):
621621
622622
Examples
623623
--------
624-
- `"'abc'" -> 'abc'`
625-
- `'"ab\\"c"' -> 'ab"c'`
624+
- `"'abc'"` -> `'abc'`
625+
- `'"ab\\"c"'` -> `'ab"c'`
626626
"""
627627

628628
def __init__(self, size: Any = None) -> None:
@@ -847,8 +847,8 @@ class ParensConverter(commands.Converter[tuple]):
847847
848848
Examples
849849
--------
850-
- `"( 1 2 4 5.5 )" -> (1, 2, 4, 5.5)`
851-
- `'( 1 ( 4 ) () ( ( 6 ( "a" ) ) ) 0 )' -> (1, (4,), (), ((6,("a",),),), 0)`
850+
- `"( 1 2 4 5.5 )"` -> `(1, 2, 4, 5.5)`
851+
- `'( 1 ( 4 ) () ( ( 6 ( "a" ) ) ) 0 )'` -> `(1, (4,), (), ((6,("a",),),), 0)`
852852
"""
853853

854854
OPENING = "("
@@ -1168,13 +1168,113 @@ def _repr_converter(obj):
11681168
return repr(obj)
11691169

11701170

1171+
class UnicodeEmojiConverter(commands.Converter[str]):
1172+
"""A converter that converts emoji shortcodes or unicode
1173+
character escapes into valid unicode emojis. Already valid
1174+
inputs are ignored.
1175+
1176+
Syntax
1177+
------
1178+
- `":eggplant:"` -> `"🍆"`
1179+
- `"\\u270c\\u1f3fd"` -> `"✌🏽"`
1180+
"""
1181+
1182+
async def convert(self, ctx: commands.Context[BotT], argument: str) -> str:
1183+
argument = StringConverter.escape(argument)
1184+
1185+
if snakecore.utils.is_emoji_shortcode(argument):
1186+
return snakecore.utils.shortcode_to_unicode_emoji(argument)
1187+
1188+
elif snakecore.utils.is_unicode_emoji(argument):
1189+
return argument
1190+
1191+
raise commands.BadArgument(
1192+
"argument must be a valid unicode emoji or emoji shortcode"
1193+
)
1194+
1195+
1196+
UnicodeEmoji = Annotated[str, UnicodeEmojiConverter]
1197+
"""A converter that converts emoji shortcodes or unicode
1198+
character escapes into valid unicode emojis. Already valid
1199+
inputs are ignored.
1200+
1201+
Syntax
1202+
------
1203+
- `":eggplant:"` -> `"🍆"`
1204+
- `"\\u270c\\u1f3fd"` -> `"✌🏽"`
1205+
"""
11711206
DateTime = Annotated[datetime.datetime, DateTimeConverter]
1207+
"""A converter that parses timestamps to `datetime` objects.
1208+
1209+
Examples
1210+
--------
1211+
- `<t:{6969...}[:t|T|d|D|f|F|R]>` -> `datetime(seconds=6969...)`
1212+
- `YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]]` -> `datetime`
1213+
- `November 18th, 2069 12:30:30.55 am; -3` -> `datetime.datetime(2029, 11, 18, 0, 30, 30, 550000, tzinfo=tzoffset(None, 10800))`
1214+
"""
11721215
Time = Annotated[datetime.time, TimeConverter]
1216+
"""A converter that parses time to `time` objects.
1217+
1218+
Examples
1219+
--------
1220+
- `<t:{6969...}[:t|T|d|D|f|F|R]>` -> `time`
1221+
- `HH[:MM[:SS]][+HH:MM[:SS]]` -> `time`
1222+
- `12:30:30 am; -3` -> `datetime.time(0, 30, 30, 550000, tzinfo=tzoffset(None, -10800))`
1223+
"""
11731224
TimeDelta = Annotated[datetime.timedelta, TimeDeltaConverter]
1225+
"""A converter that parses time intervals to `timedelta` objects.
1226+
1227+
Examples
1228+
--------
1229+
- `<t:{6969...}[:t|T|d|D|f|F|R]>` -> `datetime(second=6969...) - datetime.now(timezone.utc)`
1230+
- `HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]` -> `time`
1231+
- `300d[ay[s]] 40m[in[ute[s]|s]]` -> `timedelta(days=30, minutes=40)``
1232+
- `6:30:05` -> `timedelta(hours=6, minutes=30, seconds=5)`
1233+
"""
11741234
ClosedRange = Annotated[range, ClosedRangeConverter]
1235+
"""A converter that parses closed integer ranges to Python `range` objects.
1236+
Both a hyphen-based notation is supported (as used in English phrases) which always
1237+
includes endpoints, as well as a mathematical notation using comparison operators.
1238+
1239+
Examples
1240+
--------
1241+
- `start-stop` -> `range(start, stop+1)`
1242+
- `start-stop|[+]step` -> `range(start, stop+1, +step)` *
1243+
- `start-stop|-step` -> `range(start, stop+1, -step)` *
1244+
1245+
- `start>|>=|≥x>|>=|≥stop` -> `range(start[+1], stop[+1])`
1246+
- `start>|>=|≥x>|>=|≥stop|[+]step` -> `range(start[+1], stop[+1], +step)` *
1247+
- `start>|>=|≥x>|>=|≥stop|-step` -> `range(start[+1], stop[+1], -step)` *
1248+
1249+
*The last '|' is considered as part of the syntax.
1250+
"""
11751251

11761252
if TYPE_CHECKING: # type checker deception
11771253
Parens = tuple
1254+
"""A special converter that establishes its own scope of arguments
1255+
and parses argument tuples.
1256+
1257+
The recognized arguments are converted into their desired formats
1258+
using the converters given to it as input, which are then converted
1259+
into a tuple of arguments. This can be used to implement parsing
1260+
of argument tuples. Nesting is also supported, as well as variadic
1261+
parsing of argument tuples. The syntax is similar to type annotations
1262+
using the `tuple` type (tuple[int, ...] = Parens[int, ...], etc.).
1263+
1264+
Arguments for this converter must be surrounded by whitespace, followed
1265+
by round parentheses on both sides (`'( ... ... ... )'`).
1266+
1267+
This converter does not parse successfully if specified inside a tuple
1268+
annotation of `discord.ext.commands.flags.Flag`, inside a subclass of
1269+
`discord.ext.commands`'s default `FlagConverter`. To migitate this, it
1270+
is recommended to subclass `snakecore.commands.converters.FlagConverter`
1271+
instead.
1272+
1273+
Examples
1274+
--------
1275+
- `"( 1 2 4 5.5 )"` -> `(1, 2, 4, 5.5)`
1276+
- `'( 1 ( 4 ) () ( ( 6 ( "a" ) ) ) 0 )'` -> `(1, (4,), (), ((6,("a",),),), 0)`
1277+
"""
11781278

11791279
class String(str): # type: ignore
11801280
"""A converter that parses string literals to string objects,
@@ -1192,8 +1292,8 @@ class String(str): # type: ignore
11921292
11931293
Examples
11941294
--------
1195-
- `"'abc'" -> 'abc'`
1196-
- `'"ab\\"c"' -> 'ab"c'`
1295+
- `"'abc'"` -> `'abc'`
1296+
- `'"ab\\"c"'` -> `'ab"c'`
11971297
"""
11981298

11991299
def __class_getitem__(cls, size: Union[StringParams, StringParamsTuple]):
@@ -1208,8 +1308,8 @@ class StringExpr(str): # type: ignore
12081308
12091309
Examples
12101310
--------
1211-
- `"'abc'" -> 'abc'`
1212-
- `'"ab\\"c"' -> 'ab"c'`
1311+
- `"'abc'"` -> `'abc'`
1312+
- `'"ab\\"c"'` -> `'ab"c'`
12131313
"""
12141314

12151315
def __class_getitem__(cls, regex_and_examples: Union[str, tuple[str, ...]]):

snakecore/utils/utils.py

+41-4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
)
2626

2727
import discord
28+
import emoji
2829

2930
from snakecore.constants import UNSET, _UnsetType
3031
from . import regex_patterns
@@ -359,9 +360,8 @@ def is_markdown_custom_emoji(string: str) -> bool:
359360

360361

361362
def is_emoji_shortcode(string: str) -> bool:
362-
"""Whether the given string matches the structure of an emoji shortcode,
363-
which is ':{unicode_characters}:'. No whitespace is allowed.
364-
Does not validate for the existence of the emoji shortcodes on Discord.
363+
"""Whether the given string is a valid unicode emoji shortcode or alias shortcode.
364+
This function uses the `emoji` package for validation.
365365
366366
Parameters
367367
----------
@@ -373,8 +373,45 @@ def is_emoji_shortcode(string: str) -> bool:
373373
bool
374374
`True` if condition is met, `False` otherwise.
375375
"""
376-
return bool(re.match(regex_patterns.EMOJI_SHORTCODE, string))
376+
return (
377+
bool(re.match(regex_patterns.EMOJI_SHORTCODE, string))
378+
and emoji.emojize(string) != string
379+
)
380+
381+
def is_unicode_emoji(string: str) -> bool:
382+
"""Whether the given string matches a valid unicode emoji.
383+
This function uses the `emoji` package for validation.
384+
385+
Parameters
386+
----------
387+
string : str
388+
The string to check for.
389+
390+
Returns
391+
-------
392+
bool
393+
`True` if condition is met, `False` otherwise.
394+
"""
395+
return emoji.is_emoji(string)
396+
397+
def shortcode_to_unicode_emoji(string: str) -> str:
398+
"""Convert the given emoji shortcode to a valid unicode emoji,
399+
if possible. This function uses the `emoji` package for shortcode parsing.
400+
401+
Parameters
402+
----------
403+
string : str
404+
The emoji shortcode.
405+
406+
Returns
407+
-------
408+
str
409+
The unicode emoji.
410+
"""
411+
if is_emoji_shortcode(string):
412+
return emoji.emojize(string, language="alias")
377413

414+
return string
378415

379416
def extract_markdown_timestamp(markdown_timestamp: str) -> int:
380417
"""Extract the UNIX timestamp '123456789696969' from a Discord markdown

0 commit comments

Comments
 (0)