feat: add bot generated replies when it's being mentioned
This commit is contained in:
parent
3d44b44625
commit
d64c2398da
9 changed files with 1670 additions and 963 deletions
|
|
@ -2,7 +2,7 @@ files: src|tests
|
|||
exclude: ^$
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
args: [ --markdown-linebreak-ext=md ]
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ ENV PYTHONPATH="$APP_PATH"
|
|||
ENV PATH="$APP_HOME/.local/bin:$PATH"
|
||||
|
||||
# hadolint ignore=DL3001,DL3008,DL3018
|
||||
RUN apk add --no-cache make python3~=3.12 curl \
|
||||
RUN apk add --no-cache make python3~=3.13 curl git \
|
||||
&& adduser -S -u "$USERID" -h "$APP_HOME" "$USERNAME" \
|
||||
&& mkdir -p "$APP_PATH" \
|
||||
&& chown -R "$USERID:$GROUPID" "$APP_PATH"
|
||||
|
|
|
|||
3
Makefile
3
Makefile
|
|
@ -11,6 +11,9 @@ fmt--mypy:
|
|||
fmt--add-noqa:
|
||||
uvx ruff check --add-noqa .
|
||||
|
||||
fmt--autoupdate:
|
||||
uvx pre-commit autoupdate
|
||||
|
||||
|
||||
.PHONY: tests
|
||||
tests:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
apiVersion: v2
|
||||
appVersion: 0.3.6
|
||||
appVersion: 0.3.7
|
||||
description: A Helm chart for Kubernetes
|
||||
name: huesoporro
|
||||
type: application
|
||||
version: 0.3.6
|
||||
version: 0.3.7
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ fullnameOverride: ''
|
|||
image:
|
||||
pullPolicy: Always
|
||||
repository: git.roboces.dev/catalin/huesoporro
|
||||
tag: 0.3.6
|
||||
tag: 0.3.7
|
||||
imagePullSecrets: []
|
||||
ingress:
|
||||
annotations: {}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "huesoporro"
|
||||
version = "0.3.6"
|
||||
version = "0.3.7"
|
||||
description = "Misc Twitch bot"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
|
|
@ -19,12 +19,13 @@ dependencies = [
|
|||
"caribou>=0.4.1",
|
||||
"aiosqlite>=0.20.0",
|
||||
"pyjwt>=2.10.1",
|
||||
"twitchio>=2.10.0",
|
||||
"twitchio==2.10.0",
|
||||
"redis>=5.2.1",
|
||||
"pytz>=2024.2",
|
||||
"discord-py>=2.4.0",
|
||||
"tenacity>=9.0.0",
|
||||
"uvicorn>=0.34.0",
|
||||
"sniffio>=1.3.1",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from tenacity import (
|
|||
stop_after_attempt,
|
||||
wait_exponential,
|
||||
)
|
||||
from twitchio import Channel
|
||||
from twitchio import Channel, Message
|
||||
from twitchio.ext import commands, routines
|
||||
|
||||
from huesoporro.actions.quotes.create_quote_action import CreateQuoteAction
|
||||
|
|
@ -172,6 +172,12 @@ class MessageHandler:
|
|||
"xdd",
|
||||
"xdding",
|
||||
]
|
||||
self.ano_suffix_reply_patterns = [
|
||||
"me la agarras con la mano. venga, tira",
|
||||
"me la agarras con la mano, espabila",
|
||||
"me la agarras con la mano y te falta calle",
|
||||
"vegetasmile",
|
||||
]
|
||||
self.send = channel_send_func
|
||||
|
||||
def get_message_type(self, content: str) -> MessageType:
|
||||
|
|
@ -188,9 +194,12 @@ class MessageHandler:
|
|||
return MessageType.LAUGH
|
||||
return MessageType.OTHER
|
||||
|
||||
async def handle_laugh(self) -> str:
|
||||
def handle_laugh(self) -> str:
|
||||
return random.choice(self.laugh_patterns) # noqa: S311
|
||||
|
||||
def handle_ano_suffix(self) -> str:
|
||||
return random.choice(self.ano_suffix_reply_patterns) # noqa: S311
|
||||
|
||||
|
||||
class SaveMessagesCog(commands.Cog):
|
||||
def __init__(self, bot: Bot):
|
||||
|
|
@ -226,6 +235,36 @@ class SaveMessagesCog(commands.Cog):
|
|||
if hasattr(self, "current_message"):
|
||||
await self.current_message.channel.send(content)
|
||||
|
||||
def is_bot_mention(self, tok: str) -> bool:
|
||||
return tok.lower() == str(self.bot.nick).lower()
|
||||
|
||||
async def _handle_bot_mention(self, message: Message) -> str | None:
|
||||
content = (message.content or "").strip()
|
||||
if not content:
|
||||
return None
|
||||
|
||||
tokens = content.split()
|
||||
contains_mention = any(self.is_bot_mention(t) for t in tokens)
|
||||
if not contains_mention:
|
||||
return None
|
||||
|
||||
# Find the first non-mention token as seed
|
||||
non_mention_tokens = (
|
||||
t.strip(".,!?;:") for t in tokens if not self.is_bot_mention(t)
|
||||
)
|
||||
seed = next((t for t in non_mention_tokens if t), None)
|
||||
|
||||
if not seed:
|
||||
return None
|
||||
|
||||
sentence = await self.generate_svc.run(seed)
|
||||
if not sentence:
|
||||
return None
|
||||
|
||||
await message.channel.send(f"@{message.author.name} {sentence}")
|
||||
|
||||
return sentence
|
||||
|
||||
@commands.Cog.event()
|
||||
async def event_message(self, message):
|
||||
"""Main message event handler"""
|
||||
|
|
@ -236,6 +275,13 @@ class SaveMessagesCog(commands.Cog):
|
|||
|
||||
await self.store_svc.run(message.content)
|
||||
|
||||
# If the message contains a mention to this bot, reply by generating
|
||||
# a sentence from the first word that is not the bot username itself.
|
||||
if await self._handle_bot_mention(message):
|
||||
# If the bot actually replies with something, it should not try to send
|
||||
# any other type of reply
|
||||
return
|
||||
|
||||
msg_type = self.message_handler.get_message_type(message.content)
|
||||
|
||||
response = None
|
||||
|
|
@ -248,10 +294,10 @@ class SaveMessagesCog(commands.Cog):
|
|||
case MessageType.WHAT:
|
||||
response = "WHAT Ramon"
|
||||
case MessageType.LAUGH:
|
||||
response = await self.message_handler.handle_laugh()
|
||||
response = self.message_handler.handle_laugh()
|
||||
case MessageType.ANO_SUFFIX:
|
||||
response = (
|
||||
f"@{message.author.name} me la agarras con la mano. venga, tira"
|
||||
f"@{message.author.name} {self.message_handler.handle_ano_suffix()}"
|
||||
)
|
||||
case MessageType.OTHER:
|
||||
return
|
||||
|
|
|
|||
149
tests/test_bot_mentions.py
Normal file
149
tests/test_bot_mentions.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import types
|
||||
|
||||
import pytest
|
||||
|
||||
from huesoporro.bot import SaveMessagesCog
|
||||
|
||||
|
||||
class DummyDB:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class FakeChannel:
|
||||
def __init__(self):
|
||||
self.sent: list[str] = []
|
||||
|
||||
async def send(self, content: str):
|
||||
self.sent.append(content)
|
||||
|
||||
|
||||
class FakeAuthor:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
|
||||
class FakeMessage:
|
||||
def __init__(self, content: str | None, author_name: str = "alice"):
|
||||
self.content = content
|
||||
self.author = FakeAuthor(author_name)
|
||||
self.channel = FakeChannel()
|
||||
|
||||
|
||||
class FakeBot:
|
||||
def __init__(self, nick: str = "Junie", channel: str = "testchan"):
|
||||
self.nick = nick
|
||||
self.channel = channel
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def patch_markov_and_svcs(monkeypatch):
|
||||
monkeypatch.setattr("huesoporro.bot.MarkovDatabase", DummyDB)
|
||||
|
||||
class _DummyStoreSvc:
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
|
||||
async def run(self, content: str | None):
|
||||
return None
|
||||
|
||||
class _DummyGenSvc:
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
|
||||
async def run(self, seed: str):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr("huesoporro.bot.SentenceStorerSvc", _DummyStoreSvc)
|
||||
monkeypatch.setattr("huesoporro.bot.SentenceGeneratorSvc", _DummyGenSvc)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cog(monkeypatch) -> SaveMessagesCog:
|
||||
bot = FakeBot()
|
||||
return SaveMessagesCog(bot) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def make_async_fn(result=None, exc: Exception | None = None):
|
||||
async def _fn(*args, **kwargs):
|
||||
if exc:
|
||||
raise exc
|
||||
return result
|
||||
|
||||
return _fn
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_bot_mention_returns_none_on_empty_content(cog: SaveMessagesCog):
|
||||
msg = FakeMessage(content=None)
|
||||
res = await cog._handle_bot_mention(msg) # type: ignore[attr-defined]
|
||||
assert res is None
|
||||
assert msg.channel.sent == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_bot_mention_returns_none_when_no_mention(cog: SaveMessagesCog):
|
||||
msg = FakeMessage(content="hello world")
|
||||
res = await cog._handle_bot_mention(msg) # type: ignore[attr-defined]
|
||||
assert res is None
|
||||
assert msg.channel.sent == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_bot_mention_returns_none_when_only_mention(
|
||||
cog: SaveMessagesCog, monkeypatch
|
||||
):
|
||||
msg = FakeMessage(content=cog.bot.nick) # type: ignore[attr-defined]
|
||||
res = await cog._handle_bot_mention(msg) # type: ignore[attr-defined]
|
||||
assert res is None
|
||||
assert msg.channel.sent == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_bot_mention_generates_and_sends_reply(
|
||||
cog: SaveMessagesCog, monkeypatch
|
||||
):
|
||||
msg = FakeMessage(content="juNie hello there")
|
||||
|
||||
monkeypatch.setattr(
|
||||
cog, "generate_svc", types.SimpleNamespace(run=make_async_fn("foo bar"))
|
||||
)
|
||||
|
||||
res = await cog._handle_bot_mention(msg) # type: ignore[attr-defined]
|
||||
|
||||
assert res == "foo bar"
|
||||
assert len(msg.channel.sent) == 1
|
||||
assert msg.channel.sent[0].startswith(f"@{msg.author.name} ")
|
||||
assert msg.channel.sent[0].endswith("foo bar")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_bot_mention_no_send_when_generator_returns_none(
|
||||
cog: SaveMessagesCog, monkeypatch
|
||||
):
|
||||
msg = FakeMessage(content=f"{cog.bot.nick} hello") # type: ignore[attr-defined]
|
||||
|
||||
monkeypatch.setattr(
|
||||
cog, "generate_svc", types.SimpleNamespace(run=make_async_fn(None))
|
||||
)
|
||||
|
||||
res = await cog._handle_bot_mention(msg) # type: ignore[attr-defined]
|
||||
assert res is None
|
||||
assert msg.channel.sent == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_bot_mention_swallows_exceptions_and_returns_none(
|
||||
cog: SaveMessagesCog, monkeypatch
|
||||
):
|
||||
msg = FakeMessage(content=f"{cog.bot.nick} hello") # type: ignore[attr-defined]
|
||||
|
||||
monkeypatch.setattr(
|
||||
cog,
|
||||
"generate_svc",
|
||||
types.SimpleNamespace(run=make_async_fn(exc=RuntimeError("boom"))),
|
||||
)
|
||||
|
||||
res = await cog._handle_bot_mention(msg) # type: ignore[attr-defined]
|
||||
assert res is None
|
||||
assert msg.channel.sent == []
|
||||
Loading…
Add table
Add a link
Reference in a new issue