Browse Source

Add Caps Lock moderator

pull/1/head
Jérôme Deuchnord 2 years ago
parent
commit
c1691c0ab8
7 changed files with 254 additions and 45 deletions
  1. +27
    -0
      README.md
  2. +34
    -8
      _twitchbot/config.py
  3. +73
    -0
      _twitchbot/moderator.py
  4. +76
    -11
      _twitchbot/twitchbot.py
  5. +9
    -0
      _twitchbot/utils.py
  6. +1
    -1
      bot.py
  7. +34
    -25
      config.json.dist

+ 27
- 0
README.md View File

@@ -69,6 +69,33 @@ Below is the complete configuration reference:
"message": "Hello World! HeyGuys" "message": "Hello World! HeyGuys"
} }
] ]
},
"moderator": {
// The configuration of the moderator (see bellow for more information)
} }
} }
``` ```

### The Moderator

Twason has features to help you moderate your chat automatically against most of the nuisance that streamers may face to.
Any moderation feature has to be activated in the configuration in the `moderator` section of the `config.json` file, and have the same options:

```json5
{
"moderation-feature": { // replace the name with the moderation feature name
"activate": false, // set this to true to activate the feature
"decision": "delete",
"message": "Calm down, {{author}}" // this message will be sent in the chat when a member becomes a pain in the ass
}
}
```

Some moderation features may include more options. In this case, they have to be included in the same way.

The available moderation features are the following:

- `caps-lock`: moderate the messages written in CAPS LOCK
Additional options:
- `min_size`: the minimum size of the message to moderate
- `threshold`: the percentage of capital letters that will trigger the moderation

+ 34
- 8
_twitchbot/config.py View File

@@ -21,6 +21,8 @@ from os import environ
from enum import Enum from enum import Enum
from typing import Union from typing import Union


from .moderator import Moderator, CapsLockModerator, ModerationDecision



class Command: class Command:
name: str name: str
@@ -91,15 +93,17 @@ class Config:
command_prefix: str command_prefix: str
commands: [Command] commands: [Command]
timer: Timer timer: Timer
moderators: [Moderator]


def __init__( def __init__(
self,
channel: str,
nickname: str,
token: str,
command_prefix: str,
commands: [Command],
timer: Timer
self,
channel: str,
nickname: str,
token: str,
command_prefix: str,
commands: [Command],
timer: Timer,
moderators: [Moderator]
): ):
self.nickname = nickname self.nickname = nickname
self.channel = channel self.channel = channel
@@ -107,6 +111,7 @@ class Config:
self.command_prefix = command_prefix self.command_prefix = command_prefix
self.commands = commands self.commands = commands
self.timer = timer self.timer = timer
self.moderators = moderators


@classmethod @classmethod
def from_dict(cls, params: dict, token: str): def from_dict(cls, params: dict, token: str):
@@ -131,6 +136,26 @@ class Config:


commands.append(command) commands.append(command)


moderators = []
for moderator in params.get('moderator', []):
decision_str = params['moderator']['caps-lock'].get("decision", "delete")
if decision_str == "delete":
decision = ModerationDecision.DELETE_MSG
elif decision_str == "timeout":
decision = ModerationDecision.TIMEOUT_USER
else:
print("WARNING: %s moderator's decision is invalid, it has been deactivated!")
decision = ModerationDecision.ABSTAIN

if moderator == 'caps-lock':
moderators.append(CapsLockModerator(
params['moderator']['caps-lock'].get("message", "{author}, stop the caps lock!"),
params['moderator']['caps-lock'].get("min-size", 5),
params['moderator']['caps-lock'].get("threshold", 50),
decision,
params['moderator']['caps-lock'].get("duration", None)
))

# Generate help command # Generate help command
if params.get('help', True): if params.get('help', True):
for command in commands: for command in commands:
@@ -144,7 +169,8 @@ class Config:
token, token,
commands_prefix, commands_prefix,
commands, commands,
timer
timer,
moderators
) )


def find_command(self, command: str) -> Union[None, Command]: def find_command(self, command: str) -> Union[None, Command]:


+ 73
- 0
_twitchbot/moderator.py View File

@@ -0,0 +1,73 @@
# Twason - The KISS Twitch bot
# Copyright (C) 2021 Jérôme Deuchnord
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.


from abc import ABC, abstractmethod
from enum import Enum
from typing import Union


class ModerationDecision(Enum):
ABSTAIN = -1
DELETE_MSG = 0
TIMEOUT_USER = 1


class Moderator(ABC):
message: str
decision: ModerationDecision

def __init__(self, message: str, timeout_duration: Union[None, int]):
self.message = message
self.timeout_duration = timeout_duration

@abstractmethod
def get_name(self) -> str:
pass

@abstractmethod
def vote(self, msg) -> ModerationDecision:
pass


class CapsLockModerator(Moderator):
def __init__(self, message: str, min_size: int, threshold: int, decision: ModerationDecision, timeout_duration: Union[None, int]):
super().__init__(message, timeout_duration)

self.min_size = min_size
self.threshold = threshold / 100
self.decision = decision

def get_name(self) -> str:
return 'Caps Lock'

def vote(self, msg: str) -> ModerationDecision:
msg = ''.join(filter(str.isalpha, msg))

if len(msg) < self.min_size:
return ModerationDecision.ABSTAIN

n = 0
for char in msg:
if char.strip() == '':
continue
if char == char.upper():
n += 1

if n / len(msg) >= self.threshold:
return self.decision

return ModerationDecision.ABSTAIN

+ 76
- 11
_twitchbot/twitchbot.py View File

@@ -17,7 +17,11 @@


import irc3 import irc3


from . import utils

from .config import TimerStrategy from .config import TimerStrategy
from .moderator import ModerationDecision, Moderator

from random import shuffle from random import shuffle
from datetime import datetime, timedelta from datetime import datetime, timedelta


@@ -26,7 +30,7 @@ config = None


@irc3.plugin @irc3.plugin
class TwitchBot: class TwitchBot:
def __init__(self, bot):
def __init__(self, bot: irc3.IrcBot):
self.config = config self.config = config
self.messages_stack = [] self.messages_stack = []
self.bot = bot self.bot = bot
@@ -44,24 +48,24 @@ class TwitchBot:
print('connection lost') print('connection lost')


@staticmethod @staticmethod
def _parse_variables(in_str: str, mask: str = None):
variables = {
'author': mask.split('!')[0]
}

for key in variables:
value = variables[key]
def _parse_variables(in_str: str, **kwargs):
for key in kwargs:
value = kwargs[key]
in_str = in_str.replace('{%s}' % key, value) in_str = in_str.replace('{%s}' % key, value)


return in_str return in_str


@irc3.event(irc3.rfc.PRIVMSG) @irc3.event(irc3.rfc.PRIVMSG)
def on_msg(self, target, mask, data, **_):
def on_msg(self, mask: str = None, target: str = None, data: str = None, tags: str = None, **_):
author = mask.split('!')[0]
command = self.config.find_command(data.lower().split(' ')[0]) command = self.config.find_command(data.lower().split(' ')[0])
tags_dict = utils.parse_tags(tags)


if command is not None: if command is not None:
print('%s: %s%s' % (mask, self.config.command_prefix, command.name))
self.bot.privmsg(target, self._parse_variables(command.message, mask))
print('%s: %s%s' % (author, self.config.command_prefix, command.name))
self.bot.privmsg(target, self._parse_variables(command.message, author=author))
elif tags_dict.get('mod') == '0':
self.moderate(tags_dict, data, author, target)


self.nb_messages_since_timer += 1 self.nb_messages_since_timer += 1
self.play_timer() self.play_timer()
@@ -86,6 +90,67 @@ class TwitchBot:
self.nb_messages_since_timer = 0 self.nb_messages_since_timer = 0
self.last_timer_date = datetime.now() self.last_timer_date = datetime.now()


def moderate(self, tags: {str: str}, msg: str, author: str, channel: str):
print(tags)
def delete_msg(mod: Moderator):
print("[DELETE (reason: %s)] %s: %s" % (mod.get_name(), author, msg))
self.bot.privmsg(
channel,
"/delete %s" % tags['id']
)

def timeout(mod: Moderator):
print("[TIMEOUT (reason: %s)] %s: %s" % (mod.get_name(), author, msg))
self.bot.privmsg(
channel,
"/timeout %s %d %s" % (
author,
mod.timeout_duration,
self._parse_variables(mod.message, author=author)
)
)

# Ignore emotes-only messages
if tags.get('emote-only', '0') == '1':
return

message_to_moderate = msg

# Remove emotes from message before moderating
for emote in tags.get('emotes', '').split('/'):
if emote == '':
break

for indices in emote.split(':')[1].split(','):
[first, last] = indices.split('-')
first, last = int(first), int(last)
if first == 0:
message_to_moderate = message_to_moderate[last + 1:]
else:
message_to_moderate = message_to_moderate[:first - 1] + message_to_moderate[last + 1:]

for moderator in self.config.moderators:
vote = moderator.vote(message_to_moderate)
if vote == ModerationDecision.ABSTAIN:
continue
if vote == ModerationDecision.DELETE_MSG:
delete_msg(moderator)
if vote == ModerationDecision.TIMEOUT_USER:
timeout(moderator)

self.bot.privmsg(channel, self._parse_variables(moderator.message, author=author))
break

@irc3.event(irc3.rfc.JOIN) @irc3.event(irc3.rfc.JOIN)
def on_join(self, mask, channel, **_): def on_join(self, mask, channel, **_):
print('JOINED %s as %s' % (channel, mask)) print('JOINED %s as %s' % (channel, mask))

@irc3.event(irc3.rfc.CONNECTED)
def on_connected(self, **_):
for line in [
"CAP REQ :twitch.tv/commands",
"CAP REQ :twitch.tv/tags"
]:
self.bot.send_line(line)

self.bot.join('#%s' % self.config.channel)

+ 9
- 0
_twitchbot/utils.py View File

@@ -0,0 +1,9 @@
#!/usr/bin/env python3


def parse_tags(tags_str: str) -> {str: str}:
tags_list = []
for tag in tags_str.split(";"):
tags_list.append((tag.split("=")))

return dict(tags_list)

+ 1
- 1
bot.py View File

@@ -35,7 +35,7 @@ def main() -> int:
bot = irc3.IrcBot.from_config({ bot = irc3.IrcBot.from_config({
'nick': twitchbot.config.nickname, 'nick': twitchbot.config.nickname,
'password': twitchbot.config.token, 'password': twitchbot.config.token,
'autojoins': [twitchbot.config.channel],
'autojoins': [],
'host': TWITCH_IRC_SERVER, 'host': TWITCH_IRC_SERVER,
'port': TWITCH_IRC_PORT, 'port': TWITCH_IRC_PORT,
'ssl': True, 'ssl': True,


+ 34
- 25
config.json.dist View File

@@ -1,27 +1,36 @@
{ {
"nickname": "yourbot",
"channel": "yourchannel",
"command_prefix": "!",
"help": true,
"commands": [
{
"name": "ping",
"aliases": ["pong"],
"message": "Pong @{author} Kappa"
}
],
"timer": {
"between": {
"time": 10,
"messages": 10
},
"strategy": "round-robin",
"pool": [
{
"name": "hello",
"aliases": ["hi"],
"message": "Hello World! HeyGuys"
}
]
}
"nickname": "yourbot",
"channel": "yourchannel",
"command_prefix": "!",
"help": true,
"commands": [
{
"name": "ping",
"aliases": ["pong"],
"message": "Pong @{author} Kappa"
}
],
"timer": {
"between": {
"time": 10,
"messages": 10
},
"strategy": "round-robin",
"pool": [
{
"name": "hello",
"aliases": ["hi"],
"message": "Hello World! HeyGuys"
}
]
},
"moderator": {
"caps-lock": {
"activate": true,
"min-size": 5,
"threshold": 50,
"decision": "delete",
"message": "DON'T SHOUT LIKE THAT, {author}!!!"
}
}
} }

Loading…
Cancel
Save