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

3 年前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. # Twason - The KISS Twitch bot
  2. # Copyright (C) 2021 Jérôme Deuchnord
  3. #
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU Affero General Public License as
  6. # published by the Free Software Foundation, either version 3 of the
  7. # License, or (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU Affero General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU Affero General Public License
  15. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. import irc3
  17. from random import shuffle
  18. from datetime import datetime, timedelta
  19. from . import utils
  20. from .config import TimerStrategy
  21. from .moderator import ModerationDecision, Moderator, FloodModerator
  22. from . import twitch_message
  23. config = None
  24. @irc3.plugin
  25. class TwitchBot:
  26. def __init__(self, bot: irc3.IrcBot):
  27. self.config = config
  28. self.messages_stack = []
  29. self.bot = bot
  30. self.log = self.bot.log
  31. self.last_timer_date = datetime.now()
  32. self.nb_messages_since_timer = 0
  33. def connection_made(self):
  34. print('connected')
  35. def server_ready(self):
  36. print('ready')
  37. def connection_lost(self):
  38. print('connection lost')
  39. @staticmethod
  40. def _parse_variables(in_str: str, **kwargs):
  41. for key in kwargs:
  42. value = kwargs[key]
  43. in_str = in_str.replace('{%s}' % key, value)
  44. return in_str
  45. # @irc3.event(r'^(?P<data>.+)$')
  46. # def on_all(self, data):
  47. # print(data)
  48. @irc3.event(irc3.rfc.PRIVMSG)
  49. def on_msg(self, mask: str = None, target: str = None, data: str = None, tags: str = None, **_):
  50. author = mask.split('!')[0]
  51. command = self.config.find_command(data.lower().split(' ')[0])
  52. tags_dict = utils.parse_tags(tags)
  53. if command is not None:
  54. print('%s: %s%s' % (author, self.config.command_prefix, command.name))
  55. self.bot.privmsg(target, self._parse_variables(command.message, author=author))
  56. elif tags_dict.get('mod') == '0':
  57. self.moderate(tags_dict, data, author, target)
  58. self.nb_messages_since_timer += 1
  59. self.play_timer()
  60. @irc3.event(twitch_message.USERNOTICE)
  61. def on_user_notice(self, tags: str, **_):
  62. tags = utils.parse_tags(tags)
  63. if tags.get('msg-id', None) == 'raid':
  64. # Notice the Flood moderator a raid has just happened
  65. for moderator in self.config.moderators:
  66. if isinstance(moderator, FloodModerator) and moderator.raid_cooldown is not None:
  67. print("Raid received from %s. Disabling the Flood moderator." % tags.get('display-name'))
  68. moderator.declare_raid()
  69. break
  70. def play_timer(self):
  71. if not self.messages_stack:
  72. print('Filling the timer messages stack in')
  73. self.messages_stack = self.config.timer.pool.copy()
  74. if self.config.timer.strategy == TimerStrategy.SHUFFLE:
  75. print('Shuffle!')
  76. shuffle(self.messages_stack)
  77. if self.nb_messages_since_timer < self.config.timer.msgs_between or \
  78. datetime.now() < self.last_timer_date + timedelta(minutes=self.config.timer.time_between):
  79. return
  80. command = self.messages_stack.pop(0)
  81. print("Timer: %s" % command.message)
  82. self.bot.privmsg('#%s' % self.config.channel, command.message)
  83. self.nb_messages_since_timer = 0
  84. self.last_timer_date = datetime.now()
  85. def moderate(self, tags: {str: str}, msg: str, author: str, channel: str):
  86. def delete_msg(mod: Moderator):
  87. print("[DELETE (reason: %s)] %s: %s" % (mod.get_name(), author, msg))
  88. self.bot.privmsg(
  89. channel,
  90. "/delete %s" % tags['id']
  91. )
  92. def timeout(mod: Moderator):
  93. print("[TIMEOUT (reason: %s)] %s: %s" % (mod.get_name(), author, msg))
  94. self.bot.privmsg(
  95. channel,
  96. "/timeout %s %d %s" % (
  97. author,
  98. mod.timeout_duration,
  99. self._parse_variables(mod.message, author=author)
  100. )
  101. )
  102. # Ignore emotes-only messages
  103. if tags.get('emote-only', '0') == '1':
  104. return
  105. message_to_moderate = msg
  106. # Remove emotes from message before moderating
  107. for emote in tags.get('emotes', '').split('/'):
  108. if emote == '':
  109. break
  110. for indices in emote.split(':')[1].split(','):
  111. [first, last] = indices.split('-')
  112. first, last = int(first), int(last)
  113. if first == 0:
  114. message_to_moderate = message_to_moderate[last + 1:]
  115. else:
  116. message_to_moderate = message_to_moderate[:first - 1] + message_to_moderate[last + 1:]
  117. for moderator in self.config.moderators:
  118. vote = moderator.vote(message_to_moderate, author)
  119. if vote == ModerationDecision.ABSTAIN:
  120. continue
  121. if vote == ModerationDecision.DELETE_MSG:
  122. delete_msg(moderator)
  123. if vote == ModerationDecision.TIMEOUT_USER:
  124. timeout(moderator)
  125. self.bot.privmsg(channel, self._parse_variables(moderator.message, author=author))
  126. break
  127. @irc3.event(irc3.rfc.JOIN)
  128. def on_join(self, mask, channel, **_):
  129. print('JOINED %s as %s' % (channel, mask))
  130. @irc3.event(irc3.rfc.CONNECTED)
  131. def on_connected(self, **_):
  132. for line in [
  133. "CAP REQ :twitch.tv/commands",
  134. "CAP REQ :twitch.tv/tags"
  135. ]:
  136. self.bot.send_line(line)
  137. self.bot.join('#%s' % self.config.channel)