This commit is contained in:
Andrey Gumirov
2022-04-29 20:11:54 +07:00
commit 911c1f3400
15 changed files with 351 additions and 0 deletions

3
.env Normal file
View File

@ -0,0 +1,3 @@
PG_USER=pg
PG_PASS=pg
PG_OUTBOUND_PORT=5432

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*__pycache__*

2
blueprints/__init__.py Normal file
View File

@ -0,0 +1,2 @@
from .menu import menu_router
from .test import test_router

17
blueprints/menu.py Normal file
View 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
View 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
View 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
View File

@ -0,0 +1 @@
from .db import DB, Candidate, TestResult

74
db/db.py Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
from .user_data_middleware import UserMiddleware

View 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
View File

@ -0,0 +1 @@
from .singleton import Singleton

26
util/singleton.py Normal file
View 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]