forked from gav/es-bot
Initial
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*__pycache__*
|
2
blueprints/__init__.py
Normal file
2
blueprints/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from .menu import menu_router
|
||||||
|
from .test import test_router
|
17
blueprints/menu.py
Normal file
17
blueprints/menu.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from vkwave.bots import DefaultRouter, SimpleBotEvent, simple_bot_message_handler
|
||||||
|
import locales
|
||||||
|
|
||||||
|
# MENU_KB.add_row()
|
||||||
|
# MENU_KB.add_text_button(text="Профиль", payload={"command": "profile"}, color=ButtonColor.SECONDARY)
|
||||||
|
# MENU_KB.add_row()
|
||||||
|
# MENU_KB.add_text_button(text="Бонус", payload={"command": "bonus"}, color=ButtonColor.POSITIVE)
|
||||||
|
|
||||||
|
menu_router = DefaultRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@simple_bot_message_handler(menu_router,)
|
||||||
|
async def menu(event: SimpleBotEvent):
|
||||||
|
return await event.answer(
|
||||||
|
message=f"Привет!",
|
||||||
|
keyboard=locales.MENU_KB.get_keyboard(),
|
||||||
|
)
|
87
blueprints/test.py
Normal file
87
blueprints/test.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
|
|
||||||
|
from vkwave.bots import DefaultRouter, SimpleBotEvent, simple_bot_message_handler, PayloadFilter, PayloadContainsFilter
|
||||||
|
from vkwave.bots import Keyboard, ButtonColor
|
||||||
|
from vkwave.bots import EventTypeFilter, BotEvent
|
||||||
|
from vkwave.types.bot_events import BotEventType
|
||||||
|
from vkwave.bots.fsm import FiniteStateMachine, StateFilter, ForWhat, State, ANY_STATE
|
||||||
|
|
||||||
|
import locales
|
||||||
|
from db import DB
|
||||||
|
from db.db import TestResult
|
||||||
|
from locales import INPUT_NAME_TEXT
|
||||||
|
|
||||||
|
|
||||||
|
# MENU_KB.add_row()
|
||||||
|
# MENU_KB.add_text_button(text="Профиль", payload={"command": "profile"}, color=ButtonColor.SECONDARY)
|
||||||
|
# MENU_KB.add_row()
|
||||||
|
# MENU_KB.add_text_button(text="Бонус", payload={"command": "bonus"}, color=ButtonColor.POSITIVE)
|
||||||
|
|
||||||
|
test_router = DefaultRouter()
|
||||||
|
|
||||||
|
test_router.registrar.add_default_filter(
|
||||||
|
EventTypeFilter(BotEventType.MESSAGE_NEW.value)) # we don't want to write it in all handlers.
|
||||||
|
|
||||||
|
|
||||||
|
# # exiting from poll (works on any state)
|
||||||
|
# @test_router.registrar.with_decorator(
|
||||||
|
# lambda event: event.object.object.message.text == "exit",
|
||||||
|
# StateFilter(fsm=fsm, state=ANY_STATE, for_what=ForWhat.FOR_USER)
|
||||||
|
# )
|
||||||
|
# async def simple_handler(event: BotEvent):
|
||||||
|
# # Check if we have the user in database
|
||||||
|
# if await fsm.get_data(event, for_what=ForWhat.FOR_USER) is not None:
|
||||||
|
# await fsm.finish(event=event, for_what=ForWhat.FOR_USER)
|
||||||
|
# return "You are quited!"
|
||||||
|
|
||||||
|
@test_router.registrar.with_decorator(
|
||||||
|
PayloadContainsFilter("test"),# for state in States.questions[:-1]]
|
||||||
|
)
|
||||||
|
async def main_part_handle(event: BotEvent):
|
||||||
|
user_id = event.object.object.message.from_id
|
||||||
|
payload = json.loads(event.object.object.message.payload)
|
||||||
|
|
||||||
|
botevent = SimpleBotEvent(event)
|
||||||
|
state_idx = int(payload["test"])
|
||||||
|
logging.debug(f"State index: {state_idx}")
|
||||||
|
|
||||||
|
|
||||||
|
q_res = payload['q'] if 'q' in payload else None
|
||||||
|
logging.debug(f"Qres: {q_res}")
|
||||||
|
|
||||||
|
# extra_state_data works as fsm.add_data(..., state_data={"name": event.object.object.message.text})
|
||||||
|
logging.debug(f"Got text: {event.object.object.message.text}")
|
||||||
|
|
||||||
|
if q_res:
|
||||||
|
DB().update_test_result(user_id, question=state_idx, answer=q_res)
|
||||||
|
|
||||||
|
if state_idx + 1 < len(locales.questions):
|
||||||
|
return await botevent.answer(
|
||||||
|
message=locales.questions[state_idx + 1][0],
|
||||||
|
keyboard=locales.questions[state_idx + 1][1].get_keyboard(),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return await botevent.answer(
|
||||||
|
message=locales.LAST_MESSAGE,
|
||||||
|
keyboard=locales.LAST_MESSAGE_KB.get_keyboard(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# @test_router.registrar.with_decorator(
|
||||||
|
# StateFilter(fsm=fsm, state=MyState.age, for_what=ForWhat.FOR_USER),
|
||||||
|
# )
|
||||||
|
# async def simple_handler(event: BotEvent):
|
||||||
|
# if not event.object.object.message.text.isdigit():
|
||||||
|
# return f"Please, send only positive numbers!"
|
||||||
|
# await fsm.add_data(
|
||||||
|
# event=event,
|
||||||
|
# for_what=ForWhat.FOR_USER,
|
||||||
|
# state_data={"age": event.object.object.message.text},
|
||||||
|
# )
|
||||||
|
# user_data = await fsm.get_data(event=event, for_what=ForWhat.FOR_USER)
|
||||||
|
#
|
||||||
|
# # finish poll and delete the user
|
||||||
|
# # `fsm.finish` will do it
|
||||||
|
# await fsm.finish(event=event, for_what=ForWhat.FOR_USER)
|
||||||
|
# return f"Your data - {user_data}"
|
11
config.py
Normal file
11
config.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import os
|
||||||
|
from util import Singleton
|
||||||
|
|
||||||
|
|
||||||
|
class Config(metaclass=Singleton):
|
||||||
|
TOKEN = os.environ["TOKEN"]
|
||||||
|
GROUP_ID = int(os.environ["GROUP_ID"])
|
||||||
|
|
||||||
|
PG_USER = os.environ["USER"]
|
||||||
|
PG_PASS = os.environ["PASS"]
|
||||||
|
PG_ADDR = os.environ["DB_ADDR"]
|
1
db/__init__.py
Normal file
1
db/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .db import DB, Candidate, TestResult
|
74
db/db.py
Normal file
74
db/db.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
from sqlalchemy import Column, DateTime, String, Integer, func, ForeignKey
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker, relationship
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import locales
|
||||||
|
from config import Config
|
||||||
|
from util import Singleton
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
|
class Candidate(Base):
|
||||||
|
__tablename__ = 'candidates'
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
first_interaction = Column(DateTime, default=func.now())
|
||||||
|
name = Column(String)
|
||||||
|
last_name = Column(String)
|
||||||
|
|
||||||
|
sex = Column(String)
|
||||||
|
|
||||||
|
test_result = relationship("TestResult", back_populates="candidate")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Candidate(id={self.id} first_interaction={self.first_interaction} name={self.name})"
|
||||||
|
|
||||||
|
|
||||||
|
class TestResult(Base):
|
||||||
|
__tablename__ = "test_results"
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
candidate_id = Column(Integer, ForeignKey('candidates.id'))
|
||||||
|
candidate = relationship("Candidate", back_populates="test_result")
|
||||||
|
|
||||||
|
answers = relationship("QuestionAnswer", back_populates="user", cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionAnswer(Base):
|
||||||
|
__tablename__ = "test_answers"
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
|
||||||
|
test_result_id = Column(Integer, ForeignKey("test_results.id"), nullable=False)
|
||||||
|
user = relationship("TestResult", back_populates="answers")
|
||||||
|
|
||||||
|
question = Column(Integer)
|
||||||
|
answer = Column(String)
|
||||||
|
|
||||||
|
|
||||||
|
class DB(metaclass=Singleton):
|
||||||
|
def __init__(self):
|
||||||
|
self._engine = create_engine(f'postgresql+psycopg2://{Config.PG_USER}:{Config.PG_PASS}@{Config.PG_ADDR}/db')
|
||||||
|
self._Session = sessionmaker()
|
||||||
|
self._Session.configure(bind=self._engine)
|
||||||
|
self._session = self._Session()
|
||||||
|
Base.metadata.create_all(self._engine)
|
||||||
|
logging.debug("Db connection succeeded!")
|
||||||
|
|
||||||
|
def get_user(self, user_id: int) -> Candidate:
|
||||||
|
return self._session.query(Candidate).filter(Candidate.id == user_id).first()
|
||||||
|
|
||||||
|
def set_name(self, user_id: int, name: str):
|
||||||
|
self._session.query(Candidate).filter(Candidate.id == user_id).update({Candidate.name: name})
|
||||||
|
self._session.commit()
|
||||||
|
|
||||||
|
def update_test_result(self, user_id: int, question: int, answer: str):
|
||||||
|
user = self._session.query(Candidate).filter(Candidate.id == user_id).first()
|
||||||
|
user.test_result[0].answers.append(QuestionAnswer(question=question, answer=answer))
|
||||||
|
self._session.commit()
|
||||||
|
|
||||||
|
def add_candidate(self, candidate: Candidate):
|
||||||
|
tres = TestResult(answers=[])
|
||||||
|
candidate.test_result = [tres]
|
||||||
|
self._session.add(candidate)
|
||||||
|
self._session.commit()
|
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
version: "2"
|
||||||
|
services:
|
||||||
|
pgdb:
|
||||||
|
image: 'postgres:12'
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=${PG_USER}
|
||||||
|
- POSTGRES_PASSWORD=${PG_PASS}
|
||||||
|
- POSTGRES_DB=db
|
||||||
|
- PGDATA=/var/lib/postgresql/data/pgdata
|
||||||
|
# volumes:
|
||||||
|
# - ${PG_MNT}:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- ${PG_OUTBOUND_PORT}:5432
|
63
locales.py
Normal file
63
locales.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
from vkwave.bots import Keyboard, ButtonColor
|
||||||
|
|
||||||
|
# menu
|
||||||
|
MENU_KB = Keyboard()
|
||||||
|
MENU_KB.add_text_button(text="Пройти тест!", payload={"test": "-1"}, color=ButtonColor.POSITIVE)
|
||||||
|
|
||||||
|
# TEST Questions
|
||||||
|
INPUT_NAME_TEXT = "Пожалуйста, введите имя:"
|
||||||
|
|
||||||
|
# 1
|
||||||
|
WHAT_ENGINEER_ARE_YOU = "Кто ты из инженеров?"
|
||||||
|
WHAT_ENGINEER_ARE_YOU_KB = Keyboard()
|
||||||
|
WHAT_ENGINEER_ARE_YOU_KB.add_text_button(text="Маск", payload={"q": "Маск", "test": "0"}, color=ButtonColor.PRIMARY)
|
||||||
|
WHAT_ENGINEER_ARE_YOU_KB.add_text_button(text="Рогозин", payload={"q": "Рогозин", "test": "0"}, color=ButtonColor.PRIMARY)
|
||||||
|
WHAT_ENGINEER_ARE_YOU_KB.add_text_button(text="Тесла", payload={"q": "Тесла", "test": "0"}, color=ButtonColor.PRIMARY)
|
||||||
|
WHAT_ENGINEER_ARE_YOU_KB.add_row()
|
||||||
|
WHAT_ENGINEER_ARE_YOU_KB.add_text_button(text="Кулибин", payload={"q": "Кулибин", "test": "0"}, color=ButtonColor.PRIMARY)
|
||||||
|
WHAT_ENGINEER_ARE_YOU_KB.add_text_button(text="Калашников", payload={"q": "Калашников", "test": "0"}, color=ButtonColor.PRIMARY)
|
||||||
|
WHAT_ENGINEER_ARE_YOU_KB.add_text_button(text="Кондратюк", payload={"q": "Кондратюк", "test": "0"}, color=ButtonColor.PRIMARY)
|
||||||
|
|
||||||
|
# 2
|
||||||
|
PROG_LANG = "Какой ЯП нравится?"
|
||||||
|
PROG_LANG_KB = Keyboard()
|
||||||
|
PROG_LANG_KB.add_text_button(text="Python", payload={"q": "Python", "test": "1"}, color=ButtonColor.PRIMARY)
|
||||||
|
PROG_LANG_KB.add_text_button(text="Pascal", payload={"q": "Pascal", "test": "1"}, color=ButtonColor.PRIMARY)
|
||||||
|
PROG_LANG_KB.add_text_button(text="C/C++", payload={"q": "ccpp", "test": "1"}, color=ButtonColor.PRIMARY)
|
||||||
|
PROG_LANG_KB.add_row()
|
||||||
|
PROG_LANG_KB.add_text_button(text="JS", payload={"q": "JS", "test": "1"}, color=ButtonColor.PRIMARY)
|
||||||
|
PROG_LANG_KB.add_text_button(text="HTML+CSS", payload={"q": "HTMLCSS", "test": "1"}, color=ButtonColor.PRIMARY)
|
||||||
|
PROG_LANG_KB.add_text_button(text="Haskel", payload={"q": "Haskel", "test": "1"}, color=ButtonColor.PRIMARY)
|
||||||
|
|
||||||
|
# 3
|
||||||
|
FAV_THEME = "Какой предмет нравится?"
|
||||||
|
FAV_THEME_KB = Keyboard()
|
||||||
|
FAV_THEME_KB.add_text_button(text="Матеша", payload={"q": "Матеша", "test": "2"}, color=ButtonColor.PRIMARY)
|
||||||
|
FAV_THEME_KB.add_text_button(text="Русский/Литра", payload={"q": "русскийлитра", "test": "2"}, color=ButtonColor.PRIMARY)
|
||||||
|
FAV_THEME_KB.add_text_button(text="Инфа", payload={"q": "Инфа", "test": "0"}, color=ButtonColor.PRIMARY)
|
||||||
|
FAV_THEME_KB.add_row()
|
||||||
|
FAV_THEME_KB.add_text_button(text="Физика", payload={"q": "Физика", "test": "2"}, color=ButtonColor.PRIMARY)
|
||||||
|
FAV_THEME_KB.add_text_button(text="другое", payload={"q": "other", "test": "2"}, color=ButtonColor.PRIMARY)
|
||||||
|
|
||||||
|
# 4
|
||||||
|
EGE = "Как готовился к ЕГЭ?"
|
||||||
|
EGE_KB = Keyboard()
|
||||||
|
EGE_KB.add_text_button(text="В школе", payload={"q": "школа", "test": "3"}, color=ButtonColor.PRIMARY)
|
||||||
|
EGE_KB.add_text_button(text="online", payload={"q": "online", "test": "3"}, color=ButtonColor.PRIMARY)
|
||||||
|
EGE_KB.add_text_button(text="репетитор", payload={"q": "репетитор", "test": "3"}, color=ButtonColor.PRIMARY)
|
||||||
|
EGE_KB.add_row()
|
||||||
|
EGE_KB.add_text_button(text="Сам", payload={"q": "Сам", "test": "3"}, color=ButtonColor.PRIMARY)
|
||||||
|
EGE_KB.add_text_button(text="wtf?", payload={"q": "wtf", "test": "3"}, color=ButtonColor.PRIMARY)
|
||||||
|
|
||||||
|
# last
|
||||||
|
LAST_MESSAGE = "Спасибо, что прошел тест!"
|
||||||
|
LAST_MESSAGE_KB = Keyboard()
|
||||||
|
LAST_MESSAGE_KB.add_text_button(text="Вернуться на главную", payload={}, color=ButtonColor.POSITIVE)
|
||||||
|
|
||||||
|
|
||||||
|
questions = [
|
||||||
|
(WHAT_ENGINEER_ARE_YOU, WHAT_ENGINEER_ARE_YOU_KB),
|
||||||
|
(PROG_LANG, PROG_LANG_KB),
|
||||||
|
(FAV_THEME, FAV_THEME_KB),
|
||||||
|
(EGE, EGE_KB),
|
||||||
|
]
|
26
main.py
Normal file
26
main.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from vkwave.bots import SimpleLongPollBot
|
||||||
|
|
||||||
|
from blueprints import (
|
||||||
|
menu_router, test_router,
|
||||||
|
)
|
||||||
|
from config import Config
|
||||||
|
from middlewares import UserMiddleware
|
||||||
|
|
||||||
|
logging.basicConfig(level="DEBUG")
|
||||||
|
|
||||||
|
bot = SimpleLongPollBot(Config.TOKEN, group_id=Config.GROUP_ID)
|
||||||
|
|
||||||
|
bot.middleware_manager.add_middleware(UserMiddleware())
|
||||||
|
|
||||||
|
|
||||||
|
bot.dispatcher.add_router(test_router)
|
||||||
|
# bot.dispatcher.add_router(games_router)
|
||||||
|
# bot.dispatcher.add_router(coin_flip_router)
|
||||||
|
# bot.dispatcher.add_router(bonus_router)
|
||||||
|
|
||||||
|
# регаем последним чтобы сначала проверялись все остальные команды
|
||||||
|
bot.dispatcher.add_router(menu_router)
|
||||||
|
|
||||||
|
bot.run_forever()
|
1
middlewares/__init__.py
Normal file
1
middlewares/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .user_data_middleware import UserMiddleware
|
24
middlewares/user_data_middleware.py
Normal file
24
middlewares/user_data_middleware.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from vkwave.bots import BaseMiddleware, BotEvent, MiddlewareResult, SimpleBotEvent
|
||||||
|
|
||||||
|
from db import DB, Candidate
|
||||||
|
|
||||||
|
|
||||||
|
class UserMiddleware(BaseMiddleware):
|
||||||
|
async def pre_process_event(self, event: BotEvent) -> MiddlewareResult:
|
||||||
|
db = DB()
|
||||||
|
botevent = SimpleBotEvent(event)
|
||||||
|
user_id = event.object.object.message.from_id
|
||||||
|
|
||||||
|
user = db.get_user(user_id)
|
||||||
|
if not user:
|
||||||
|
user_info = await botevent.get_user()
|
||||||
|
logging.debug(f"Got user info: {user_info}")
|
||||||
|
user = Candidate(id=user_id, sex=user_info.sex, name=user_info.first_name, last_name=user_info.last_name)
|
||||||
|
db.add_candidate(user)
|
||||||
|
logging.debug(f"Created new user: {user}")
|
||||||
|
print(f"Found user: {user}")
|
||||||
|
|
||||||
|
event["current_user"] = user
|
||||||
|
return MiddlewareResult(True)
|
1
util/__init__.py
Normal file
1
util/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .singleton import Singleton
|
26
util/singleton.py
Normal file
26
util/singleton.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
class Singleton(type):
|
||||||
|
_instances = {}
|
||||||
|
|
||||||
|
def __call__(cls, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Test:
|
||||||
|
>>> flag = False
|
||||||
|
>>> class A(metaclass=Singleton):
|
||||||
|
... def __init__(self):
|
||||||
|
... global flag
|
||||||
|
... assert not flag
|
||||||
|
... flag = True
|
||||||
|
...
|
||||||
|
>>> class B(metaclass=Singleton): pass
|
||||||
|
...
|
||||||
|
>>> a = A();b = B();a1 = A();b1 = B()
|
||||||
|
>>> id(a) == id(a1) and id(b) == id(b1) and id(a) != id(b)
|
||||||
|
True
|
||||||
|
|
||||||
|
:param args:
|
||||||
|
:param kwargs:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if cls not in cls._instances:
|
||||||
|
cls._instances[cls] = super().__call__(*args, **kwargs)
|
||||||
|
return cls._instances[cls]
|
Reference in New Issue
Block a user