Experimental Discord bot written in Python
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

basecog.py 4.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. from discord import Guild, Message, PartialEmoji, RawReactionActionEvent, TextChannel
  2. from discord.ext import commands
  3. from datetime import timedelta
  4. from rscollections import AgeBoundDict
  5. from storage import ConfigKey, Storage
  6. import json
  7. class BaseCog(commands.Cog):
  8. def __init__(self, bot):
  9. self.bot = bot
  10. self.listened_mod_react_message_ids = AgeBoundDict(timedelta(minutes=5), lambda message_id, tpl : tpl[0])
  11. @classmethod
  12. def get_cog_default(cls, key: str):
  13. """
  14. Convenience method for getting a cog configuration default from
  15. `CONFIG['cogs'][<cogname>][<key>]`.
  16. """
  17. cogs: dict = CONFIG['cog_defaults']
  18. cog = cogs.get(cls.__name__)
  19. if cog is None:
  20. return None
  21. return cog.get(key)
  22. def listen_for_reactions_to(self, message: Message, context = None) -> None:
  23. """
  24. Registers a warning message as something a mod may react to to enact
  25. some action. `context` will be passed back in `on_mod_react` and can be
  26. any value that helps give the cog context about the action being
  27. performed.
  28. """
  29. self.listened_mod_react_message_ids[message.id] = (message.created_at, context)
  30. @commands.Cog.listener()
  31. async def on_raw_reaction_add(self, payload: RawReactionActionEvent):
  32. 'Event handler'
  33. if payload.user_id == self.bot.user.id:
  34. # Ignore bot's own reactions
  35. return
  36. member: Member = payload.member
  37. if member is None:
  38. return
  39. guild: Guild = self.bot.get_guild(payload.guild_id)
  40. if guild is None:
  41. # Possibly a DM
  42. return
  43. channel: GuildChannel = guild.get_channel(payload.channel_id)
  44. if channel is None:
  45. # Possibly a DM
  46. return
  47. message: Message = await channel.fetch_message(payload.message_id)
  48. if message is None:
  49. # Message deleted?
  50. return
  51. if message.author.id != self.bot.user.id:
  52. # Bot didn't author this
  53. return
  54. if not member.permissions_in(channel).ban_members:
  55. # Not a mod
  56. return
  57. tpl = self.listened_mod_react_message_ids.get(message.id)
  58. if tpl is None:
  59. # Not a message we're listening for
  60. return
  61. context = tpl[1]
  62. await self.on_mod_react(message, payload.emoji, context)
  63. async def on_mod_react(self, message: Message, emoji: PartialEmoji, context) -> None:
  64. """
  65. Override point for getting a mod's emote on a bot message. Used to take
  66. action on a warning, such as banning an offending user. This event is
  67. only triggered for registered bot messages and reactions by members
  68. with the proper permissions. The given `context` value is whatever was
  69. passed in `listen_to_reactions_to()`.
  70. """
  71. pass
  72. async def validate_param(self, context: commands.Context, param_name: str, value,
  73. allowed_types: tuple = None,
  74. min_value = None,
  75. max_value = None) -> bool:
  76. """
  77. Convenience method for validating a command parameter is of the expected
  78. type and in the expected range. Bad values will cause a reply to be sent
  79. to the original message and a False will be returned. If all checks
  80. succeed, True will be returned.
  81. """
  82. if allowed_types is not None and not isinstance(value, allowed_types):
  83. if len(allowed_types) == 1:
  84. await context.message.reply(f'⚠️ `{param_name}` must be of type ' +
  85. f'{allowed_types[0]}.', mention_author=False)
  86. else:
  87. await context.message.reply(f'⚠️ `{param_name}` must be of types ' +
  88. f'{allowed_types}.', mention_author=False)
  89. return False
  90. if min_value is not None and value < min_value:
  91. await context.message.reply(f'⚠️ `{param_name}` must be >= {min_value}.',
  92. mention_author=False)
  93. return False
  94. if max_value is not None and value > max_value:
  95. await context.message.reply(f'⚠️ `{param_name}` must be <= {max_value}.',
  96. mention_author=False)
  97. return True
  98. @classmethod
  99. async def warn(cls, guild: Guild, message: str) -> Message:
  100. """
  101. Sends a warning message to the configured warning channel for the
  102. given guild. If no warning channel is configured no action is taken.
  103. Returns the Message if successful or None if not.
  104. """
  105. channel_id = Storage.get_config_value(guild, ConfigKey.WARNING_CHANNEL_ID)
  106. if channel_id is None:
  107. cls.guild_trace(guild, 'No warning channel set! No warning issued.')
  108. return None
  109. channel: TextChannel = guild.get_channel(channel_id)
  110. if channel is None:
  111. cls.guild_trace(guild, 'Configured warning channel does not exist!')
  112. return None
  113. mention: str = Storage.get_config_value(guild, ConfigKey.WARNING_MENTION)
  114. text: str = message
  115. if mention is not None:
  116. text = f'{mention} {text}'
  117. msg: Message = await channel.send(text)
  118. return msg
  119. @classmethod
  120. async def update_warn(cls, warn_message: Message, new_text: str) -> None:
  121. """
  122. Updates the text of a previously posted `warn`. Includes configured
  123. mentions if necessary.
  124. """
  125. text: str = new_text
  126. mention: str = Storage.get_config_value(
  127. warn_message.guild,
  128. ConfigKey.WARNING_MENTION)
  129. if mention is not None:
  130. text = f'{mention} {text}'
  131. await warn_message.edit(content=text)
  132. @classmethod
  133. def guild_trace(cls, guild: Guild, message: str) -> None:
  134. print(f'[guild {guild.id}|{guild.name}] {message}')