Experimental Discord bot written in Python
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

utils.py 4.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. """
  2. General utility functions.
  3. """
  4. import re
  5. import sys
  6. import traceback
  7. from datetime import datetime, timedelta
  8. from typing import Any, Optional, Type, Union
  9. from discord import Guild
  10. from discord.ext.commands import Cog, Group
  11. def dump_stacktrace(e: Exception) -> None:
  12. print(e, file=sys.stderr)
  13. traceback.print_exception(type(e), e, e.__traceback__)
  14. def timedelta_from_str(s: str) -> timedelta:
  15. """
  16. Parses a timespan. Format examples:
  17. "30m"
  18. "10s"
  19. "90d"
  20. "1h30m"
  21. "73d18h22m52s"
  22. """
  23. p: re.Pattern = re.compile('^(?:[0-9]+[dhms])+$')
  24. if p.match(s) is None:
  25. raise ValueError("Illegal timespan value '{s}'.")
  26. p = re.compile('([0-9]+)([dhms])')
  27. days: int = 0
  28. hours: int = 0
  29. minutes: int = 0
  30. seconds: int = 0
  31. for m in p.finditer(s):
  32. scalar = int(m.group(1))
  33. unit = m.group(2)
  34. if unit == 'd':
  35. days = scalar
  36. elif unit == 'h':
  37. hours = scalar
  38. elif unit == 'm':
  39. minutes = scalar
  40. elif unit == 's':
  41. seconds = scalar
  42. return timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
  43. def str_from_timedelta(td: timedelta) -> str:
  44. """
  45. Encodes a timedelta as a str. E.g. "3d2h"
  46. """
  47. d: int = td.days
  48. h: int = td.seconds // 3600
  49. m: int = (td.seconds // 60) % 60
  50. s: int = td.seconds % 60
  51. components: list[str] = []
  52. if d != 0:
  53. components.append(f'{d}d')
  54. if h != 0:
  55. components.append(f'{h}h')
  56. if m != 0:
  57. components.append(f'{m}m')
  58. if s != 0 or len(components) == 0:
  59. components.append(f'{s}s')
  60. return ''.join(components)
  61. def describe_timedelta(td: timedelta, max_components: int = 2) -> str:
  62. """
  63. Formats a human-readable description of a time span. E.g. "3 days 2 hours".
  64. """
  65. d: int = td.days
  66. h: int = td.seconds // 3600
  67. m: int = (td.seconds // 60) % 60
  68. s: int = td.seconds % 60
  69. components: list[str] = []
  70. if d != 0:
  71. components.append('1 day' if d == 1 else f'{d} days')
  72. if h != 0:
  73. components.append('1 hour' if h == 1 else f'{h} hours')
  74. if m != 0:
  75. components.append('1 minute' if m == 1 else f'{m} minutes')
  76. if s != 0 or len(components) == 0:
  77. components.append('1 second' if s == 1 else f'{s} seconds')
  78. if len(components) > max_components:
  79. components = components[0:max_components]
  80. return ' '.join(components)
  81. def first_command_group(cog: Cog) -> Optional[Group]:
  82. """Returns the first command Group found in a cog."""
  83. for member_name in dir(cog):
  84. member = getattr(cog, member_name)
  85. if isinstance(member, Group):
  86. return member
  87. return None
  88. def bot_log(guild: Optional[Guild], cog_class: Optional[Type], message: Any) -> None:
  89. """Logs a message to stdout with time, cog, and guild info."""
  90. now: datetime = datetime.now() # local
  91. s = f'[{now.strftime("%Y-%m-%dT%H:%M:%S")}|'
  92. s += f'{cog_class.__name__}|' if cog_class else '-|'
  93. s += f'{guild.name}] ' if guild else '-] '
  94. s += str(message)
  95. print(s)
  96. __QUOTE_CHARS: str = '\'"'
  97. __ID_REGEX: re.Pattern = re.compile('^[0-9]{17,20}$')
  98. __MENTION_REGEX: re.Pattern = re.compile('^<@[!&]([0-9]{17,20})>$')
  99. __USER_MENTION_REGEX: re.Pattern = re.compile('^<@!([0-9]{17,20})>$')
  100. __ROLE_MENTION_REGEX: re.Pattern = re.compile('^<@&([0-9]{17,20})>$')
  101. def is_user_id(val: str) -> bool:
  102. """Tests if a string is in user/role ID format."""
  103. return __ID_REGEX.match(val) is not None
  104. def is_mention(val: str) -> bool:
  105. """Tests if a string is a user or role mention."""
  106. return __MENTION_REGEX.match(val) is not None
  107. def is_role_mention(val: str) -> bool:
  108. """Tests if a string is a role mention."""
  109. return __ROLE_MENTION_REGEX.match(val) is not None
  110. def is_user_mention(val: str) -> bool:
  111. """Tests if a string is a user mention."""
  112. return __USER_MENTION_REGEX.match(val) is not None
  113. def user_id_from_mention(mention: str) -> str:
  114. """Extracts the user ID from a mention. Raises a ValueError if malformed."""
  115. m = __USER_MENTION_REGEX.match(mention)
  116. if m:
  117. return m.group(1)
  118. raise ValueError(f'"{mention}" is not an @ user mention')
  119. def mention_from_user_id(user_id: Union[str, int]) -> str:
  120. """Returns a Markdown user mention from a user id."""
  121. return f'<@!{user_id}>'
  122. def mention_from_role_id(role_id: Union[str, int]) -> str:
  123. """Returns a Markdown role mention from a role id."""
  124. return f'<@&{role_id}>'
  125. def str_from_quoted_str(val: str) -> str:
  126. """Removes the leading and trailing quotes from a string."""
  127. if len(val) < 2 or val[0:1] not in __QUOTE_CHARS or val[-1:] not in __QUOTE_CHARS:
  128. raise ValueError(f'Not a quoted string: {val}')
  129. return val[1:-1]