commit 9f76abcfc4438e3c45f724a6f7e1ada1c1b57fe2 Author: Olly Hearn Date: Sun Nov 12 22:43:47 2023 +0300 not working shit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2a16037 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +**/postgres_data +config.py +*.pyc +**/__pycache__ diff --git a/bot/Dockerfile b/bot/Dockerfile new file mode 100644 index 0000000..d202b3e --- /dev/null +++ b/bot/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.11.3-alpine +WORKDIR /app +COPY requirements.txt . +RUN pip install -r requirements.txt +ADD app/ . +ENTRYPOINT ["python"] +CMD ["/app/bot.py"] diff --git a/bot/app/bot.py b/bot/app/bot.py new file mode 100644 index 0000000..c3e1749 --- /dev/null +++ b/bot/app/bot.py @@ -0,0 +1,227 @@ +# Telebot imports +from telebot.async_telebot import AsyncTeleBot +from telebot.asyncio_storage import StateMemoryStorage, StatePickleStorage +from telebot.asyncio_handler_backends import State, StatesGroup +from telebot.asyncio_filters import StateFilter +from telebot import types +from telebot.callback_data import CallbackData, CallbackDataFilter +from telebot.types import Message +from telebot.util import user_link + +# Async things imports +import asyncio + +# Other modules imports +from datetime import datetime +import math +from typing import Union + +# Local imports +from config import token +import textbook +import keyboards + +# DB +from db.base import Session, engine, Base +from db.models import User, Group, GroupMember, Fund, FundMember + + +bot = AsyncTeleBot(token, state_storage=StatePickleStorage()) +session: Session = None + + +class States(StatesGroup): + default = State() + newfund_amount = State() + + +# Utils + + +def get_fund_text(fund: Fund): + count = len(fund.fund_members) + contributors = len(fund.fund_members.filter(FundMember.contributed == True)) + personal_amount = math.ceil(fund.amount / count) + return ( + "🟢 {name}\n\n", + "💵 Сумма: {amount}р\n\n", + "Каждый скидывает по {personal_amount}\n", + "Уже собрано: {collected_amount}\n\n", + "👥 Скинули: {contributors}/{count} чел.", + ).format( + name=fund.name, + amount=fund.amount, + personal_amount=personal_amount, + collected_amount=personal_amount * contributors, + contributors=contributors, + count=count, + ) + + +@bot.message_handler(commands=["start"]) +async def start(msg: Message): + if msg.chat.type == "private": + if user := session.query(User).filter(User.id == msg.chat.id).first(): + await bot.send_message( + chat_id=msg.chat.id, + text=textbook.private_info.format(count=len(user.fund_members)), + ) + else: + user = User( + id=msg.chat.id, + name=msg.from_user.first_name, + username=msg.from_user.username, + ) + session.add(user) + session.commit() + await bot.send_message(chat_id=msg.chat.id, text=textbook.start_private) + + elif msg.chat.type in ("group", "supergroup"): + if not session.query(Group).filter(Group.id == msg.chat.id).first(): + group = Group(id=msg.chat.id) + session.add(group) + session.commit() + await bot.send_message(chat_id=msg.chat.id, text=textbook.start_group) + + +@bot.message_handler(commands=["setup"]) +async def setup(msg: Message): + if msg.chat.type in ("group", "supergroup"): + await bot.send_message( + chat_id=msg.chat.id, text=textbook.setup, reply_markup=keyboards.setup() + ) + + +@bot.callback_query_handler(func=lambda c: c.data == "register_group_member") +async def register_group_member(call: types.CallbackQuery): + new = False + if not session.query(User).filter(User.id == call.from_user.id).first(): + user = User( + id=call.from_user.id, + name=call.from_user.first_name, + username=call.from_user.username, + ) + session.add(user) + if ( + group_member := session.query(GroupMember) + .filter( + GroupMember.group_id == call.message.chat.id, + GroupMember.user_id == call.from_user.id, + ) + .first() + ): + session.delete(group_member) + else: + group_member = GroupMember( + group_id=call.message.chat.id, + user_id=call.from_user.id, + ) + session.add(group_member) + new = True + session.commit() + await bot.answer_callback_query( + callback_query_id=call.id, + text=textbook.user_parted if new else textbook.user_left, + show_alert=True, + ) + + +@bot.message_handler(commands=["newfund"]) +async def newfund(msg: Message): + if msg.chat.type in ("group", "supergroup"): + if ( + session.query(Fund) + .filter(Fund.group_id == msg.chat.id, Fund.active == True) + .first() + ): + await bot.send_message( + chat_id=msg.chat.id, + text=textbook.newfund_already_exists, + ) + else: + await bot.set_state( + user_id=msg.from_user.id, + chat_id=msg.chat.id, + state=States.newfund_amount, + ) + await bot.send_message( + chat_id=msg.chat.id, + text=textbook.newfund_amount.format(user=user_link(msg.from_user)), + reply_markup=keyboards.cancel(), + parse_mode="html", + ) + + +@bot.callback_query_handler( + func=lambda c: c.data == "cancel", state=States.newfund_amount +) +async def cancel_newfund_amount(call: types.CallbackQuery): + await bot.set_state( + user_id=call.from_user.id, chat_id=call.message.chat.id, state=States.default + ) + await bot.answer_callback_query( + callback_query_id=call.id, + text=textbook.cancel, + ) + + +@bot.message_handler(func=lambda message: True) +async def newfund_amount(msg: Message): + await bot.send_message(chat_id=msg.chat.id, text="test") + if msg.text.isdigit(): + await bot.set_state( + user_id=msg.from_user.id, chat_id=msg.chat.id, state=States.default + ) + fund = Fund( + owner_id=msg.from_user.id, group_id=msg.chat.id, amount=int(msg.text) + ) + session.add(fund) + for group_member in session.query(GroupMember).filter( + GroupMember.group_id == msg.chat.id + ): + fund_member = FundMember(user_id=group_member.user.id, fund_id=fund.id) + session.add(fund_member) + session.commit() + await bot.send_message( + chat_id=msg.chat.id, + text=textbook.fund_created.format(fund=fund.name), + ) + await asyncio.sleep(1) + await bot.send_message( + chat_id=msg.chat.id, + text=get_fund_text(fund), + reply_markup=keyboards.fund_markup(), + ) + + else: + await bot.send_message( + chat_id=msg.chat.id, + text=textbook.not_number.format(user=user_link(msg.from_user)), + reply_markup=keyboards.cancel(), + parse_mode="html", + ) + + +@bot.message_handler(commands=["mystate"]) +async def mystate(msg: Message): + state = await bot.get_state(user_id=msg.from_user.id) + await bot.send_message(chat_id=msg.chat.id, text=state) + + +@bot.message_handler(commands=["chatid"]) +async def chatid(msg: Message): + await bot.send_message(chat_id=msg.chat.id, text=msg.chat.id) + + +async def main(): + a = asyncio.create_task(bot.polling(non_stop=True)) + await a + + +if __name__ == "__main__": + print("Bot started", flush=True) + Base.metadata.create_all(engine) + session = Session() + bot.add_custom_filter(StateFilter(bot)) + bot.enable_saving_states(filename="./.state-save/states.pkl") + asyncio.run(main()) diff --git a/bot/app/db/base.py b/bot/app/db/base.py new file mode 100644 index 0000000..817e2d3 --- /dev/null +++ b/bot/app/db/base.py @@ -0,0 +1,11 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.exc import OperationalError +from sqlalchemy.orm import sessionmaker +from db.settings import Settings + +engine = create_engine(Settings().uri) + +Session = sessionmaker(bind=engine) + +Base = declarative_base() diff --git a/bot/app/db/models.py b/bot/app/db/models.py new file mode 100644 index 0000000..4abea4c --- /dev/null +++ b/bot/app/db/models.py @@ -0,0 +1,64 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import Column, Integer, String, BigInteger, ForeignKey, Boolean +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship, backref + +import uuid + +from db.base import Base +from datetime import date + + +class User(Base): + __tablename__ = "user" + + id = Column(BigInteger, primary_key=True) + name = Column(String(64)) + username = Column(String(32)) + + owns_funds = relationship("Fund", backref="owner") + fund_members = relationship("FundMember", backref="user") + group_members = relationship("GroupMember", backref="user") + + +class Group(Base): + __tablename__ = "group" + + id = Column(BigInteger, primary_key=True) + + funds = relationship("Fund", backref="group") + + group_members = relationship("GroupMember", backref="group") + + +class GroupMember(Base): + __tablename__ = "groupmember" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + user_id = Column(BigInteger, ForeignKey("user.id")) + group_id = Column(BigInteger, ForeignKey("group.id")) + + +class Fund(Base): + __tablename__ = "fund" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + name = Column( + String(40), default="Сбор {date}".format(date=date.today().strftime("%d.%m")) + ) + description = Column(String(120), default=None) + owner_id = Column(BigInteger, ForeignKey("user.id")) + group_id = Column(BigInteger, ForeignKey("group.id")) + amount = Column(Integer) + active = Column(Boolean, default=True) + + users = relationship("FundMember", backref="fund") + + +class FundMember(Base): + __tablename__ = "fundmember" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + user_id = Column(BigInteger, ForeignKey("user.id")) + fund_id = Column(UUID(as_uuid=True), ForeignKey("fund.id")) + contributed = Column(Boolean, default=False) diff --git a/bot/app/db/settings.py b/bot/app/db/settings.py new file mode 100644 index 0000000..b9d4ae2 --- /dev/null +++ b/bot/app/db/settings.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass +import os + + +@dataclass +class Settings: + dialect: str = os.getenv("DIALECT", "postgresql") + driver: str = os.getenv("DRIVER", "psycopg2") + user: str = os.getenv("USER", "user") + password: str = os.getenv("PASSWORD", "password") + db_name: str = os.getenv("DB_NAME", "db") + host: str = os.getenv("HOST", "postgres") + port: int = os.getenv("PORT", 5432) + + @property + def uri(self) -> str: + return f"{self.dialect}+{self.driver}://{self.user}:{self.password}@{self.host}:{self.port}/{self.db_name}" diff --git a/bot/app/keyboards.py b/bot/app/keyboards.py new file mode 100644 index 0000000..190e9e6 --- /dev/null +++ b/bot/app/keyboards.py @@ -0,0 +1,28 @@ +from telebot.types import ( + InlineKeyboardButton as button, + InlineKeyboardMarkup as keyboard, +) + + +def setup() -> keyboard: + return keyboard( + keyboard=[ + [button(text="💸 Стать участником", callback_data="register_group_member")], + ] + ) + + +def cancel() -> keyboard: + return keyboard( + keyboard=[ + [button(text="❌ Отменить", callback_data="cancel")], + ] + ) + + +def fund_markup() -> keyboard: + return keyboard( + keyboard=[ + [button(text="🏁 Завершить сбор", callback_data="close_fund")], + ] + ) diff --git a/bot/app/textbook.py b/bot/app/textbook.py new file mode 100644 index 0000000..8774c1b --- /dev/null +++ b/bot/app/textbook.py @@ -0,0 +1,17 @@ +start_private = "Привет, спасибо, что активировали меня в личных сообщениях! Теперь я смогу уведомлять вас о сборах, в которых вы забыли принять участие!" +private_info = "Вы принимаете участие в {count} сборах!" +start_group = "Всем привет, я @waterfundbot! Я помогу собрать деньги на что угодно, уведомлю каждого о сборе в чате, напомню не скинувшим, и многое другое!\n\n Для начала админу чата необходимо прописать /setup. Появится сообщение, под которым будет кнопка - ее необходимо нажать всем, кто планирует участвовать в сборах в этом чате.\n\nНачать новый сбор /newfund\n\nТакже попрошу всех участвующих в сборах начать со мной диалог в личном чате, чтобы я мог уведомлять вас лично." + +setup = "Все, кто планирует участвовать в сборах в этом чате, должны нажать на кнопочку ниже. Если вы передумали - нажмите еще раз, и вы откажетесь от участия." +user_parted = "Вы приняли участие в сборах в этом чате!" +user_left = "Вы отказались от участия в сборах в этом чате" + +newfund_already_exists = "Предыдущий сбор все еще активен! Пропишите /fund, чтобы показать его, и завершите его, если необходимо создать новый!" +newfund_amount = ( + 'Отлично, новый сбор. {user}, напишите сумму сбора, или кнопку "❌ Отменить"' +) +cancel = "Хорошо, проехали" +not_number = 'Вы ввели не число. {user}, напишите сумму сбора, или кнопку "❌ Отменить"' +fund_created = "Создан новый сбор: {fund}" + +fund_not_found = "На данный момент в этом чате сборов нет! Создать новый - /newfund" diff --git a/bot/requirements.txt b/bot/requirements.txt new file mode 100644 index 0000000..7c75ce6 --- /dev/null +++ b/bot/requirements.txt @@ -0,0 +1,7 @@ +pytelegrambotapi +asyncio +aiohttp +psycopg-binary +pydantic +sqlalchemy +psycopg2-binary diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e1e8130 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +services: + postgres: + image: postgres:15.1 + volumes: + - ./postgres_data:/var/lib/postgresql/data/:rw + environment: + POSTGRES_USER: user + POSTGRES_PASSWORD: password + POSTGRES_DB: db + healthcheck: + test: ["CMD-SHELL", "pg_isready -d db --user user"] + interval: 2s + timeout: 2s + retries: 5 + bot: + build: + context: bot + environment: + DIALECT: postgresql + DRIVER: psycopg2 + USER: user + PASSWORD: password + DB_NAME: db + HOST: postgres + PORT: 5432 + depends_on: + postgres: + condition: service_healthy