Init
This commit is contained in:
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
||||||
|
|
||||||
|
.env
|
||||||
|
tokens.json
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
11
pyproject.toml
Normal file
11
pyproject.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[project]
|
||||||
|
name = "twitch-chat-bot"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = [
|
||||||
|
"aiofiles>=24.1.0",
|
||||||
|
"pydantic-ai>=0.1.3",
|
||||||
|
"twitchapi>=4.4.0",
|
||||||
|
]
|
||||||
71
src/auth.py
Normal file
71
src/auth.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import aiofiles
|
||||||
|
|
||||||
|
from twitchAPI.twitch import Twitch
|
||||||
|
from twitchAPI.oauth import UserAuthenticator
|
||||||
|
from twitchAPI.type import AuthScope
|
||||||
|
|
||||||
|
|
||||||
|
APP_ID = os.environ["TWITCH_APP_ID"]
|
||||||
|
APP_SECRET = os.environ["TWITCH_APP_SECRET"]
|
||||||
|
USER_SCOPE = [AuthScope.CHAT_READ, AuthScope.CHAT_EDIT]
|
||||||
|
|
||||||
|
|
||||||
|
class TokenManager:
|
||||||
|
FILENAME = "tokens.json"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def load(cls) -> tuple[str, str] | None:
|
||||||
|
try:
|
||||||
|
async with aiofiles.open(cls.FILENAME, "r") as f:
|
||||||
|
data = await f.read()
|
||||||
|
|
||||||
|
json_data = json.loads(data)
|
||||||
|
|
||||||
|
return json_data["auth_token"], json_data["refresh_token"]
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def save(cls, auth_token: str, refresh_token: str):
|
||||||
|
async with aiofiles.open(cls.FILENAME, "w") as f:
|
||||||
|
await f.write(
|
||||||
|
json.dumps({
|
||||||
|
"auth_token": auth_token,
|
||||||
|
"refresh_token": refresh_token
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_auth_token(client: Twitch) -> tuple[str, str]:
|
||||||
|
auth = UserAuthenticator(client, USER_SCOPE)
|
||||||
|
|
||||||
|
token_data = await auth.authenticate()
|
||||||
|
if token_data is None:
|
||||||
|
raise RuntimeError("Authorization failed!")
|
||||||
|
|
||||||
|
return token_data
|
||||||
|
|
||||||
|
|
||||||
|
async def get_client() -> Twitch:
|
||||||
|
client = Twitch(APP_ID, APP_SECRET)
|
||||||
|
|
||||||
|
saved_token = await TokenManager.load()
|
||||||
|
|
||||||
|
if saved_token:
|
||||||
|
token, refresh_token = saved_token
|
||||||
|
else:
|
||||||
|
token, refresh_token = await get_auth_token(client)
|
||||||
|
await TokenManager.save(token, refresh_token)
|
||||||
|
|
||||||
|
await client.set_user_authentication(
|
||||||
|
token,
|
||||||
|
scope=USER_SCOPE,
|
||||||
|
refresh_token=refresh_token,
|
||||||
|
validate=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return client
|
||||||
53
src/chatbot.py
Normal file
53
src/chatbot.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
from asyncio import sleep
|
||||||
|
|
||||||
|
from twitchAPI.type import ChatEvent
|
||||||
|
from twitchAPI.chat import Chat, EventData, ChatMessage
|
||||||
|
|
||||||
|
from auth import get_client, Twitch
|
||||||
|
from handlers import HANDLERS
|
||||||
|
|
||||||
|
|
||||||
|
class ChatBot:
|
||||||
|
TARGET_CHANNELS = [
|
||||||
|
"kurbezz",
|
||||||
|
"kamsyll"
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, client: Twitch):
|
||||||
|
self.client = client
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def on_ready(cls, ready_event: EventData):
|
||||||
|
print("[system]: Ready!")
|
||||||
|
|
||||||
|
for channel in cls.TARGET_CHANNELS:
|
||||||
|
print(f"[system]: Subscribe to {channel}...")
|
||||||
|
await ready_event.chat.join_room(channel)
|
||||||
|
print(f"[system]: Subscribed to {channel}!")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def on_message(cls, msg: ChatMessage):
|
||||||
|
print(f"[{msg.user.name}]: {msg.text}")
|
||||||
|
|
||||||
|
for handler in HANDLERS:
|
||||||
|
await handler(msg)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def run(cls):
|
||||||
|
client = await get_client()
|
||||||
|
|
||||||
|
chat = await Chat(client)
|
||||||
|
|
||||||
|
chat.register_event(ChatEvent.READY, cls.on_ready)
|
||||||
|
chat.register_event(ChatEvent.MESSAGE, cls.on_message)
|
||||||
|
|
||||||
|
chat.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
await sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("[system]: Shutting down...")
|
||||||
|
finally:
|
||||||
|
chat.stop()
|
||||||
|
await client.close()
|
||||||
18
src/handlers/__init__.py
Normal file
18
src/handlers/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from typing import Callable, Awaitable
|
||||||
|
|
||||||
|
from twitchAPI.chat import ChatMessage
|
||||||
|
|
||||||
|
from .goida import on_goida_handler
|
||||||
|
from .lasqexx import on_lasqexx_message
|
||||||
|
from .greetings import on_greetings
|
||||||
|
from .farewells import on_farewells
|
||||||
|
from .gemini import on_gemini_handler
|
||||||
|
|
||||||
|
|
||||||
|
HANDLERS: list[Callable[[ChatMessage], Awaitable[bool]]] = [
|
||||||
|
on_goida_handler,
|
||||||
|
on_lasqexx_message,
|
||||||
|
on_greetings,
|
||||||
|
on_farewells,
|
||||||
|
on_gemini_handler,
|
||||||
|
]
|
||||||
15
src/handlers/farewells.py
Normal file
15
src/handlers/farewells.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from twitchAPI.chat import ChatMessage
|
||||||
|
|
||||||
|
|
||||||
|
TRIGGER_AND_RESPONSE: list[tuple[str, str]] = [
|
||||||
|
("пока", "Пока")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def on_farewells(msg: ChatMessage) -> bool:
|
||||||
|
for trigger, response in TRIGGER_AND_RESPONSE:
|
||||||
|
if trigger in msg.text.lower():
|
||||||
|
await msg.reply(response)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
48
src/handlers/gemini.py
Normal file
48
src/handlers/gemini.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from twitchAPI.chat import ChatMessage
|
||||||
|
|
||||||
|
from pydantic_ai import Agent
|
||||||
|
from pydantic_ai.models.gemini import GeminiModel
|
||||||
|
from pydantic_ai.providers.google_gla import GoogleGLAProvider
|
||||||
|
|
||||||
|
|
||||||
|
GEMINI_API_KEY = os.environ["GEMINI_API_KEY"]
|
||||||
|
|
||||||
|
SYSTEM_PROMPT = """
|
||||||
|
Ты чат бот на сервисе Twitch.
|
||||||
|
К тебе могут обратиться с помощью имен: Булат, курбез, kurbezz.
|
||||||
|
|
||||||
|
### Правила:
|
||||||
|
1. Отвечай на запросы пользователей
|
||||||
|
2. Не признавайся, что ты чат бот или нейросеть. Отвечай, что ты человек.
|
||||||
|
3. Не используй запрещенные слова на сервисе Twitch.
|
||||||
|
4. Используй для ответа чистый текст или смайлы. (Не markdown или html)
|
||||||
|
5. Отвечай коротко и ясно.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
model = GeminiModel(
|
||||||
|
'gemini-2.0-flash',
|
||||||
|
provider=GoogleGLAProvider(api_key=GEMINI_API_KEY)
|
||||||
|
)
|
||||||
|
|
||||||
|
agent = Agent(
|
||||||
|
model=model,
|
||||||
|
retries=5,
|
||||||
|
instrument=True,
|
||||||
|
system_prompt=SYSTEM_PROMPT
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_gemini_handler(msg: ChatMessage) -> bool:
|
||||||
|
if not msg.text.startswith("!gemini "):
|
||||||
|
return False
|
||||||
|
|
||||||
|
prompt = msg.text[8:]
|
||||||
|
|
||||||
|
result = await agent.run(prompt)
|
||||||
|
|
||||||
|
await msg.reply(result.output)
|
||||||
|
|
||||||
|
return True
|
||||||
10
src/handlers/goida.py
Normal file
10
src/handlers/goida.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from twitchAPI.chat import ChatMessage
|
||||||
|
|
||||||
|
|
||||||
|
async def on_goida_handler(message: ChatMessage) -> bool:
|
||||||
|
if "гойда" not in message.text.lower():
|
||||||
|
return False
|
||||||
|
|
||||||
|
await message.reply("ГООООООООООООООООООООООООООООООООООООЙДА!")
|
||||||
|
|
||||||
|
return True
|
||||||
16
src/handlers/greetings.py
Normal file
16
src/handlers/greetings.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from twitchAPI.chat import ChatMessage
|
||||||
|
|
||||||
|
|
||||||
|
TRIGGER_AND_RESPONSE: list[tuple[str, str]] = [
|
||||||
|
# ("ку", "Ку"),
|
||||||
|
("привет", "Привет")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def on_greetings(msg: ChatMessage) -> bool:
|
||||||
|
for trigger, response in TRIGGER_AND_RESPONSE:
|
||||||
|
if trigger in msg.text.lower():
|
||||||
|
await msg.reply(response)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
20
src/handlers/lasqexx.py
Normal file
20
src/handlers/lasqexx.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from twitchAPI.chat import ChatMessage
|
||||||
|
|
||||||
|
|
||||||
|
TRIGGER_AND_RESPONSE: list[tuple[str, str]] = [
|
||||||
|
("здароу", "Здароу, давай иди уже"),
|
||||||
|
("сосал?", "А ты? Иди уже"),
|
||||||
|
("лан я пошёл", "да да, иди уже")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def on_lasqexx_message(msg: ChatMessage):
|
||||||
|
if 'lasqexx' != msg.user.name:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for trigger, response in TRIGGER_AND_RESPONSE:
|
||||||
|
if trigger in msg.text.lower():
|
||||||
|
await msg.reply(response)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
7
src/main.py
Normal file
7
src/main.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from asyncio import run
|
||||||
|
|
||||||
|
from chatbot import ChatBot
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run(ChatBot.run())
|
||||||
Reference in New Issue
Block a user