瀏覽代碼

ci: add CI (#7)

pull/8/head
Deuchnord 6 月之前
committed by GitHub
父節點
當前提交
dccb9acc14
沒有發現已知的金鑰在資料庫的簽署中 GPG Key ID: B5690EEEBB952194
共有 11 個文件被更改,包括 428 次插入158 次删除
  1. +26
    -0
      .github/dependabot.yml
  2. +19
    -0
      .github/workflows/black.yml
  3. +32
    -0
      .github/workflows/release.yml
  4. +17
    -0
      .github/workflows/semantic-pr.yml
  5. +151
    -25
      poetry.lock
  6. +4
    -3
      pyproject.toml
  7. +13
    -11
      twason/__main__.py
  8. +59
    -45
      twason/config.py
  9. +47
    -33
      twason/moderator.py
  10. +3
    -2
      twason/twitch_message.py
  11. +57
    -39
      twason/twitchbot.py

+ 26
- 0
.github/dependabot.yml 查看文件

@@ -0,0 +1,26 @@
version: 2

updates:
- package-ecosystem: pip
directory: "/"
target-branch: master
open-pull-requests-limit: 5
schedule:
interval: daily
reviewers:
- Deuchnord
commit-message:
prefix: chore
include: scope

- package-ecosystem: github-actions
directory: "/"
open-pull-requests-limit: 5
target-branch: master
schedule:
interval: weekly
reviewers:
- Deuchnord
commit-message:
prefix: ci
include: scope

+ 19
- 0
.github/workflows/black.yml 查看文件

@@ -0,0 +1,19 @@
name: Code style

on:
push:
branches: [master, features]
pull_request:
branches: [master, features]

jobs:
lint:
runs-on: ubuntu-latest

name: Code Style
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.x'
- uses: psf/black@stable

+ 32
- 0
.github/workflows/release.yml 查看文件

@@ -0,0 +1,32 @@
name: Release Application

on:
release:
types: [published]

jobs:
pipy:
name: Build and Release to PyPI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'

- name: Prepare environment
run: |
python -m pip install --upgrade pip poetry

- name: Build package
run: |
poetry install
make i18n build

- name: Publish to PyPI
env:
POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_PASSWORD }}
run: |
poetry publish

+ 17
- 0
.github/workflows/semantic-pr.yml 查看文件

@@ -0,0 +1,17 @@
name: "Semantic Pull Request"

on:
pull_request:
types:
- opened
- edited
- synchronize

jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5.4.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 151
- 25
poetry.lock 查看文件

@@ -1,53 +1,179 @@
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.

[[package]]
name = "black"
version = "25.1.0"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"},
{file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"},
{file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"},
{file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"},
{file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"},
{file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"},
{file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"},
{file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"},
{file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"},
{file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"},
{file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"},
{file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"},
{file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"},
{file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"},
{file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"},
{file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"},
{file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"},
{file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"},
{file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"},
{file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"},
{file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"},
{file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"},
]

[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"

[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.10)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]

[[package]]
name = "click"
version = "8.2.0"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "click-8.2.0-py3-none-any.whl", hash = "sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c"},
{file = "click-8.2.0.tar.gz", hash = "sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d"},
]

[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}

[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["dev"]
markers = "platform_system == \"Windows\""
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]

[[package]]
name = "docopt"
version = "0.6.2"
description = "Pythonic argument parser, that will make you smile"
category = "main"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"},
]

[[package]]
name = "irc3"
version = "1.1.7"
version = "1.1.10"
description = "plugable irc client library based on asyncio with DCC and SASL support"
category = "main"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "irc3-1.1.10-py3-none-any.whl", hash = "sha256:603782d669422804674442cfc097e5c8f1c9d82bee8dac67e28669220af4a601"},
{file = "irc3-1.1.10.tar.gz", hash = "sha256:fe3afa945d4328919aa3f00ccba56644162681a4cd6dcdf9a4538b7f30258e38"},
]

[package.dependencies]
docopt = "*"
venusian = ">=3.0"

[package.extras]
test = ["pytest-asyncio", "pytest-aiohttp", "feedparser", "requests", "pysocks", "twitter", "aiocron", "redis", "pytest", "irc3-plugins-test"]
test = ["aiocron", "feedparser", "irc3-plugins-test", "pysocks", "pytest", "pytest-aiohttp", "pytest-asyncio", "redis", "requests", "twitter"]
web = ["aiohttp"]

[[package]]
name = "mypy-extensions"
version = "1.1.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"},
{file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"},
]

[[package]]
name = "packaging"
version = "25.0"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
]

[[package]]
name = "pathspec"
version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
]

[[package]]
name = "platformdirs"
version = "4.3.8"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"},
{file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"},
]

[package.extras]
docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"]
type = ["mypy (>=1.14.1)"]

[[package]]
name = "venusian"
version = "3.0.0"
version = "3.1.1"
description = "A library for deferring decorator actions"
category = "main"
optional = false
python-versions = ">=3.5"
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "venusian-3.1.1-py3-none-any.whl", hash = "sha256:0845808a985976acbceaa1fbb871c7fac4fb28ae75453232970e9c2c2866dbf4"},
{file = "venusian-3.1.1.tar.gz", hash = "sha256:534fb3b355669283eb3954581931e5d1d071fce61d029d58f3219a5e3a6f0c41"},
]

[package.extras]
docs = ["sphinx", "repoze.sphinx.autointerface"]
testing = ["pytest", "pytest-cov", "coverage"]
docs = ["Sphinx (>=4.3.2)", "pylons-sphinx-themes", "repoze.sphinx.autointerface", "sphinx-copybutton"]
testing = ["coverage", "pytest", "pytest-cov"]

[metadata]
lock-version = "1.1"
python-versions = "^3.7"
content-hash = "1d29e6d81c2ba0236771fc1ba673e7367e504279164d4f1556d492be4200f620"

[metadata.files]
docopt = [
{file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"},
]
irc3 = [
{file = "irc3-1.1.7-py3-none-any.whl", hash = "sha256:088b7be88817dee5d02e7e06c9dbf503c8e807a8cccc9b36b5be419918574cf1"},
{file = "irc3-1.1.7.tar.gz", hash = "sha256:6800c8876a889736961e199e834dc9933244affa20d4da086fb4b47f76984e12"},
]
venusian = [
{file = "venusian-3.0.0-py3-none-any.whl", hash = "sha256:06e7385786ad3a15c70740b2af8d30dfb063a946a851dcb4159f9e2a2302578f"},
{file = "venusian-3.0.0.tar.gz", hash = "sha256:f6842b7242b1039c0c28f6feef29016e7e7dd3caaeb476a193acf737db31ee38"},
]
lock-version = "2.1"
python-versions = "^3.12"
content-hash = "5d7d7513799a39e01fdb8f7159317204c36855b5948f12b89a71ef7b47bd6a1d"

+ 4
- 3
pyproject.toml 查看文件

@@ -9,10 +9,11 @@ license = "AGPL-3.0-or-later"
twason = 'twason.__main__:main'

[tool.poetry.dependencies]
python = "^3.7"
irc3 = "^1.1.7"
python = "^3.12"
irc3 = "^1.1"

[tool.poetry.dev-dependencies]
[tool.poetry.group.dev.dependencies]
black = "^25.1"

[build-system]
requires = ["poetry-core>=1.0.0"]


+ 13
- 11
twason/__main__.py 查看文件

@@ -32,15 +32,17 @@ def main() -> int:
args = get_arguments()

twitchbot.config = get_config(args.config)
bot = irc3.IrcBot.from_config({
'nick': twitchbot.config.nickname,
'password': twitchbot.config.token,
'autojoins': [],
'host': TWITCH_IRC_SERVER,
'port': TWITCH_IRC_PORT,
'ssl': True,
'includes': [twitchbot.__name__]
})
bot = irc3.IrcBot.from_config(
{
"nick": twitchbot.config.nickname,
"password": twitchbot.config.token,
"autojoins": [],
"host": TWITCH_IRC_SERVER,
"port": TWITCH_IRC_PORT,
"ssl": True,
"includes": [twitchbot.__name__],
}
)

bot.run(forever=True)

@@ -49,10 +51,10 @@ def main() -> int:

def get_arguments():
parser = argparse.ArgumentParser()
parser.add_argument('--config', '-c', type=str, default='config.json')
parser.add_argument("--config", "-c", type=str, default="config.json")

return parser.parse_args()


if __name__ == '__main__':
if __name__ == "__main__":
exit(main())

+ 59
- 45
twason/config.py 查看文件

@@ -30,7 +30,9 @@ class Command:
aliases: [str]
disabled: bool

def __init__(self, name: str, message: str, aliases: [str] = None, disabled: bool = False):
def __init__(
self, name: str, message: str, aliases: [str] = None, disabled: bool = False
):
self.name = name
self.message = message
self.aliases = aliases if aliases is not None else []
@@ -39,10 +41,10 @@ class Command:
@classmethod
def from_dict(cls, params: dict):
return Command(
params.get('name'),
params['message'],
params.get('aliases', []),
params.get('disabled', False)
params.get("name"),
params["message"],
params.get("aliases", []),
params.get("disabled", False),
)


@@ -62,8 +64,8 @@ class Timer:
time_between: int = 10,
msgs_between: int = 10,
strategy: TimerStrategy = TimerStrategy.ROUND_ROBIN,
pool: [Command] = None
):
pool: [Command] = None,
):
self.time_between = time_between
self.msgs_between = msgs_between
self.strategy = strategy
@@ -73,16 +75,16 @@ class Timer:
def from_dict(cls, param: dict):
pool = []

for c in param.get('pool', []):
for c in param.get("pool", []):
command = Command.from_dict(c)
if not command.disabled:
pool.append(command)

return Timer(
time_between=param.get('between', {}).get('time', 10),
msgs_between=param.get('between', {}).get('messages', 10),
strategy=TimerStrategy(param.get('strategy', 'round-robin')),
pool=pool
time_between=param.get("between", {}).get("time", 10),
msgs_between=param.get("between", {}).get("messages", 10),
strategy=TimerStrategy(param.get("strategy", "round-robin")),
pool=pool,
)


@@ -103,7 +105,7 @@ class Config:
command_prefix: str,
commands: [Command],
timer: Timer,
moderators: [moderator.Moderator]
moderators: [moderator.Moderator],
):
self.nickname = nickname
self.channel = channel
@@ -115,14 +117,14 @@ class Config:

@classmethod
def from_dict(cls, params: dict, token: str):
timer = Timer.from_dict(params.get('timer', {}))
timer = Timer.from_dict(params.get("timer", {}))

commands_prefix = params.get('command_prefix', '!')
commands_prefix = params.get("command_prefix", "!")
commands = []

help_command = Command("help", "Voici les commandes disponibles : ")

for command in params.get('commands', []):
for command in params.get("commands", []):
command = Command.from_dict(command)

if command.disabled:
@@ -137,43 +139,53 @@ class Config:
commands.append(command)

moderators = []
for mod in params.get('moderator', []):
moderator_config = params['moderator'][mod]
if mod == 'caps-lock':
moderators.append(moderator.CapsLockModerator(
moderator_config.get("message", "{author}, stop the caps lock!"),
cls.parse_decision(moderator_config.get("decision", "delete")),
moderator_config.get("duration", None),
moderator_config.get("min-size", 5),
moderator_config.get("threshold", 50)
))
if mod == 'flood':
moderators.append(moderator.FloodModerator(
moderator_config.get("message", "{author}, stop the flood!"),
cls.parse_decision(moderator_config.get("decision", "timeout")),
moderator_config.get("duration", None),
moderator_config.get("max-word-length", None),
moderator_config.get("raid-cooldown", None),
moderator_config.get("ignore-hashtags", False),
moderator_config.get("max-msg-occurrences", None),
moderator_config.get("min-time-between-occurrence", None)
))
for mod in params.get("moderator", []):
moderator_config = params["moderator"][mod]
if mod == "caps-lock":
moderators.append(
moderator.CapsLockModerator(
moderator_config.get(
"message", "{author}, stop the caps lock!"
),
cls.parse_decision(moderator_config.get("decision", "delete")),
moderator_config.get("duration", None),
moderator_config.get("min-size", 5),
moderator_config.get("threshold", 50),
)
)
if mod == "flood":
moderators.append(
moderator.FloodModerator(
moderator_config.get("message", "{author}, stop the flood!"),
cls.parse_decision(moderator_config.get("decision", "timeout")),
moderator_config.get("duration", None),
moderator_config.get("max-word-length", None),
moderator_config.get("raid-cooldown", None),
moderator_config.get("ignore-hashtags", False),
moderator_config.get("max-msg-occurrences", None),
moderator_config.get("min-time-between-occurrence", None),
)
)

# Generate help command
if params.get('help', True):
if params.get("help", True):
for command in commands:
help_command.message = "%s %s%s" % (help_command.message, commands_prefix, command.name)
help_command.message = "%s %s%s" % (
help_command.message,
commands_prefix,
command.name,
)

commands.append(help_command)

return Config(
params.get('channel'),
params.get('nickname'),
params.get("channel"),
params.get("nickname"),
token,
commands_prefix,
commands,
timer,
moderators
moderators,
)

@classmethod
@@ -183,7 +195,9 @@ class Config:
elif decision_str == "timeout":
decision = moderator.ModerationDecision.TIMEOUT_USER
else:
print("WARNING: %s moderator's decision is invalid, it has been deactivated!")
print(
"WARNING: %s moderator's decision is invalid, it has been deactivated!"
)
decision = moderator.ModerationDecision.ABSTAIN
return decision

@@ -201,6 +215,6 @@ class Config:


def get_config(file_path: str):
with open(file_path, 'r') as config_file:
token = environ['TWITCH_TOKEN']
with open(file_path, "r") as config_file:
token = environ["TWITCH_TOKEN"]
return Config.from_dict(json.loads(config_file.read()), token)

+ 47
- 33
twason/moderator.py 查看文件

@@ -33,7 +33,12 @@ class Moderator(ABC):
message: str
decision: ModerationDecision

def __init__(self, message: str, decision: ModerationDecision, timeout_duration: Union[None, int]):
def __init__(
self,
message: str,
decision: ModerationDecision,
timeout_duration: Union[None, int],
):
self.message = message
self.timeout_duration = timeout_duration
self.decision = decision
@@ -49,12 +54,12 @@ class Moderator(ABC):

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

@@ -62,17 +67,17 @@ class CapsLockModerator(Moderator):
self.threshold = threshold / 100

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

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

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

n = 0
for char in msg:
if char.strip() == '':
if char.strip() == "":
continue
if char == char.upper():
n += 1
@@ -85,15 +90,15 @@ class CapsLockModerator(Moderator):

class FloodModerator(Moderator):
def __init__(
self,
message: str,
decision: ModerationDecision,
timeout_duration: Union[None, int],
max_word_length: Union[None, int],
raid_cooldown: Union[None, int],
ignore_hashtags: bool,
max_msg_occurrences: Union[None, int],
min_time_between_occurrence: Union[None, int]
self,
message: str,
decision: ModerationDecision,
timeout_duration: Union[None, int],
max_word_length: Union[None, int],
raid_cooldown: Union[None, int],
ignore_hashtags: bool,
max_msg_occurrences: Union[None, int],
min_time_between_occurrence: Union[None, int],
):
super().__init__(message, decision, timeout_duration)
self.max_word_length = max_word_length
@@ -105,15 +110,18 @@ class FloodModerator(Moderator):
self.last_msgs = []

def get_name(self) -> str:
return 'Flood'
return "Flood"

def vote(self, msg: str, author: str) -> ModerationDecision:
if self.raid_cooldown is not None and self.last_raid + timedelta(minutes=self.raid_cooldown) > datetime.now():
if (
self.raid_cooldown is not None
and self.last_raid + timedelta(minutes=self.raid_cooldown) > datetime.now()
):
return ModerationDecision.ABSTAIN

if self.max_word_length is not None:
for word in msg.split(' '):
if word.startswith('#'):
for word in msg.split(" "):
if word.startswith("#"):
continue
if len(word) > self.max_word_length:
return ModerationDecision.TIMEOUT_USER
@@ -123,26 +131,32 @@ class FloodModerator(Moderator):

clean_msg = None
for last_msg in self.last_msgs:
if last_msg['first-occurrence'] + timedelta(seconds=self.min_time_between_occurrence) <= datetime.now():
if (
last_msg["first-occurrence"]
+ timedelta(seconds=self.min_time_between_occurrence)
<= datetime.now()
):
clean_msg = last_msg
break

if author != last_msg['author'] or msg != last_msg['message']:
if author != last_msg["author"] or msg != last_msg["message"]:
break

last_msg['occurrences'] += 1
if last_msg['occurrences'] >= self.max_msg_occurrences:
last_msg["occurrences"] += 1
if last_msg["occurrences"] >= self.max_msg_occurrences:
return ModerationDecision.TIMEOUT_USER

if clean_msg is not None:
self.last_msgs.remove(clean_msg)

self.last_msgs.append({
'first-occurrence': datetime.now(),
'author': author,
'message': msg,
'occurrences': 1
})
self.last_msgs.append(
{
"first-occurrence": datetime.now(),
"author": author,
"message": msg,
"occurrences": 1,
}
)

return ModerationDecision.ABSTAIN



+ 3
- 2
twason/twitch_message.py 查看文件

@@ -16,5 +16,6 @@

from irc3.rfc import raw

USERNOTICE = r'^(@(?P<tags>\S+) )?:(?P<mask>\S+) (?P<event>(USERNOTICE)) (?P<target>\S+)$'

USERNOTICE = (
r"^(@(?P<tags>\S+) )?:(?P<mask>\S+) (?P<event>(USERNOTICE)) (?P<target>\S+)$"
)

+ 57
- 39
twason/twitchbot.py 查看文件

@@ -41,19 +41,19 @@ class TwitchBot:
self.nb_messages_since_timer = 0

def connection_made(self):
print('connected')
print("connected")

def server_ready(self):
print('ready')
print("ready")

def connection_lost(self):
print('connection lost')
print("connection lost")

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

@@ -62,15 +62,24 @@ class TwitchBot:
# print(data)

@irc3.event(irc3.rfc.PRIVMSG)
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])
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' % (author, self.config.command_prefix, command.name))
self.bot.privmsg(target, self._parse_variables(command.message, author=author))
elif tags_dict.get('mod') == '0':
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
@@ -79,30 +88,39 @@ class TwitchBot:
@irc3.event(twitch_message.USERNOTICE)
def on_user_notice(self, tags: str, **_):
tags = utils.parse_tags(tags)
if tags.get('msg-id', None) == 'raid':
if tags.get("msg-id", None) == "raid":
# Notice the Flood moderator a raid has just happened
for moderator in self.config.moderators:
if isinstance(moderator, FloodModerator) and moderator.raid_cooldown is not None:
print("Raid received from %s. Disabling the Flood moderator." % tags.get('display-name'))
if (
isinstance(moderator, FloodModerator)
and moderator.raid_cooldown is not None
):
print(
"Raid received from %s. Disabling the Flood moderator."
% tags.get("display-name")
)
moderator.declare_raid()
break

def play_timer(self):
if not self.messages_stack:
print('Filling the timer messages stack in')
print("Filling the timer messages stack in")
self.messages_stack = self.config.timer.pool.copy()
if self.config.timer.strategy == TimerStrategy.SHUFFLE:
print('Shuffle!')
print("Shuffle!")
shuffle(self.messages_stack)

if self.nb_messages_since_timer < self.config.timer.msgs_between or \
datetime.now() < self.last_timer_date + timedelta(minutes=self.config.timer.time_between):
if (
self.nb_messages_since_timer < self.config.timer.msgs_between
or datetime.now()
< self.last_timer_date + timedelta(minutes=self.config.timer.time_between)
):
return

command = self.messages_stack.pop(0)

print("Timer: %s" % command.message)
self.bot.privmsg('#%s' % self.config.channel, command.message)
self.bot.privmsg("#%s" % self.config.channel, command.message)

self.nb_messages_since_timer = 0
self.last_timer_date = datetime.now()
@@ -110,40 +128,41 @@ class TwitchBot:
def moderate(self, tags: {str: str}, msg: str, author: str, channel: str):
def delete_msg(mod: Moderator):
print("[DELETE (reason: %s)] %s: %s" % (mod.get_name(), author, msg))
self.bot.privmsg(
channel,
"/delete %s" % tags['id']
)
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" % (
"/timeout %s %d %s"
% (
author,
mod.timeout_duration,
self._parse_variables(mod.message, author=author)
)
self._parse_variables(mod.message, author=author),
),
)

# Ignore emotes-only messages
if tags.get('emote-only', '0') == '1':
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 == '':
for emote in tags.get("emotes", "").split("/"):
if emote == "":
break

for indices in emote.split(':')[1].split(','):
[first, last] = indices.split('-')
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:]
message_to_moderate = message_to_moderate[last + 1 :]
else:
message_to_moderate = message_to_moderate[:first - 1] + message_to_moderate[last + 1:]
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, author)
@@ -154,19 +173,18 @@ class TwitchBot:
if vote == ModerationDecision.TIMEOUT_USER:
timeout(moderator)

self.bot.privmsg(channel, self._parse_variables(moderator.message, author=author))
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))
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"
]:
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)
self.bot.join("#%s" % self.config.channel)

Loading…
取消
儲存