ソースを参照

Add Caps Lock moderator

pull/1/head
コミット
c1691c0ab8
7個のファイルの変更254行の追加45行の削除
  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 ファイルの表示

@@ -69,6 +69,33 @@ Below is the complete configuration reference:
"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 ファイルの表示

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

from .moderator import Moderator, CapsLockModerator, ModerationDecision


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

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.channel = channel
@@ -107,6 +111,7 @@ class Config:
self.command_prefix = command_prefix
self.commands = commands
self.timer = timer
self.moderators = moderators

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

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
if params.get('help', True):
for command in commands:
@@ -144,7 +169,8 @@ class Config:
token,
commands_prefix,
commands,
timer
timer,
moderators
)

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


+ 73
- 0
_twitchbot/moderator.py ファイルの表示

@@ -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 ファイルの表示

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

import irc3

from . import utils

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

from random import shuffle
from datetime import datetime, timedelta

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

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

@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)

return in_str

@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])
tags_dict = utils.parse_tags(tags)

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.play_timer()
@@ -86,6 +90,67 @@ class TwitchBot:
self.nb_messages_since_timer = 0
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)
def on_join(self, mask, channel, **_):
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 ファイルの表示

@@ -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 ファイルの表示

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


+ 34
- 25
config.json.dist ファイルの表示

@@ -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}!!!"
}
}
}

読み込み中…
キャンセル
保存