Experimental Discord bot written in Python
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  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. import discord
  10. from discord import Guild, Permissions
  11. from discord.ext.commands import Cog
  12. def dump_stacktrace(e: BaseException) -> None:
  13. print(e, file=sys.stderr)
  14. traceback.print_exception(type(e), e, e.__traceback__)
  15. def timedelta_from_str(s: str) -> timedelta:
  16. """
  17. Parses a timespan.
  18. Format examples:
  19. "30m"
  20. "10s"
  21. "90d"
  22. "1h30m"
  23. "73d18h22m52s"
  24. Parameters
  25. ----------
  26. s : str
  27. string to parse
  28. Returns
  29. -------
  30. timedelta
  31. Raises
  32. ------
  33. ValueError
  34. if parsing fails
  35. """
  36. p: re.Pattern = re.compile('^(?:[0-9]+[a-zA-Z])+$')
  37. if p.match(s) is None:
  38. raise ValueError(f'Illegal timespan value "{s}". Examples: 30s, 5m, 1h30m, 30d')
  39. p = re.compile('([0-9]+)([dhms])')
  40. days: int = 0
  41. hours: int = 0
  42. minutes: int = 0
  43. seconds: int = 0
  44. for m in p.finditer(s):
  45. scalar = int(m.group(1))
  46. unit = m.group(2).lower()
  47. if unit == 'd':
  48. days = scalar
  49. elif unit == 'h':
  50. hours = scalar
  51. elif unit == 'm':
  52. minutes = scalar
  53. elif unit == 's':
  54. seconds = scalar
  55. else:
  56. raise ValueError(f'Invalid unit "{unit}". Valid units: "s"=seconds, "m"=minutes, "h"=hours, "d"=days')
  57. return timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
  58. def str_from_timedelta(td: timedelta) -> str:
  59. """
  60. Encodes a timedelta as a str. E.g. "3d2h"
  61. """
  62. d: int = td.days
  63. h: int = td.seconds // 3600
  64. m: int = (td.seconds // 60) % 60
  65. s: int = td.seconds % 60
  66. components: list[str] = []
  67. if d != 0:
  68. components.append(f'{d}d')
  69. if h != 0:
  70. components.append(f'{h}h')
  71. if m != 0:
  72. components.append(f'{m}m')
  73. if s != 0 or len(components) == 0:
  74. components.append(f'{s}s')
  75. return ''.join(components)
  76. def describe_timedelta(td: timedelta, max_components: int = 2) -> str:
  77. """
  78. Formats a human-readable description of a time span. E.g. "3 days 2 hours".
  79. """
  80. d: int = td.days
  81. h: int = td.seconds // 3600
  82. m: int = (td.seconds // 60) % 60
  83. s: int = td.seconds % 60
  84. components: list[str] = []
  85. if d != 0:
  86. components.append('1 day' if d == 1 else f'{d} days')
  87. if h != 0:
  88. components.append('1 hour' if h == 1 else f'{h} hours')
  89. if m != 0:
  90. components.append('1 minute' if m == 1 else f'{m} minutes')
  91. if s != 0 or len(components) == 0:
  92. components.append('1 second' if s == 1 else f'{s} seconds')
  93. if len(components) > max_components:
  94. components = components[0:max_components]
  95. return ' '.join(components)
  96. def _old_first_command_group(cog: Cog) -> Optional[discord.ext.commands.Group]:
  97. """Returns the first command Group found in a cog."""
  98. for member_name in dir(cog):
  99. member = getattr(cog, member_name)
  100. if isinstance(member, discord.ext.commands.Group):
  101. return member
  102. return None
  103. def first_command_group(cog: Cog) -> Optional[discord.app_commands.Group]:
  104. """Returns the first slash command Group found in a cog."""
  105. for member_name in dir(cog):
  106. member = getattr(cog, member_name)
  107. if isinstance(member, discord.app_commands.Group):
  108. return member
  109. return None
  110. def bot_log(guild: Optional[Guild], cog_class: Optional[Type], message: Any) -> None:
  111. """Logs a message to stdout with time, cog, and guild info."""
  112. now: datetime = datetime.now() # local
  113. s = f'[{now.strftime("%Y-%m-%dT%H:%M:%S")}|'
  114. s += f'{cog_class.__name__}|' if cog_class else '-|'
  115. s += f'{guild.name}] ' if guild else '-] '
  116. s += str(message)
  117. print(s)
  118. __QUOTE_CHARS: str = '\'"'
  119. __ID_REGEX: re.Pattern = re.compile('^[0-9]{17,20}$')
  120. __MENTION_REGEX: re.Pattern = re.compile('^<@[!&]([0-9]{17,20})>$')
  121. __USER_MENTION_REGEX: re.Pattern = re.compile('^<@!([0-9]{17,20})>$')
  122. __ROLE_MENTION_REGEX: re.Pattern = re.compile('^<@&([0-9]{17,20})>$')
  123. def is_user_id(val: str) -> bool:
  124. """Tests if a string is in user/role ID format."""
  125. return __ID_REGEX.match(val) is not None
  126. def is_mention(val: str) -> bool:
  127. """Tests if a string is a user or role mention."""
  128. return __MENTION_REGEX.match(val) is not None
  129. def is_role_mention(val: str) -> bool:
  130. """Tests if a string is a role mention."""
  131. return __ROLE_MENTION_REGEX.match(val) is not None
  132. def is_user_mention(val: str) -> bool:
  133. """Tests if a string is a user mention."""
  134. return __USER_MENTION_REGEX.match(val) is not None
  135. def user_id_from_mention(mention: str) -> str:
  136. """Extracts the user ID from a mention. Raises a ValueError if malformed."""
  137. m = __USER_MENTION_REGEX.match(mention)
  138. if m:
  139. return m.group(1)
  140. raise ValueError(f'"{mention}" is not an @ user mention')
  141. def mention_from_user_id(user_id: Union[str, int]) -> str:
  142. """Returns a Markdown user mention from a user id."""
  143. return f'<@!{user_id}>'
  144. def mention_from_role_id(role_id: Union[str, int]) -> str:
  145. """Returns a Markdown role mention from a role id."""
  146. return f'<@&{role_id}>'
  147. def str_from_quoted_str(val: str) -> str:
  148. """Removes the leading and trailing quotes from a string."""
  149. if len(val) < 2 or val[0:1] not in __QUOTE_CHARS or val[-1:] not in __QUOTE_CHARS:
  150. raise ValueError(f'Not a quoted string: {val}')
  151. return val[1:-1]
  152. MOD_PERMISSIONS: Permissions = Permissions(Permissions.manage_messages.flag)
  153. from discord import Interaction
  154. from discord.app_commands import Transformer
  155. from discord.ext.commands import BadArgument
  156. class TimeDeltaTransformer(Transformer):
  157. async def transform(self, interaction: Interaction, value: Any) -> timedelta:
  158. try:
  159. return timedelta_from_str(str(value))
  160. except ValueError as e:
  161. print("Invalid time delta:", e)
  162. raise BadArgument(str(e))
  163. @property
  164. def _error_display_name(self) -> str:
  165. return 'timedelta'