The KISS Twitch bot
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

217 lines
6.8 KiB

  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 json
  17. from os import environ
  18. from enum import Enum
  19. from typing import Union
  20. from . import moderator
  21. class Command:
  22. name: str
  23. message: str
  24. aliases: [str]
  25. disabled: bool
  26. def __init__(self, name: str, message: str, aliases: [str] = None, disabled: bool = False):
  27. self.name = name
  28. self.message = message
  29. self.aliases = aliases if aliases is not None else []
  30. self.disabled = disabled
  31. @classmethod
  32. def from_dict(cls, params: dict):
  33. return Command(
  34. params.get('name'),
  35. params['message'],
  36. params.get('aliases', []),
  37. params.get('disabled', False)
  38. )
  39. class TimerStrategy(Enum):
  40. ROUND_ROBIN = "round-robin"
  41. SHUFFLE = "shuffle"
  42. class Timer:
  43. time_between: int
  44. msgs_between: int
  45. strategy: TimerStrategy
  46. pool: [Command]
  47. def __init__(
  48. self,
  49. time_between: int = 10,
  50. msgs_between: int = 10,
  51. strategy: TimerStrategy = TimerStrategy.ROUND_ROBIN,
  52. pool: [Command] = None
  53. ):
  54. self.time_between = time_between
  55. self.msgs_between = msgs_between
  56. self.strategy = strategy
  57. self.pool = pool if pool else []
  58. @classmethod
  59. def from_dict(cls, param: dict):
  60. pool = []
  61. for c in param.get('pool', []):
  62. command = Command.from_dict(c)
  63. if not command.disabled:
  64. pool.append(command)
  65. return Timer(
  66. time_between=param.get('between', {}).get('time', 10),
  67. msgs_between=param.get('between', {}).get('messages', 10),
  68. strategy=TimerStrategy(param.get('strategy', 'round-robin')),
  69. pool=pool
  70. )
  71. class Config:
  72. channel: str
  73. nickname: str
  74. token: str
  75. command_prefix: str
  76. commands: [Command]
  77. timer: Timer
  78. moderators: [moderator.Moderator]
  79. def __init__(
  80. self,
  81. channel: str,
  82. nickname: str,
  83. token: str,
  84. command_prefix: str,
  85. commands: [Command],
  86. timer: Timer,
  87. moderators: [moderator.Moderator]
  88. ):
  89. self.nickname = nickname
  90. self.channel = channel
  91. self.token = token
  92. self.command_prefix = command_prefix
  93. self.commands = commands
  94. self.timer = timer
  95. self.moderators = moderators
  96. @classmethod
  97. def from_dict(cls, params: dict, token: str):
  98. timer = Timer.from_dict(params.get('timer', {}))
  99. commands_prefix = params.get('command_prefix', '!')
  100. commands = []
  101. help_command = Command("help", "Voici les commandes disponibles : ")
  102. for command in params.get('commands', []):
  103. command = Command.from_dict(command)
  104. if command.disabled:
  105. continue
  106. commands.append(command)
  107. for command in timer.pool:
  108. if command.name is None:
  109. continue
  110. commands.append(command)
  111. moderators = []
  112. for mod in params.get('moderator', []):
  113. moderator_config = params['moderator'][mod]
  114. if mod == 'caps-lock':
  115. moderators.append(moderator.CapsLockModerator(
  116. moderator_config.get("message", "{author}, stop the caps lock!"),
  117. cls.parse_decision(moderator_config.get("decision", "delete")),
  118. moderator_config.get("duration", None),
  119. moderator_config.get("min-size", 5),
  120. moderator_config.get("threshold", 50)
  121. ))
  122. if mod == 'flood':
  123. moderators.append(moderator.FloodModerator(
  124. moderator_config.get("message", "{author}, stop the flood!"),
  125. cls.parse_decision(moderator_config.get("decision", "timeout")),
  126. moderator_config.get("duration", None),
  127. moderator_config.get("max-word-length", None),
  128. moderator_config.get("raid-cooldown", None),
  129. moderator_config.get("ignore-hashtags", False),
  130. moderator_config.get("max-msg-occurrences", None),
  131. moderator_config.get("min-time-between-occurrence", None)
  132. ))
  133. if mod == 'links':
  134. moderators.append(moderator.LinksModerator(
  135. moderator_config.get(
  136. "message",
  137. "{author}, your message contained forbidden links, it had to be removed for safety."
  138. ),
  139. cls.parse_decision(moderator_config.get("decision", "delete")),
  140. moderator_config.get("duration", None),
  141. moderator_config.get("authorized", [])
  142. ))
  143. # Generate help command
  144. if params.get('help', True):
  145. for command in commands:
  146. help_command.message = "%s %s%s" % (help_command.message, commands_prefix, command.name)
  147. commands.append(help_command)
  148. return Config(
  149. params.get('channel'),
  150. params.get('nickname'),
  151. token,
  152. commands_prefix,
  153. commands,
  154. timer,
  155. moderators
  156. )
  157. @classmethod
  158. def parse_decision(cls, decision_str) -> moderator.ModerationDecision:
  159. if decision_str == "delete":
  160. decision = moderator.ModerationDecision.DELETE_MSG
  161. elif decision_str == "timeout":
  162. decision = moderator.ModerationDecision.TIMEOUT_USER
  163. else:
  164. print("WARNING: %s moderator's decision is invalid, it has been deactivated!")
  165. decision = moderator.ModerationDecision.ABSTAIN
  166. return decision
  167. def find_command(self, command: str) -> Union[None, Command]:
  168. if not command.startswith(self.command_prefix):
  169. return None
  170. command = command[1:]
  171. for c in self.commands:
  172. if c.name == command or command in c.aliases:
  173. return c
  174. return None
  175. def get_config(file_path: str):
  176. with open(file_path, 'r') as config_file:
  177. token = environ['TWITCH_TOKEN']
  178. return Config.from_dict(json.loads(config_file.read()), token)