feat: remove !h and make the bot have an in-memory dict of greeted users instead of using the backoff service
This commit is contained in:
parent
48a3235323
commit
b2185f4174
52 changed files with 404 additions and 353 deletions
|
|
@ -29,9 +29,10 @@ WORKDIR "$APP_PATH"
|
|||
|
||||
COPY --chown=$USERNAME pyproject.toml uv.lock Makefile README.md ./
|
||||
|
||||
COPY --chown=$USERNAME src/ src/
|
||||
|
||||
RUN uv sync
|
||||
|
||||
COPY --chown=$USERNAME src/ src/
|
||||
COPY --chown=$USERNAME migrations/ migrations/
|
||||
|
||||
|
||||
|
|
|
|||
2
Makefile
2
Makefile
|
|
@ -16,7 +16,7 @@ tests:
|
|||
uv run coverage xml
|
||||
|
||||
serve:
|
||||
uv run python -m src.huesoporro.main
|
||||
uv run python -m src.apps.httpapi.litestar.main
|
||||
|
||||
build:
|
||||
docker build . -t git.roboces.dev/catalin/$(PROJECT_NAME):$(PROJECT_TAG) --target $(PROJECT_TARGET)
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ type: application
|
|||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.2.9
|
||||
version: 0.3.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "0.2.9"
|
||||
appVersion: "0.3.0"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ image:
|
|||
# This sets the pull policy for images.
|
||||
pullPolicy: Always
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: "0.2.9"
|
||||
tag: "0.3.0"
|
||||
|
||||
# This is for the secretes for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
|
||||
imagePullSecrets: []
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
languages.python.version = "3.12.8";
|
||||
|
||||
enterShell = ''
|
||||
uv sync
|
||||
'';
|
||||
|
||||
dotenv.enable = true;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
[project]
|
||||
name = "huesoporro"
|
||||
version = "0.2.9"
|
||||
description = "Misc Twitch bots"
|
||||
version = "0.3.0"
|
||||
description = "Misc Twitch bot"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
{ name = "185504a9", email = "catalin@roboces.dev" }
|
||||
|
|
@ -25,6 +25,13 @@ dependencies = [
|
|||
"discord-py>=2.4.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
huesoporro = "apps.cli.typer.main:app"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = [
|
||||
"mypy>=1.13.0",
|
||||
|
|
@ -46,7 +53,8 @@ module = [
|
|||
"caribou.migrate",
|
||||
"twitchio",
|
||||
"twitchio.ext",
|
||||
"gtts"
|
||||
"gtts",
|
||||
"yt_dlp"
|
||||
]
|
||||
ignore_missing_imports = true
|
||||
|
||||
|
|
@ -60,3 +68,9 @@ extend-ignore = ["S101", "ISC002", "COM812", "ISC001", "EM101", "EM102"]
|
|||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
|
||||
[dependency-groups]
|
||||
cli = [
|
||||
"typer>=0.15.1",
|
||||
"yt-dlp>=2025.1.26",
|
||||
]
|
||||
|
|
|
|||
24
src/apps/cli/typer/main.py
Normal file
24
src/apps/cli/typer/main.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from pathlib import Path
|
||||
|
||||
from loguru import logger
|
||||
from typer import Typer
|
||||
|
||||
from huesoporro.actions.import_from_vod import ImportFromVODAction
|
||||
from huesoporro.settings import Settings
|
||||
from huesoporro.svc.clean_cc_svc import CleanCCSvc
|
||||
from huesoporro.svc.download_closed_captions import DownloadClosedCaptionsSvc
|
||||
|
||||
app = Typer()
|
||||
|
||||
|
||||
@app.command()
|
||||
def import_vod_cc(channel_name: str, youtube_url: str, db_path: Path | None = None):
|
||||
logger.info(f"Importing VOD closed captions for {channel_name} from {youtube_url}")
|
||||
s = Settings.get(db_filepath=db_path)
|
||||
import_from_vod_action = ImportFromVODAction(
|
||||
download_closed_captions_svc=DownloadClosedCaptionsSvc(),
|
||||
clean_cc_svc=CleanCCSvc(),
|
||||
s=s,
|
||||
)
|
||||
for cc_filepath in import_from_vod_action.run(channel_name, youtube_url):
|
||||
logger.info(f"Closed captions imported from {cc_filepath}")
|
||||
0
src/apps/httpapi/__init__.py
Normal file
0
src/apps/httpapi/__init__.py
Normal file
0
src/apps/httpapi/litestar/__init__.py
Normal file
0
src/apps/httpapi/litestar/__init__.py
Normal file
|
|
@ -1,16 +1,17 @@
|
|||
from litestar import Request
|
||||
from litestar.exceptions import HTTPException
|
||||
|
||||
from src.huesoporro.actions.authenticate import AuthenticateAction
|
||||
from src.huesoporro.actions.get_user_by_jwt import GetUserByJWTAction
|
||||
from src.huesoporro.infra.authenticator import TwitchAuthenticator
|
||||
from src.huesoporro.infra.db import Database
|
||||
from src.huesoporro.infra.repos import UserRepo
|
||||
from src.huesoporro.models import User
|
||||
from src.huesoporro.settings import Settings
|
||||
from src.huesoporro.svc.get_chatbot_settings import ChatbotSettingsGetterSvc
|
||||
from src.huesoporro.svc.get_sentences_svc import SentencesGetterSvc
|
||||
from src.huesoporro.svc.store_settings import ChatbotSettingsStorerSvc
|
||||
from huesoporro.actions.authenticate import AuthenticateAction
|
||||
from huesoporro.actions.get_user_by_jwt import GetUserByJWTAction
|
||||
from huesoporro.infra.authenticator import TwitchAuthenticator
|
||||
from huesoporro.infra.db import Database
|
||||
from huesoporro.infra.repos import UserRepo
|
||||
from huesoporro.libs.db import MarkovDatabase
|
||||
from huesoporro.models import User
|
||||
from huesoporro.settings import Settings
|
||||
from huesoporro.svc.get_chatbot_settings import ChatbotSettingsGetterSvc
|
||||
from huesoporro.svc.store import SentenceStorerSvc
|
||||
from huesoporro.svc.store_settings import ChatbotSettingsStorerSvc
|
||||
|
||||
|
||||
def get_settings() -> Settings:
|
||||
|
|
@ -53,8 +54,8 @@ async def store_chatbot_settings_svc(db: Database):
|
|||
return ChatbotSettingsStorerSvc(db=db)
|
||||
|
||||
|
||||
async def get_sentences_svc(db: Database):
|
||||
return SentencesGetterSvc(db=db)
|
||||
async def get_sentences_storer_svc(db: MarkovDatabase):
|
||||
return SentenceStorerSvc(db=db)
|
||||
|
||||
|
||||
async def get_user_repo(s: Settings):
|
||||
|
|
@ -6,37 +6,35 @@ from litestar.exceptions import HTTPException
|
|||
from litestar.static_files import StaticFilesConfig
|
||||
from litestar.template import TemplateConfig
|
||||
|
||||
from src.huesoporro.api.dependencies import (
|
||||
from apps.httpapi.litestar.dependencies import (
|
||||
authenticate,
|
||||
get_authenticate_action,
|
||||
get_authenticator,
|
||||
get_chatbot_settings_svc,
|
||||
get_db,
|
||||
get_get_user_by_jwt_action,
|
||||
get_sentences_svc,
|
||||
get_sentences_storer_svc,
|
||||
get_settings,
|
||||
get_user_repo,
|
||||
store_chatbot_settings_svc,
|
||||
)
|
||||
from src.huesoporro.api.errors import (
|
||||
from apps.httpapi.litestar.errors import (
|
||||
after_exception_handler,
|
||||
http_exception_handler,
|
||||
httpx_status_error_handler,
|
||||
)
|
||||
from src.huesoporro.api.routes.api import (
|
||||
from apps.httpapi.litestar.routes.api import (
|
||||
get_bot_settings,
|
||||
get_bot_status,
|
||||
get_index,
|
||||
get_sentences,
|
||||
get_tts_overlay,
|
||||
get_tts_permalink,
|
||||
manage_bot,
|
||||
save_bot_settings,
|
||||
save_new_sentence,
|
||||
)
|
||||
from src.huesoporro.api.routes.auth import get_code, login
|
||||
from src.huesoporro.bot import BotsManager
|
||||
from src.huesoporro.settings import Settings
|
||||
from apps.httpapi.litestar.routes.auth import get_code, login
|
||||
from huesoporro.bot import BotsManager
|
||||
from huesoporro.settings import Settings
|
||||
|
||||
|
||||
@get("/healthz")
|
||||
|
|
@ -57,8 +55,6 @@ def create_app():
|
|||
get_bot_status,
|
||||
save_bot_settings,
|
||||
get_bot_settings,
|
||||
get_sentences,
|
||||
save_new_sentence,
|
||||
],
|
||||
static_files_config=(
|
||||
StaticFilesConfig(
|
||||
|
|
@ -87,7 +83,7 @@ def create_app():
|
|||
"bm": Provide(BotsManager, use_cache=True),
|
||||
"gbs": Provide(get_chatbot_settings_svc),
|
||||
"sbs": Provide(store_chatbot_settings_svc),
|
||||
"sgs": Provide(get_sentences_svc),
|
||||
"sss": Provide(get_sentences_storer_svc),
|
||||
"authenticator": Provide(get_authenticator),
|
||||
"authenticate_action": Provide(get_authenticate_action),
|
||||
"user_repo": Provide(get_user_repo),
|
||||
0
src/apps/httpapi/litestar/routes/__init__.py
Normal file
0
src/apps/httpapi/litestar/routes/__init__.py
Normal file
|
|
@ -1,14 +1,14 @@
|
|||
from typing import Literal
|
||||
|
||||
from litestar import MediaType, Response, get, post, put
|
||||
from litestar import MediaType, Response, get, put
|
||||
from litestar.datastructures import UploadFile
|
||||
from litestar.response import Template
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from src.huesoporro.bot import BotsManager
|
||||
from src.huesoporro.models import ChatbotSettings, User
|
||||
from src.huesoporro.svc.get_chatbot_settings import ChatbotSettingsGetterSvc
|
||||
from src.huesoporro.svc.get_sentences_svc import SentencesGetterSvc
|
||||
from src.huesoporro.svc.store_settings import ChatbotSettingsStorerSvc
|
||||
from huesoporro.bot import BotsManager
|
||||
from huesoporro.models import ChatbotSettings, User
|
||||
from huesoporro.svc.get_chatbot_settings import ChatbotSettingsGetterSvc
|
||||
from huesoporro.svc.store_settings import ChatbotSettingsStorerSvc
|
||||
|
||||
|
||||
class ManageBotDTO(BaseModel):
|
||||
|
|
@ -16,6 +16,13 @@ class ManageBotDTO(BaseModel):
|
|||
channel_name: str | None = None
|
||||
|
||||
|
||||
class ImportTextFileDTO(BaseModel):
|
||||
file: UploadFile
|
||||
channel_name: str
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
|
||||
@get(
|
||||
"/tts",
|
||||
media_type=MediaType.HTML,
|
||||
|
|
@ -97,17 +104,3 @@ async def save_bot_settings(
|
|||
) -> dict:
|
||||
await sbs.run(user=user, bot_settings=data)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@get("/sentences")
|
||||
async def get_sentences(user: User, sgs: SentencesGetterSvc) -> Template:
|
||||
sentences = await sgs.run(user=user)
|
||||
return Template(
|
||||
template_name="sentences.html",
|
||||
context={"sentences": [sentence.model_dump() for sentence in sentences]},
|
||||
)
|
||||
|
||||
|
||||
@post("/api/v1/sentences")
|
||||
async def save_new_sentence(user: User, data: dict) -> dict:
|
||||
return {"id": 54, "sentence": data["sentence"]}
|
||||
|
|
@ -4,8 +4,8 @@ from litestar import MediaType, get
|
|||
from litestar.datastructures.cookie import Cookie
|
||||
from litestar.response import Redirect, Template
|
||||
|
||||
from src.huesoporro.actions.authenticate import AuthenticateAction
|
||||
from src.huesoporro.settings import Settings
|
||||
from huesoporro.actions.authenticate import AuthenticateAction
|
||||
from huesoporro.settings import Settings
|
||||
|
||||
|
||||
@get(path="/o/code")
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
from src.huesoporro.infra.authenticator import TwitchAuthenticator
|
||||
from src.huesoporro.infra.repos import UserRepo
|
||||
from src.huesoporro.models import User
|
||||
from src.huesoporro.settings import Settings
|
||||
from huesoporro.infra.authenticator import TwitchAuthenticator
|
||||
from huesoporro.infra.repos import UserRepo
|
||||
from huesoporro.models import User
|
||||
from huesoporro.settings import Settings
|
||||
|
||||
|
||||
class AuthenticateAction(BaseModel):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
from src.huesoporro.models import Quote
|
||||
from src.huesoporro.svc.get_random_quote import RandomQuoteGetterSvc
|
||||
from huesoporro.models import Quote
|
||||
from huesoporro.svc.get_random_quote import RandomQuoteGetterSvc
|
||||
|
||||
|
||||
class GetRandomQuoteAction(BaseModel):
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.huesoporro.infra.authenticator import TwitchAuthenticator
|
||||
from src.huesoporro.infra.repos import UserRepo
|
||||
from src.huesoporro.models import User
|
||||
from src.huesoporro.settings import Settings
|
||||
from huesoporro.infra.authenticator import TwitchAuthenticator
|
||||
from huesoporro.infra.repos import UserRepo
|
||||
from huesoporro.models import User
|
||||
from huesoporro.settings import Settings
|
||||
|
||||
|
||||
class GetUserByJWTAction(BaseModel):
|
||||
|
|
|
|||
34
src/huesoporro/actions/import_from_vod.py
Normal file
34
src/huesoporro/actions/import_from_vod.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from huesoporro.libs.db import MarkovDatabase
|
||||
from huesoporro.settings import Settings
|
||||
from huesoporro.svc.clean_cc_svc import CleanCCSvc
|
||||
from huesoporro.svc.download_closed_captions import DownloadClosedCaptionsSvc
|
||||
from huesoporro.svc.store import SentenceStorerSvc
|
||||
|
||||
|
||||
class ImportFromVODAction(BaseModel):
|
||||
download_closed_captions_svc: DownloadClosedCaptionsSvc
|
||||
|
||||
clean_cc_svc: CleanCCSvc
|
||||
s: Settings = Field(default_factory=Settings.get)
|
||||
|
||||
ignore_lines: set[str] = {
|
||||
"WEBVTT",
|
||||
"Kind: captions",
|
||||
"Language: en",
|
||||
"Language: es",
|
||||
}
|
||||
|
||||
def run(self, channel_name: str, youtube_url: str) -> Generator[Path, None, None]:
|
||||
for cc_filepath in self.download_closed_captions_svc.run(youtube_url):
|
||||
storer_svc = SentenceStorerSvc(
|
||||
db=MarkovDatabase(channel=channel_name, settings=self.s)
|
||||
)
|
||||
for line in self.clean_cc_svc.run(cc_filepath):
|
||||
if line and line not in self.ignore_lines:
|
||||
storer_svc.store_sentence(line.strip())
|
||||
yield cc_filepath
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
from src.huesoporro.infra.authenticator import TwitchAuthenticator
|
||||
from src.huesoporro.infra.repos import UserRepo
|
||||
from src.huesoporro.models import User
|
||||
from src.huesoporro.settings import Settings
|
||||
from huesoporro.infra.authenticator import TwitchAuthenticator
|
||||
from huesoporro.infra.repos import UserRepo
|
||||
from huesoporro.models import User
|
||||
from huesoporro.settings import Settings
|
||||
|
||||
|
||||
class RefreshAction(BaseModel):
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import datetime
|
|||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.huesoporro.models import Quote, User
|
||||
from src.huesoporro.svc.is_mod import IsModSvc
|
||||
from src.huesoporro.svc.quote_storer_svc import QuoteStorerSvc
|
||||
from huesoporro.models import Quote, User
|
||||
from huesoporro.svc.is_mod import IsModSvc
|
||||
from huesoporro.svc.quote_storer_svc import QuoteStorerSvc
|
||||
|
||||
|
||||
class StoreQuoteAction(BaseModel):
|
||||
|
|
|
|||
|
|
@ -2,25 +2,26 @@ import asyncio
|
|||
import random
|
||||
from collections.abc import Callable
|
||||
from enum import StrEnum
|
||||
from typing import ClassVar
|
||||
|
||||
from loguru import logger
|
||||
from twitchio import Channel
|
||||
from twitchio.ext import commands, routines
|
||||
|
||||
from src.huesoporro.actions.get_random_quote import GetRandomQuoteAction
|
||||
from src.huesoporro.actions.store_quote import StoreQuoteAction
|
||||
from src.huesoporro.api.dependencies import get_settings
|
||||
from src.huesoporro.infra.db import Database
|
||||
from src.huesoporro.infra.repos import QuoteRepo
|
||||
from src.huesoporro.libs.db import Database as MarkovDB
|
||||
from src.huesoporro.models import ChatbotSettings, User
|
||||
from src.huesoporro.svc.backoff_service import BackoffService
|
||||
from src.huesoporro.svc.generate import SentenceGeneratorSvc
|
||||
from src.huesoporro.svc.get_random_quote import RandomQuoteGetterSvc
|
||||
from src.huesoporro.svc.hello import HelloGeneratorSvc
|
||||
from src.huesoporro.svc.is_mod import IsModSvc
|
||||
from src.huesoporro.svc.quote_storer_svc import QuoteStorerSvc
|
||||
from src.huesoporro.svc.store import SentenceStorerSvc
|
||||
from huesoporro.actions.get_random_quote import GetRandomQuoteAction
|
||||
from huesoporro.actions.store_quote import StoreQuoteAction
|
||||
from huesoporro.infra.db import Database
|
||||
from huesoporro.infra.repos import QuoteRepo
|
||||
from huesoporro.libs.db import MarkovDatabase
|
||||
from huesoporro.models import ChatbotSettings, User
|
||||
from huesoporro.settings import Settings
|
||||
from huesoporro.svc.backoff_service import BackoffService
|
||||
from huesoporro.svc.generate import SentenceGeneratorSvc
|
||||
from huesoporro.svc.get_random_quote import RandomQuoteGetterSvc
|
||||
from huesoporro.svc.hello import get_hello_generator_svc
|
||||
from huesoporro.svc.is_mod import IsModSvc
|
||||
from huesoporro.svc.quote_storer_svc import QuoteStorerSvc
|
||||
from huesoporro.svc.store import SentenceStorerSvc
|
||||
|
||||
|
||||
class Bot(commands.Bot):
|
||||
|
|
@ -30,10 +31,10 @@ class Bot(commands.Bot):
|
|||
)
|
||||
self.channel = channel
|
||||
self.user = user
|
||||
self.generate_svc = SentenceGeneratorSvc(db=MarkovDB(channel=channel))
|
||||
self.hello_svc = HelloGeneratorSvc()
|
||||
self.generate_svc = SentenceGeneratorSvc(db=MarkovDatabase(channel=channel))
|
||||
self.hello_svc = get_hello_generator_svc()
|
||||
db = Database()
|
||||
self.quote_repo = QuoteRepo(s=get_settings())
|
||||
self.quote_repo = QuoteRepo(s=Settings.get())
|
||||
self.get_random_quote_svc = RandomQuoteGetterSvc(quote_repo=self.quote_repo)
|
||||
self.get_random_quote_action = GetRandomQuoteAction(
|
||||
quote_getter_svc=self.get_random_quote_svc
|
||||
|
|
@ -54,11 +55,6 @@ class Bot(commands.Bot):
|
|||
logger.info(f"Logged in as {self.nick}")
|
||||
logger.info(f"User id is {self.user_id}")
|
||||
|
||||
@commands.command(aliases=["h"])
|
||||
async def hello(self, ctx: commands.Context, username: str | None = None):
|
||||
username = username or ctx.author.name
|
||||
await ctx.send(self.hello_svc.run(username))
|
||||
|
||||
@commands.command(aliases=["g"])
|
||||
async def generate(self, ctx: commands.Context, *, words: str | None = None):
|
||||
sentence = await self.generate_svc.run(words)
|
||||
|
|
@ -98,8 +94,9 @@ class Bot(commands.Bot):
|
|||
quote = await self.get_random_quote_action.run(channel_name=self.channel)
|
||||
if quote:
|
||||
channel = self.get_channel_conn()
|
||||
logger.info(f"Sending random quote {quote.quote}")
|
||||
await channel.send(quote.quote)
|
||||
if channel:
|
||||
logger.info(f"Sending random quote {quote.quote}")
|
||||
await channel.send(quote.quote)
|
||||
|
||||
async def send_generation(self):
|
||||
sentence = await self.generate_svc.run()
|
||||
|
|
@ -123,6 +120,23 @@ class Bot(commands.Bot):
|
|||
self.generation_routine.cancel()
|
||||
|
||||
|
||||
class HelloMessagesCog(commands.Cog):
|
||||
hello_patterns: ClassVar[list[str]] = ["hola", "HOLA", "hiii", "ayo"]
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.hello_svc = get_hello_generator_svc()
|
||||
|
||||
@commands.Cog.event()
|
||||
async def event_message(self, message):
|
||||
if not message.author:
|
||||
return
|
||||
if message.content in self.hello_patterns:
|
||||
hello = self.hello_svc.run(message.author.name)
|
||||
if hello:
|
||||
await message.channel.send(hello)
|
||||
|
||||
|
||||
class MessageType(StrEnum):
|
||||
COMMAND = "COMMAND"
|
||||
HELLO = "HELLO"
|
||||
|
|
@ -136,7 +150,6 @@ class MessageHandler:
|
|||
"""Handles different types of messages with their corresponding responses"""
|
||||
|
||||
def __init__(self, channel_send_func: Callable):
|
||||
self.hello_patterns = ["hola", "HOLA", "hiii", "ayo"]
|
||||
self.laugh_patterns = [
|
||||
"om",
|
||||
"KEK",
|
||||
|
|
@ -154,8 +167,6 @@ class MessageHandler:
|
|||
"""Determines the type of message based on its content"""
|
||||
if content.startswith("!"):
|
||||
return MessageType.COMMAND
|
||||
if content in self.hello_patterns:
|
||||
return MessageType.HELLO
|
||||
if content == "Yes":
|
||||
return MessageType.YES
|
||||
if content.startswith("WHAT"):
|
||||
|
|
@ -164,10 +175,6 @@ class MessageHandler:
|
|||
return MessageType.LAUGH
|
||||
return MessageType.OTHER
|
||||
|
||||
async def handle_hello(self, author_name: str, hello_svc) -> str:
|
||||
"""Handles hello messages"""
|
||||
return hello_svc.run(author_name)
|
||||
|
||||
async def handle_laugh(self) -> str:
|
||||
"""Handles laugh messages"""
|
||||
return random.choice(self.laugh_patterns) # noqa: S311
|
||||
|
|
@ -176,20 +183,17 @@ class MessageHandler:
|
|||
class SaveMessagesCog(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.store_svc = SentenceStorerSvc(db=MarkovDB(channel=bot.channel))
|
||||
self.hello_svc = HelloGeneratorSvc()
|
||||
self.store_svc = SentenceStorerSvc(db=MarkovDatabase(channel=bot.channel))
|
||||
self.generate_svc = SentenceGeneratorSvc(db=MarkovDatabase(channel=bot.channel))
|
||||
self.backoff_svc = BackoffService()
|
||||
self.message_handler = MessageHandler(self._send_message)
|
||||
|
||||
# Register a separate send function for each message type
|
||||
self.send_functions = {
|
||||
MessageType.HELLO: self._create_typed_send("hello"),
|
||||
MessageType.YES: self._create_typed_send("yes"),
|
||||
MessageType.WHAT: self._create_typed_send("what"),
|
||||
MessageType.LAUGH: self._create_typed_send("laugh"),
|
||||
}
|
||||
|
||||
# Register each send function with its own backoff
|
||||
for func in self.send_functions.values():
|
||||
self.backoff_svc.add_callable(func, backoff_seconds=10)
|
||||
|
||||
|
|
@ -229,10 +233,6 @@ class SaveMessagesCog(commands.Cog):
|
|||
match msg_type:
|
||||
case MessageType.COMMAND:
|
||||
return
|
||||
case MessageType.HELLO:
|
||||
response = await self.message_handler.handle_hello(
|
||||
message.author.name, self.hello_svc
|
||||
)
|
||||
case MessageType.YES:
|
||||
response = "Indeed"
|
||||
case MessageType.WHAT:
|
||||
|
|
@ -258,6 +258,7 @@ class BotsManager:
|
|||
logger.info(f"Adding bot for {user.user}")
|
||||
bot = Bot(user=user, channel=channel, chatbot_settings=chatbot_settings)
|
||||
bot.add_cog(SaveMessagesCog(bot))
|
||||
bot.add_cog(HelloMessagesCog(bot))
|
||||
self.bots[user.user] = bot
|
||||
|
||||
async def run_user_bot(self, user: User):
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import httpx
|
|||
from litestar.exceptions import HTTPException
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from src.huesoporro.models import TwitchAuth
|
||||
from src.huesoporro.settings import Settings
|
||||
from huesoporro.models import TwitchAuth
|
||||
from huesoporro.settings import Settings
|
||||
|
||||
|
||||
class TwitchAuthenticator(BaseModel):
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import aiosqlite
|
|||
from loguru import logger
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from src.huesoporro.models import ChatbotSettings, Sentence, User
|
||||
from src.huesoporro.settings import Settings
|
||||
from huesoporro.models import ChatbotSettings, User
|
||||
from huesoporro.settings import Settings
|
||||
|
||||
|
||||
class Database(BaseModel):
|
||||
|
|
@ -78,22 +78,3 @@ class Database(BaseModel):
|
|||
if not result:
|
||||
return None
|
||||
return ChatbotSettings(**dict(result))
|
||||
|
||||
async def save_sentence(self, sentence: str, auto_commit=True):
|
||||
async with self.get_client(auto_commit=auto_commit) as db:
|
||||
await db.execute(
|
||||
"INSERT INTO sentences (sentence) VALUES (?)",
|
||||
(sentence,),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
async def get_sentences(self, user: User) -> list[Sentence]:
|
||||
async with self.get_client() as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
async with db.execute(
|
||||
"SELECT * FROM sentences WHERE user_id = ?", (user.user,)
|
||||
) as cursor:
|
||||
result = await cursor.fetchall()
|
||||
if not result:
|
||||
return []
|
||||
return [Sentence(user=user, **dict(value)) for value in result]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from gtts import gTTS
|
|||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.huesoporro.settings import Settings
|
||||
from huesoporro.settings import Settings
|
||||
|
||||
|
||||
class GTTS(BaseModel):
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ from typing import Generic, TypeVar
|
|||
import aiosqlite
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from src.huesoporro.models import Quote, User
|
||||
from src.huesoporro.settings import Settings
|
||||
from huesoporro.models import Quote, User
|
||||
from huesoporro.settings import Settings
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ import sqlite3
|
|||
import string
|
||||
from typing import Any
|
||||
|
||||
import platformdirs
|
||||
from loguru import logger
|
||||
|
||||
from huesoporro.settings import Settings
|
||||
|
||||
class Database:
|
||||
|
||||
class MarkovDatabase:
|
||||
"""
|
||||
The database created is called `MarkovChain_{channel}.db`,
|
||||
and populated with 27 + 27^2 = 756 tables. Firstly, 27 tables with the structure of
|
||||
|
|
@ -86,14 +87,11 @@ class Database:
|
|||
to both get results from "hello" and "hello,".
|
||||
"""
|
||||
|
||||
def __init__(self, channel: str):
|
||||
self.user_data_path = platformdirs.user_data_path(
|
||||
"huesoporro",
|
||||
ensure_exists=True,
|
||||
)
|
||||
self.db_path = (
|
||||
self.user_data_path / f"MarkovChain_{channel.replace('#', '').lower()}.db"
|
||||
)
|
||||
def __init__(self, channel: str, settings: Settings | None = None):
|
||||
settings = settings or Settings.get()
|
||||
|
||||
self.db_path = settings.default_data_path / f"MarkovChain_{channel}.db"
|
||||
self.user_data_path = self.db_path.parent
|
||||
self._execute_queue: list = []
|
||||
|
||||
if self.db_path.is_file():
|
||||
|
|
@ -357,7 +355,7 @@ class Database:
|
|||
|
||||
from nltk import ngrams
|
||||
|
||||
from src.huesoporro.libs.tokenizer import tokenize
|
||||
from huesoporro.libs.tokenizer import tokenize
|
||||
|
||||
channel = channel.replace("#", "").lower()
|
||||
copyfile(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import uvicorn
|
||||
|
||||
from src.huesoporro.settings import Settings
|
||||
from huesoporro.settings import Settings
|
||||
|
||||
if __name__ == "__main__":
|
||||
settings = Settings.get()
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from typing import Literal
|
|||
import jwt
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from src.huesoporro.settings import Settings
|
||||
from huesoporro.settings import Settings
|
||||
|
||||
|
||||
class TwitchAuth(BaseModel):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
import platformdirs
|
||||
from pydantic import Field, HttpUrl, SecretStr, field_validator
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
|
@ -8,18 +9,19 @@ from pydantic_settings import BaseSettings
|
|||
class Settings(BaseSettings):
|
||||
port: int = 8000
|
||||
host: str = "0.0.0.0" # noqa: S104
|
||||
|
||||
default_data_path: Path = platformdirs.user_data_path(
|
||||
"huesoporro",
|
||||
ensure_exists=True,
|
||||
)
|
||||
static_files_path: Path = Field(
|
||||
default_factory=lambda: Path(__file__).parent / "static"
|
||||
)
|
||||
templates_files_path: Path = Field(
|
||||
default_factory=lambda: Path(__file__).parent / "templates"
|
||||
)
|
||||
tts_cache_path: Path = Field(
|
||||
default_factory=lambda: Path(__file__).parent / "tts_files"
|
||||
)
|
||||
db_filepath: Path = Field(
|
||||
default_factory=lambda: Path(__file__).parent / "huesoporro.db"
|
||||
)
|
||||
tts_cache_path: Path = default_data_path / "tts_files"
|
||||
db_filepath: Path = default_data_path / "huesoporro.db"
|
||||
twitch_client_id: str
|
||||
twitch_client_secret: SecretStr
|
||||
jwt_secret: SecretStr
|
||||
|
|
@ -31,8 +33,8 @@ class Settings(BaseSettings):
|
|||
|
||||
@staticmethod
|
||||
@lru_cache(maxsize=1)
|
||||
def get():
|
||||
return Settings() # type: ignore[call-arg] # pydantic-setting magic
|
||||
def get(**data):
|
||||
return Settings(**data) # type: ignore[call-arg] # pydantic-setting magic
|
||||
|
||||
@field_validator("allowed_users")
|
||||
@classmethod
|
||||
|
|
|
|||
57
src/huesoporro/svc/clean_cc_svc.py
Normal file
57
src/huesoporro/svc/clean_cc_svc.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import re
|
||||
from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class CleanCCSvc(BaseModel):
|
||||
@classmethod
|
||||
def clean_vtt_line(cls, line):
|
||||
"""
|
||||
Clean a single line of VTT text by removing timestamps and HTML tags.
|
||||
|
||||
Args:
|
||||
line (str): Single line from VTT file
|
||||
|
||||
Returns:
|
||||
str: Cleaned line or None if line should be skipped
|
||||
"""
|
||||
# Skip empty lines
|
||||
if not line.strip():
|
||||
return None
|
||||
# Skip WEBVTT header
|
||||
if line.strip().startswith("WEBVTT"):
|
||||
return None
|
||||
|
||||
# Skip timestamp lines (e.g., "00:00:00.000 --> 00:00:02.000")
|
||||
if re.match(
|
||||
r"\d{2}:\d{2}:\d{2}\.\d{3}\s+-{2}>\s+\d{2}:\d{2}:\d{2}\.\d{3}", line
|
||||
):
|
||||
return None
|
||||
|
||||
# Skip numeric identifiers
|
||||
if line.strip().isdigit():
|
||||
return None
|
||||
|
||||
# Remove HTML-style tags
|
||||
line = re.sub(r"<[^>]+>", "", line)
|
||||
|
||||
# Remove multiple spaces
|
||||
line = " ".join(line.split())
|
||||
|
||||
return line.strip()
|
||||
|
||||
@classmethod
|
||||
def process_vtt_file(cls, file_path: Path):
|
||||
seen_lines = set()
|
||||
|
||||
with file_path.open("r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
cleaned = cls.clean_vtt_line(line)
|
||||
if line not in seen_lines:
|
||||
seen_lines.add(line)
|
||||
yield cleaned
|
||||
|
||||
def run(self, cc_file_path: Path) -> Generator[str, None, None]:
|
||||
return self.process_vtt_file(cc_file_path)
|
||||
44
src/huesoporro/svc/download_closed_captions.py
Normal file
44
src/huesoporro/svc/download_closed_captions.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import tempfile
|
||||
from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
|
||||
import yt_dlp
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class DownloadClosedCaptionsSvc(BaseModel):
|
||||
@staticmethod
|
||||
def run(youtube_url: str, sub_lang: str = "es") -> Generator[Path, None, None]:
|
||||
"""Download closed captions from a yt video and save it to a temp file
|
||||
|
||||
Args:
|
||||
youtube_url: URL of the YouTube video
|
||||
sub_lang: Language code for the subtitles (default: "es" for Spanish)
|
||||
|
||||
Returns:
|
||||
Path: Path to the downloaded subtitles file
|
||||
|
||||
Raises:
|
||||
ValueError: If subtitles are not available in the requested language
|
||||
"""
|
||||
temp_dir = Path(tempfile.mkdtemp())
|
||||
logger.info(f"Downloading subtitles for {youtube_url} to {temp_dir}")
|
||||
ydl_opts = {
|
||||
"skip_download": True,
|
||||
"writesubtitles": True,
|
||||
"writeautomaticsub": True,
|
||||
"subtitleslangs": [sub_lang],
|
||||
"subtitlesformat": "vtt",
|
||||
"cookiesfrombrowser": ("firefox",),
|
||||
"paths": {"home": str(temp_dir)},
|
||||
"quiet": True,
|
||||
}
|
||||
|
||||
try:
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.download([youtube_url])
|
||||
return temp_dir.glob(f"*.{sub_lang}.vtt")
|
||||
|
||||
except yt_dlp.utils.DownloadError as exc:
|
||||
raise ValueError(f"Failed to download subtitles: {exc!s}") from exc
|
||||
|
|
@ -3,8 +3,8 @@ import string
|
|||
from loguru import logger
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from src.huesoporro.libs.db import Database as MarkovDB
|
||||
from src.huesoporro.libs.tokenizer import detokenize, tokenize
|
||||
from huesoporro.libs.db import MarkovDatabase as MarkovDB
|
||||
from huesoporro.libs.tokenizer import detokenize, tokenize
|
||||
|
||||
|
||||
class SentenceGeneratorSvc(BaseModel):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
from src.huesoporro.infra.db import Database
|
||||
from src.huesoporro.models import ChatbotSettings, User
|
||||
from huesoporro.infra.db import Database
|
||||
from huesoporro.models import ChatbotSettings, User
|
||||
|
||||
|
||||
class ChatbotSettingsGetterSvc(BaseModel):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
from src.huesoporro.infra.repos import QuoteRepo
|
||||
from src.huesoporro.models import Quote
|
||||
from huesoporro.infra.repos import QuoteRepo
|
||||
from huesoporro.models import Quote
|
||||
|
||||
|
||||
class RandomQuoteGetterSvc(BaseModel):
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
from src.huesoporro.infra.db import Database
|
||||
from src.huesoporro.models import Sentence, User
|
||||
|
||||
|
||||
class SentencesGetterSvc(BaseModel):
|
||||
db: Database
|
||||
|
||||
async def run(self, user: User) -> list[Sentence]:
|
||||
return await self.db.get_sentences(user=user)
|
||||
|
|
@ -1,20 +1,35 @@
|
|||
import random
|
||||
from functools import lru_cache
|
||||
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class HelloGeneratorSvc(BaseModel):
|
||||
hellos: list[str] = Field(
|
||||
default_factory=lambda: [
|
||||
"Hola",
|
||||
"Ayo",
|
||||
"Hi",
|
||||
"Bon día",
|
||||
"Hola mi tremendo elemento",
|
||||
"HOLA",
|
||||
"hiii",
|
||||
]
|
||||
)
|
||||
hellos: list[str] = [
|
||||
"Hola",
|
||||
"Ayo",
|
||||
"Hi",
|
||||
"Bon día",
|
||||
"Hola mi tremendo elemento",
|
||||
"HOLA",
|
||||
"hiii",
|
||||
]
|
||||
|
||||
def run(self, username: str):
|
||||
return f"{random.choice(self.hellos)} @{username}" # noqa: S311
|
||||
greeted_users: dict[str, str] = Field(default_factory=dict)
|
||||
|
||||
def run(self, username: str) -> str | None:
|
||||
if username in self.greeted_users:
|
||||
logger.info(f"User {username} already greeted")
|
||||
return None
|
||||
|
||||
greeting = f"{random.choice(self.hellos)} @{username}" # noqa: S311
|
||||
|
||||
self.greeted_users[username] = greeting
|
||||
|
||||
return greeting
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_hello_generator_svc() -> HelloGeneratorSvc:
|
||||
return HelloGeneratorSvc()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
from src.huesoporro.infra.db import Database
|
||||
from src.huesoporro.models import User
|
||||
from huesoporro.infra.db import Database
|
||||
from huesoporro.models import User
|
||||
|
||||
|
||||
class IsModSvc(BaseModel):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
from src.huesoporro.infra.repos import QuoteRepo
|
||||
from src.huesoporro.models import Quote
|
||||
from huesoporro.infra.repos import QuoteRepo
|
||||
from huesoporro.models import Quote
|
||||
|
||||
|
||||
class QuoteStorerSvc(BaseModel):
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ from loguru import logger
|
|||
from nltk.tokenize import sent_tokenize
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from src.huesoporro.libs.db import Database as MarkovDB
|
||||
from src.huesoporro.libs.tokenizer import tokenize
|
||||
from huesoporro.libs.db import MarkovDatabase
|
||||
from huesoporro.libs.tokenizer import tokenize
|
||||
|
||||
|
||||
class SentenceStorerSvc(BaseModel):
|
||||
db: MarkovDB
|
||||
db: MarkovDatabase
|
||||
key_length: int = 2
|
||||
end_tag: str = "<END>"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
from src.huesoporro.infra.db import Database
|
||||
from src.huesoporro.models import ChatbotSettings, User
|
||||
from huesoporro.infra.db import Database
|
||||
from huesoporro.models import ChatbotSettings, User
|
||||
|
||||
|
||||
class ChatbotSettingsStorerSvc(BaseModel):
|
||||
|
|
|
|||
|
|
@ -185,7 +185,6 @@
|
|||
const chatbotManager = new ChatbotManager();
|
||||
chatbotManager.setEvents();
|
||||
|
||||
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
<details class="dropdown">
|
||||
<summary>Le Funny</summary>
|
||||
<ul dir="rtl">
|
||||
<li><a href="/sentences" >Sentences</a></li>
|
||||
<li><a href="#" disabled>Quotes</a></li>
|
||||
<li><a href="#" disabled>Copypastas</a></li>
|
||||
<li><a href="/quotes" disabled>Quotes</a></li>
|
||||
<li><a href="/copypastas" disabled>Copypastas</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -1,137 +0,0 @@
|
|||
{% include 'header.html' %}
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<nav class="container">
|
||||
<ul>
|
||||
<li><a href="/">Chatbot</a></li>
|
||||
<li><a href="/tts">TTS</a></li>
|
||||
{% include 'le_funny_dropdown.html' %}
|
||||
</ul>
|
||||
{% include 'logout.html' %}
|
||||
</nav>
|
||||
<script>
|
||||
function timestampToAwareDatetime(timestamp) {
|
||||
const awareDatetime = new Date(timestamp);
|
||||
awareDatetime.setMinutes(awareDatetime.getMinutes() - awareDatetime.getTimezoneOffset());
|
||||
return awareDatetime;
|
||||
}
|
||||
|
||||
function prettifyTimestamp(timestamp) {
|
||||
const awareDatetime = timestampToAwareDatetime(timestamp);
|
||||
return awareDatetime.toLocaleString();
|
||||
}
|
||||
|
||||
function saveNewSentence(shortHash) {
|
||||
/*
|
||||
Save a newly-added sentence by POST'ing against /api/v1/sentences.
|
||||
|
||||
- if response is not 200, return nothing (validation logic to be added later
|
||||
- if response is 200, retrieve save-new-${shortHash} and delete-new-${shortHas} and change their ids
|
||||
to save-${newSentenceIdRetrievedFromResponse} and delete-${newSentenceIdRetrievedFromResponse}
|
||||
assume a body like: {"id": 1, "sentence": "New sentence"}
|
||||
|
||||
*/
|
||||
const sentenceTextInput = document.getElementById(`newSentence${shortHash}`);
|
||||
const sentence = sentenceTextInput.value;
|
||||
// if sentence is empty, return
|
||||
if (!sentence) {
|
||||
return;
|
||||
}
|
||||
const sentenceData = {
|
||||
"sentence": sentence
|
||||
}
|
||||
fetch(`/api/v1/sentences`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(sentenceData)
|
||||
}).then(response => {
|
||||
response.json().then(data => {
|
||||
console.log(data);
|
||||
const saveButton = document.getElementById(`saveNew${shortHash}`);
|
||||
const deleteButton = document.getElementById(`deleteNew${shortHash}`);
|
||||
saveButton.id = `save-${data.id}`;
|
||||
deleteButton.id = `delete-${data.id}`;
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function saveExistingSentence(id) {
|
||||
// get sentence value by
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const timestampCells = document.querySelectorAll('.timestamp-cell');
|
||||
console.log(timestampCells);
|
||||
timestampCells.forEach(cell => {
|
||||
const timestampStr = cell.textContent.trim();
|
||||
const timestamp = parseFloat(timestampStr) * 1000;
|
||||
cell.textContent = prettifyTimestamp(timestamp);
|
||||
});
|
||||
|
||||
const addButton = document.getElementById('add-sentence');
|
||||
addButton.addEventListener('click', () => {
|
||||
// add a new empty row at the top of the tbody
|
||||
const sentencesTable = document.getElementById('sentencesTable');
|
||||
const newRow = sentencesTable.insertRow();
|
||||
const nowPrettyDate = prettifyTimestamp(Date.now());
|
||||
const shortHash = Math.random().toString(36).substring(2, 10);
|
||||
newRow.innerHTML = `
|
||||
<td><textarea placeholder="Enter new sentence" id="newSentence${shortHash}"></textarea></td>
|
||||
<td class="timestamp-cell">${nowPrettyDate}</td>
|
||||
<td>
|
||||
<div class="container">
|
||||
<button id="saveNew${shortHash}" style="background-color: #00c482; border-color: #00c482" onclick="saveNewSentence('${shortHash}')">
|
||||
Save
|
||||
</button>
|
||||
<button id="deleteNew${shortHash}" style="border-color: #aa0000; background-color: #aa0000" onclick="this.parentElement.parentElement.parentElement.remove()">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</header>
|
||||
<main class="container">
|
||||
<section>
|
||||
<form role="search">
|
||||
<input name="search" type="search" placeholder="Search sentence"/>
|
||||
<input type="submit" value="Search"/>
|
||||
<button id="add-sentence" class="secondary" type="button">Add</button>
|
||||
</form>
|
||||
<table id="sentencesTable">
|
||||
<thead style="background-color: white; color: black">
|
||||
<tr>
|
||||
<th scope="col">Sentence</th>
|
||||
<th scope="col">Last modified</th>
|
||||
<th scope="col">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sentencesTableBody">
|
||||
{% for sentence in sentences %}
|
||||
<tr>
|
||||
<td><textarea>{{ sentence.sentence }}</textarea></td>
|
||||
<td class="timestamp-cell">{{ sentence.last_updated_at }}</td>
|
||||
<td>
|
||||
<div class="container">
|
||||
<button id="save-{{ sentence.id }}" style="background-color: #00c482; border-color: #00c482">
|
||||
Save
|
||||
</button>
|
||||
<button id="delete-{{ sentence.id }}" style="border-color: #aa0000; background-color: #aa0000">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -7,7 +7,7 @@ from gtts import gTTS
|
|||
from litestar import WebSocket
|
||||
from loguru import logger
|
||||
|
||||
from src.huesoporro.settings import Settings
|
||||
from huesoporro.settings import Settings
|
||||
|
||||
|
||||
class TTSManager:
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ from caribou.migrate import load_migrations
|
|||
from polyfactory.factories.pydantic_factory import ModelFactory
|
||||
from polyfactory.pytest_plugin import register_fixture
|
||||
|
||||
from src.huesoporro.infra.db import Database
|
||||
from src.huesoporro.models import ChatbotSettings, Quote, User
|
||||
from src.huesoporro.settings import Settings
|
||||
from src.huesoporro.svc.backoff_service import BackoffService
|
||||
from src.huesoporro.svc.is_mod import IsModSvc
|
||||
from huesoporro.infra.db import Database
|
||||
from huesoporro.models import ChatbotSettings, Quote, User
|
||||
from huesoporro.settings import Settings
|
||||
from huesoporro.svc.backoff_service import BackoffService
|
||||
from huesoporro.svc.is_mod import IsModSvc
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import json
|
|||
|
||||
import pytest
|
||||
|
||||
from src.huesoporro.infra.repos import QuoteRepo, UserRepo
|
||||
from src.huesoporro.models import User
|
||||
from huesoporro.infra.repos import QuoteRepo, UserRepo
|
||||
from huesoporro.models import User
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import time
|
|||
|
||||
import pytest
|
||||
|
||||
from src.huesoporro.models import ChatbotSettings, User
|
||||
from src.huesoporro.svc.is_mod import IsModSvc
|
||||
from huesoporro.models import ChatbotSettings, User
|
||||
from huesoporro.svc.is_mod import IsModSvc
|
||||
|
||||
|
||||
async def test_is_mod_svc_returns_true_for_channel(is_mod_svc: IsModSvc, user: User):
|
||||
|
|
|
|||
45
uv.lock
generated
45
uv.lock
generated
|
|
@ -517,8 +517,8 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "huesoporro"
|
||||
version = "0.2.9"
|
||||
source = { virtual = "." }
|
||||
version = "0.3.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiosqlite" },
|
||||
{ name = "caribou" },
|
||||
|
|
@ -538,6 +538,10 @@ dependencies = [
|
|||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
cli = [
|
||||
{ name = "typer" },
|
||||
{ name = "yt-dlp" },
|
||||
]
|
||||
dev = [
|
||||
{ name = "mypy" },
|
||||
{ name = "polyfactory" },
|
||||
|
|
@ -567,6 +571,10 @@ requires-dist = [
|
|||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
cli = [
|
||||
{ name = "typer", specifier = ">=0.15.1" },
|
||||
{ name = "yt-dlp", specifier = ">=2025.1.26" },
|
||||
]
|
||||
dev = [
|
||||
{ name = "mypy", specifier = ">=1.13.0" },
|
||||
{ name = "polyfactory", specifier = ">=2.18.1" },
|
||||
|
|
@ -1367,6 +1375,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/63/6a/aca01554949f3a401991dc32fe22837baeaccb8a0d868256cbb26a029778/ruff-0.9.7-py3-none-win_arm64.whl", hash = "sha256:b075a700b2533feb7a01130ff656a4ec0d5f340bb540ad98759b8401c32c2037", size = 10177763 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
|
|
@ -1450,6 +1467,21 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/4b/be/49d93b0e13dad69a636e550a7b96a5208af9a91100f9b142a363882e0c4c/twitchio-2.10.0-py3-none-any.whl", hash = "sha256:7aa0b6950dad90feeb04b03fd10d3e4292fa8a7c2e7aea6b2fd6686bc5425fb2", size = 143761 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.15.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/dca7b219718afd37a0068f4f2530a727c2b74a8b6e8e0c0080a4c0de4fcd/typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a", size = 99789 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
|
|
@ -1688,3 +1720,12 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/f5/d5/688db678e987c3e0fb17867970700b92603cadf36c56e5fb08f23e822a0c/yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c", size = 315723 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yt-dlp"
|
||||
version = "2025.2.19"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/53/36/ef300ba4a228b74612d4013b43ed303a0d6d2de17a71fc37e0b821577e0a/yt_dlp-2025.2.19.tar.gz", hash = "sha256:f33ca76df2e4db31880f2fe408d44f5058d9f135015b13e50610dfbe78245bea", size = 2929199 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/45/6d1b759e68f5363b919828fb0e0c167a1cd5003b5b7c74cc0f0c2096be4f/yt_dlp-2025.2.19-py3-none-any.whl", hash = "sha256:3ed218eaeece55e9d715afd41abc450dc406ee63bf79355169dfde312d38fdb8", size = 3186543 },
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue