| Autor | SHA1 | Wiadomość | Data |
|---|---|---|---|
|
|
28a76bbbc8
|
ci(deps): bump amannn/action-semantic-pull-request from 5.4.0 to 5.5.3 (#8)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
6 miesięcy temu |
|
|
08247941ad | ci(black): fix branch | 6 miesięcy temu |
|
|
e672f9c325 | ci(dependabot): remove legacy reviewers | 6 miesięcy temu |
|
|
c74b4208a3 | ci: fix dependabot | 6 miesięcy temu |
|
|
dccb9acc14
|
ci: add CI (#7) | 6 miesięcy temu |
| @@ -0,0 +1,22 @@ | |||||
| version: 2 | |||||
| updates: | |||||
| - package-ecosystem: pip | |||||
| directory: "/" | |||||
| target-branch: main | |||||
| open-pull-requests-limit: 5 | |||||
| schedule: | |||||
| interval: daily | |||||
| commit-message: | |||||
| prefix: chore | |||||
| include: scope | |||||
| - package-ecosystem: github-actions | |||||
| directory: "/" | |||||
| open-pull-requests-limit: 5 | |||||
| target-branch: main | |||||
| schedule: | |||||
| interval: weekly | |||||
| commit-message: | |||||
| prefix: ci | |||||
| include: scope | |||||
| @@ -0,0 +1,19 @@ | |||||
| name: Code style | |||||
| on: | |||||
| push: | |||||
| branches: [main, features] | |||||
| pull_request: | |||||
| branches: [main, 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 | |||||
| @@ -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 | |||||
| @@ -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.5.3 | |||||
| env: | |||||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |||||
| @@ -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]] | [[package]] | ||||
| name = "docopt" | name = "docopt" | ||||
| version = "0.6.2" | version = "0.6.2" | ||||
| description = "Pythonic argument parser, that will make you smile" | description = "Pythonic argument parser, that will make you smile" | ||||
| category = "main" | |||||
| optional = false | optional = false | ||||
| python-versions = "*" | python-versions = "*" | ||||
| groups = ["main"] | |||||
| files = [ | |||||
| {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, | |||||
| ] | |||||
| [[package]] | [[package]] | ||||
| name = "irc3" | name = "irc3" | ||||
| version = "1.1.7" | |||||
| version = "1.1.10" | |||||
| description = "plugable irc client library based on asyncio with DCC and SASL support" | description = "plugable irc client library based on asyncio with DCC and SASL support" | ||||
| category = "main" | |||||
| optional = false | optional = false | ||||
| python-versions = "*" | 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] | [package.dependencies] | ||||
| docopt = "*" | docopt = "*" | ||||
| venusian = ">=3.0" | venusian = ">=3.0" | ||||
| [package.extras] | [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"] | 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]] | [[package]] | ||||
| name = "venusian" | name = "venusian" | ||||
| version = "3.0.0" | |||||
| version = "3.1.1" | |||||
| description = "A library for deferring decorator actions" | description = "A library for deferring decorator actions" | ||||
| category = "main" | |||||
| optional = false | 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] | [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] | [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" | |||||
| @@ -9,10 +9,11 @@ license = "AGPL-3.0-or-later" | |||||
| twason = 'twason.__main__:main' | twason = 'twason.__main__:main' | ||||
| [tool.poetry.dependencies] | [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] | [build-system] | ||||
| requires = ["poetry-core>=1.0.0"] | requires = ["poetry-core>=1.0.0"] | ||||
| @@ -32,15 +32,17 @@ def main() -> int: | |||||
| args = get_arguments() | args = get_arguments() | ||||
| twitchbot.config = get_config(args.config) | 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) | bot.run(forever=True) | ||||
| @@ -49,10 +51,10 @@ def main() -> int: | |||||
| def get_arguments(): | def get_arguments(): | ||||
| parser = argparse.ArgumentParser() | 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() | return parser.parse_args() | ||||
| if __name__ == '__main__': | |||||
| if __name__ == "__main__": | |||||
| exit(main()) | exit(main()) | ||||
| @@ -30,7 +30,9 @@ class Command: | |||||
| aliases: [str] | aliases: [str] | ||||
| disabled: bool | 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.name = name | ||||
| self.message = message | self.message = message | ||||
| self.aliases = aliases if aliases is not None else [] | self.aliases = aliases if aliases is not None else [] | ||||
| @@ -39,10 +41,10 @@ class Command: | |||||
| @classmethod | @classmethod | ||||
| def from_dict(cls, params: dict): | def from_dict(cls, params: dict): | ||||
| return Command( | 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, | time_between: int = 10, | ||||
| msgs_between: int = 10, | msgs_between: int = 10, | ||||
| strategy: TimerStrategy = TimerStrategy.ROUND_ROBIN, | strategy: TimerStrategy = TimerStrategy.ROUND_ROBIN, | ||||
| pool: [Command] = None | |||||
| ): | |||||
| pool: [Command] = None, | |||||
| ): | |||||
| self.time_between = time_between | self.time_between = time_between | ||||
| self.msgs_between = msgs_between | self.msgs_between = msgs_between | ||||
| self.strategy = strategy | self.strategy = strategy | ||||
| @@ -73,16 +75,16 @@ class Timer: | |||||
| def from_dict(cls, param: dict): | def from_dict(cls, param: dict): | ||||
| pool = [] | pool = [] | ||||
| for c in param.get('pool', []): | |||||
| for c in param.get("pool", []): | |||||
| command = Command.from_dict(c) | command = Command.from_dict(c) | ||||
| if not command.disabled: | if not command.disabled: | ||||
| pool.append(command) | pool.append(command) | ||||
| return Timer( | 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, | command_prefix: str, | ||||
| commands: [Command], | commands: [Command], | ||||
| timer: Timer, | timer: Timer, | ||||
| moderators: [moderator.Moderator] | |||||
| moderators: [moderator.Moderator], | |||||
| ): | ): | ||||
| self.nickname = nickname | self.nickname = nickname | ||||
| self.channel = channel | self.channel = channel | ||||
| @@ -115,14 +117,14 @@ class Config: | |||||
| @classmethod | @classmethod | ||||
| def from_dict(cls, params: dict, token: str): | 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 = [] | commands = [] | ||||
| help_command = Command("help", "Voici les commandes disponibles : ") | 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) | command = Command.from_dict(command) | ||||
| if command.disabled: | if command.disabled: | ||||
| @@ -137,43 +139,53 @@ class Config: | |||||
| commands.append(command) | commands.append(command) | ||||
| moderators = [] | 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 | # Generate help command | ||||
| if params.get('help', True): | |||||
| if params.get("help", True): | |||||
| for command in commands: | 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) | commands.append(help_command) | ||||
| return Config( | return Config( | ||||
| params.get('channel'), | |||||
| params.get('nickname'), | |||||
| params.get("channel"), | |||||
| params.get("nickname"), | |||||
| token, | token, | ||||
| commands_prefix, | commands_prefix, | ||||
| commands, | commands, | ||||
| timer, | timer, | ||||
| moderators | |||||
| moderators, | |||||
| ) | ) | ||||
| @classmethod | @classmethod | ||||
| @@ -183,7 +195,9 @@ class Config: | |||||
| elif decision_str == "timeout": | elif decision_str == "timeout": | ||||
| decision = moderator.ModerationDecision.TIMEOUT_USER | decision = moderator.ModerationDecision.TIMEOUT_USER | ||||
| else: | 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 | decision = moderator.ModerationDecision.ABSTAIN | ||||
| return decision | return decision | ||||
| @@ -201,6 +215,6 @@ class Config: | |||||
| def get_config(file_path: str): | 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) | return Config.from_dict(json.loads(config_file.read()), token) | ||||
| @@ -33,7 +33,12 @@ class Moderator(ABC): | |||||
| message: str | message: str | ||||
| decision: ModerationDecision | 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.message = message | ||||
| self.timeout_duration = timeout_duration | self.timeout_duration = timeout_duration | ||||
| self.decision = decision | self.decision = decision | ||||
| @@ -49,12 +54,12 @@ class Moderator(ABC): | |||||
| class CapsLockModerator(Moderator): | class CapsLockModerator(Moderator): | ||||
| def __init__( | 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) | super().__init__(message, decision, timeout_duration) | ||||
| @@ -62,17 +67,17 @@ class CapsLockModerator(Moderator): | |||||
| self.threshold = threshold / 100 | self.threshold = threshold / 100 | ||||
| def get_name(self) -> str: | def get_name(self) -> str: | ||||
| return 'Caps Lock' | |||||
| return "Caps Lock" | |||||
| def vote(self, msg: str, author: str) -> ModerationDecision: | 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: | if len(msg) < self.min_size: | ||||
| return ModerationDecision.ABSTAIN | return ModerationDecision.ABSTAIN | ||||
| n = 0 | n = 0 | ||||
| for char in msg: | for char in msg: | ||||
| if char.strip() == '': | |||||
| if char.strip() == "": | |||||
| continue | continue | ||||
| if char == char.upper(): | if char == char.upper(): | ||||
| n += 1 | n += 1 | ||||
| @@ -85,15 +90,15 @@ class CapsLockModerator(Moderator): | |||||
| class FloodModerator(Moderator): | class FloodModerator(Moderator): | ||||
| def __init__( | 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) | super().__init__(message, decision, timeout_duration) | ||||
| self.max_word_length = max_word_length | self.max_word_length = max_word_length | ||||
| @@ -105,15 +110,18 @@ class FloodModerator(Moderator): | |||||
| self.last_msgs = [] | self.last_msgs = [] | ||||
| def get_name(self) -> str: | def get_name(self) -> str: | ||||
| return 'Flood' | |||||
| return "Flood" | |||||
| def vote(self, msg: str, author: str) -> ModerationDecision: | 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 | return ModerationDecision.ABSTAIN | ||||
| if self.max_word_length is not None: | 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 | continue | ||||
| if len(word) > self.max_word_length: | if len(word) > self.max_word_length: | ||||
| return ModerationDecision.TIMEOUT_USER | return ModerationDecision.TIMEOUT_USER | ||||
| @@ -123,26 +131,32 @@ class FloodModerator(Moderator): | |||||
| clean_msg = None | clean_msg = None | ||||
| for last_msg in self.last_msgs: | 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 | clean_msg = last_msg | ||||
| break | break | ||||
| if author != last_msg['author'] or msg != last_msg['message']: | |||||
| if author != last_msg["author"] or msg != last_msg["message"]: | |||||
| break | 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 | return ModerationDecision.TIMEOUT_USER | ||||
| if clean_msg is not None: | if clean_msg is not None: | ||||
| self.last_msgs.remove(clean_msg) | 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 | return ModerationDecision.ABSTAIN | ||||
| @@ -16,5 +16,6 @@ | |||||
| from irc3.rfc import raw | 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+)$" | |||||
| ) | |||||
| @@ -41,19 +41,19 @@ class TwitchBot: | |||||
| self.nb_messages_since_timer = 0 | self.nb_messages_since_timer = 0 | ||||
| def connection_made(self): | def connection_made(self): | ||||
| print('connected') | |||||
| print("connected") | |||||
| def server_ready(self): | def server_ready(self): | ||||
| print('ready') | |||||
| print("ready") | |||||
| def connection_lost(self): | def connection_lost(self): | ||||
| print('connection lost') | |||||
| print("connection lost") | |||||
| @staticmethod | @staticmethod | ||||
| def _parse_variables(in_str: str, **kwargs): | def _parse_variables(in_str: str, **kwargs): | ||||
| for key in kwargs: | for key in kwargs: | ||||
| value = kwargs[key] | 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 | ||||
| @@ -62,15 +62,24 @@ class TwitchBot: | |||||
| # print(data) | # print(data) | ||||
| @irc3.event(irc3.rfc.PRIVMSG) | @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) | tags_dict = utils.parse_tags(tags) | ||||
| if command is not None: | 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.moderate(tags_dict, data, author, target) | ||||
| self.nb_messages_since_timer += 1 | self.nb_messages_since_timer += 1 | ||||
| @@ -79,30 +88,39 @@ class TwitchBot: | |||||
| @irc3.event(twitch_message.USERNOTICE) | @irc3.event(twitch_message.USERNOTICE) | ||||
| def on_user_notice(self, tags: str, **_): | def on_user_notice(self, tags: str, **_): | ||||
| tags = utils.parse_tags(tags) | 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 | # Notice the Flood moderator a raid has just happened | ||||
| for moderator in self.config.moderators: | 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() | moderator.declare_raid() | ||||
| break | break | ||||
| def play_timer(self): | def play_timer(self): | ||||
| if not self.messages_stack: | 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() | self.messages_stack = self.config.timer.pool.copy() | ||||
| if self.config.timer.strategy == TimerStrategy.SHUFFLE: | if self.config.timer.strategy == TimerStrategy.SHUFFLE: | ||||
| print('Shuffle!') | |||||
| print("Shuffle!") | |||||
| shuffle(self.messages_stack) | 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 | return | ||||
| command = self.messages_stack.pop(0) | command = self.messages_stack.pop(0) | ||||
| print("Timer: %s" % command.message) | 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.nb_messages_since_timer = 0 | ||||
| self.last_timer_date = datetime.now() | 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 moderate(self, tags: {str: str}, msg: str, author: str, channel: str): | ||||
| def delete_msg(mod: Moderator): | def delete_msg(mod: Moderator): | ||||
| print("[DELETE (reason: %s)] %s: %s" % (mod.get_name(), author, msg)) | 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): | def timeout(mod: Moderator): | ||||
| print("[TIMEOUT (reason: %s)] %s: %s" % (mod.get_name(), author, msg)) | print("[TIMEOUT (reason: %s)] %s: %s" % (mod.get_name(), author, msg)) | ||||
| self.bot.privmsg( | self.bot.privmsg( | ||||
| channel, | channel, | ||||
| "/timeout %s %d %s" % ( | |||||
| "/timeout %s %d %s" | |||||
| % ( | |||||
| author, | author, | ||||
| mod.timeout_duration, | mod.timeout_duration, | ||||
| self._parse_variables(mod.message, author=author) | |||||
| ) | |||||
| self._parse_variables(mod.message, author=author), | |||||
| ), | |||||
| ) | ) | ||||
| # Ignore emotes-only messages | # Ignore emotes-only messages | ||||
| if tags.get('emote-only', '0') == '1': | |||||
| if tags.get("emote-only", "0") == "1": | |||||
| return | return | ||||
| message_to_moderate = msg | message_to_moderate = msg | ||||
| # Remove emotes from message before moderating | # 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 | 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) | first, last = int(first), int(last) | ||||
| if first == 0: | if first == 0: | ||||
| message_to_moderate = message_to_moderate[last + 1:] | |||||
| message_to_moderate = message_to_moderate[last + 1 :] | |||||
| else: | 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: | for moderator in self.config.moderators: | ||||
| vote = moderator.vote(message_to_moderate, author) | vote = moderator.vote(message_to_moderate, author) | ||||
| @@ -154,19 +173,18 @@ class TwitchBot: | |||||
| if vote == ModerationDecision.TIMEOUT_USER: | if vote == ModerationDecision.TIMEOUT_USER: | ||||
| timeout(moderator) | 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 | 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) | @irc3.event(irc3.rfc.CONNECTED) | ||||
| def on_connected(self, **_): | 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.send_line(line) | ||||
| self.bot.join('#%s' % self.config.channel) | |||||
| self.bot.join("#%s" % self.config.channel) | |||||