Compare commits

1 Commits
main ... dev

Author SHA1 Message Date
fec9ce0bca not working shit 2023-11-11 15:15:03 +03:00
17 changed files with 482 additions and 47 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
config.py config.py
*.pyc *.pyc
**/__pycache__ **/__pycache__
**/.state-save

0
bot/app/__init__.py Normal file
View File

105
bot/app/alembic.ini Normal file
View File

@ -0,0 +1,105 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = alembic
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python-dateutil library that can be
# installed by adding `alembic[tz]` to the pip requirements
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to alembic/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = postgresql://user:password@postgres:5432/db
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

1
bot/app/alembic/README Normal file
View File

@ -0,0 +1 @@
Generic single-database configuration.

84
bot/app/alembic/env.py Normal file
View File

@ -0,0 +1,84 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
import sys
from os.path import abspath, dirname
sys.path.insert(0, '/app')
from db.base import Base
from db.models import *
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,30 @@
"""add active
Revision ID: 9d20a4ff1eab
Revises:
Create Date: 2023-10-29 12:09:17.958616
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9d20a4ff1eab'
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('queue', sa.Column('active', sa.Boolean(), nullable=True))
op.add_column('queueuser', sa.Column('active', sa.Boolean(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('queueuser', 'active')
op.drop_column('queue', 'active')
# ### end Alembic commands ###

View File

@ -17,10 +17,11 @@ from datetime import datetime
import math import math
import socket import socket
import os import os
import json
from typing import Union from typing import Union
# Local imports # Local imports
from config import token from config import token, admins
from constants import ( from constants import (
MAX_QUEUES_OWN, MAX_QUEUES_OWN,
MAX_QUEUES_PARTS_IN, MAX_QUEUES_PARTS_IN,
@ -29,10 +30,12 @@ from constants import (
) )
import textbook import textbook
import keyboards import keyboards
from pagination import PaginatedList
# DB # DB
from db.base import Session, engine, Base from db.base import Session, engine, Base
from db.models import User, Queue, QueueUser from db.models import User, Queue, QueueUser
from sqlalchemy import select
bot = AsyncTeleBot(token, state_storage=StatePickleStorage()) bot = AsyncTeleBot(token, state_storage=StatePickleStorage())
@ -42,13 +45,17 @@ class States(StatesGroup):
default = State() default = State()
changing_name = State() changing_name = State()
changing_queue_name = State() changing_queue_name = State()
admin_broadcasting = State()
# Utils # Utils
def get_queue_stats_text(queue: Queue) -> str: def get_queue_stats_text(queue: Queue) -> str:
s = textbook.queue_stats.format(name=queue.name, count=len(queue.users)) queue_users = queue.users.filter_by(active=True)
s = textbook.queue_stats.format(
name=queue.name, count=len(queue_users)
)
return s return s
@ -62,7 +69,7 @@ async def get_queue_from_state_data(call: types.CallbackQuery) -> Queue:
callback_query_id=call.id, text=textbook.queue_operational_error callback_query_id=call.id, text=textbook.queue_operational_error
) )
return None return None
queue = session.query(Queue).filter_by(id=queue_id).first() queue = session.query(Queue).filter_by(id=queue_id, active=True).first()
if queue.owner.id != call.from_user.id: if queue.owner.id != call.from_user.id:
await bot.answer_callback_query( await bot.answer_callback_query(
callback_query_id=call.id, text=textbook.queue_operational_error callback_query_id=call.id, text=textbook.queue_operational_error
@ -95,7 +102,7 @@ def get_first_queue_user(queue: Queue) -> Union[QueueUser, None]:
def kick_first(queue: Queue) -> bool: def kick_first(queue: Queue) -> bool:
if len(queue.users): if len(queue.users):
first_user = get_first_queue_user(queue) first_user = get_first_queue_user(queue)
session.delete(first_user) setattr(first_user, "active", False)
session.commit() session.commit()
normalize_queue(queue) normalize_queue(queue)
return True return True
@ -118,15 +125,19 @@ def normalize_queue(queue: Queue) -> Queue:
# setattr(first_user, "position", 0) # setattr(first_user, "position", 0)
# session.commit() # session.commit()
# queue = session.query(Queue).filter_by(id=queue.id).first() # queue = session.query(Queue).filter_by(id=queue.id).first()
for i, qu in enumerate(sorted(queue.users, key=lambda qu: qu.position)): queue_users = queue.users.filter_by(active= True)
setattr(qu, "position", i) for i, qu in enumerate(sorted(queue_users, key=lambda qu: qu.position)):
# setattr(qu, "position", i)
pass
session.commit() session.commit()
return queue return queue
async def update_queue_users_message(msg: Message, queue: Queue): async def update_queue_users_message(msg: Message, queue: Queue):
queue = normalize_queue(queue) queue = normalize_queue(queue)
users_str = "\n".join([f"{qu.position}. {qu.user.name}" for qu in queue.users]) filtered_users = list(filter(lambda qu: qu.active, queue.users))
users = sorted(filtered_users, key=lambda qu: qu.position)
users_str = "\n".join([f"{qu.position}. {qu.user.name}" for qu in users])
await bot.edit_message_text( await bot.edit_message_text(
chat_id=msg.chat.id, chat_id=msg.chat.id,
message_id=msg.id, message_id=msg.id,
@ -156,15 +167,19 @@ async def start(msg: Message):
await bot.set_state(user_id=msg.from_user.id, state=States.default) await bot.set_state(user_id=msg.from_user.id, state=States.default)
if len(msg.text.split()) > 1: if len(msg.text.split()) > 1:
command, queue_id = msg.text.split() command, queue_id = msg.text.split()
if queue := session.query(Queue).filter_by(id=queue_id).first(): if (
queue := session.query(Queue)
.filter_by(id=queue_id, active=True)
.first()
):
if ( if (
not session.query(QueueUser) not session.query(QueueUser)
.filter_by(queue_id=queue.id, user_id=msg.from_user.id) .filter_by(queue_id=queue.id, user_id=msg.from_user.id, active=True)
.first() .first()
) and not len(user.takes_part_in_queues) > MAX_QUEUES_PARTS_IN: ) and not len(user.takes_part_in_queues) > MAX_QUEUES_PARTS_IN:
last_user = ( last_user = (
session.query(QueueUser) session.query(QueueUser)
.filter_by(queue_id=queue.id) .filter_by(queue_id=queue.id, active=True)
.order_by(QueueUser.position.desc()) .order_by(QueueUser.position.desc())
.first() .first()
) )
@ -215,7 +230,7 @@ async def to_menu_handler(call: types.CallbackQuery):
@bot.callback_query_handler(func=lambda c: c.data[:2] == "p:") @bot.callback_query_handler(func=lambda c: c.data[:2] == "p:")
async def proceed_user_handler(call: types.CallbackQuery): async def proceed_user_handler(call: types.CallbackQuery):
queue_id = call.data[2:] queue_id = call.data[2:]
queue = session.query(Queue).filter_by(id=queue_id).first() queue = session.query(Queue).filter_by(id=queue_id, active=True).first()
user = session.query(User).filter_by(id=call.from_user.id).first() user = session.query(User).filter_by(id=call.from_user.id).first()
if next_queue_user := proceed_queue_user(queue, user): if next_queue_user := proceed_queue_user(queue, user):
try: try:
@ -250,6 +265,11 @@ async def proceed_user_handler(call: types.CallbackQuery):
await bot.answer_callback_query(callback_query_id=call.id) await bot.answer_callback_query(callback_query_id=call.id)
@bot.callback_query_handler(func=lambda c: c.data == "dummy")
async def dummy_handler(call: types.CallbackQuery):
await bot.answer_callback_query(callback_query_id=call.id)
# Main menu # Main menu
@ -257,7 +277,7 @@ async def proceed_user_handler(call: types.CallbackQuery):
async def new_queue_handler(call: types.CallbackQuery): async def new_queue_handler(call: types.CallbackQuery):
user = session.query(User).filter_by(id=call.from_user.id).first() user = session.query(User).filter_by(id=call.from_user.id).first()
if user: if user:
if len(user.owns_queues) < MAX_QUEUES_OWN: if len(list(filter(lambda q: q.active, user.owns_queues))) < MAX_QUEUES_OWN:
queue = Queue(owner_id=call.from_user.id) queue = Queue(owner_id=call.from_user.id)
session.add(queue) session.add(queue)
session.commit() session.commit()
@ -277,7 +297,7 @@ async def new_queue_handler(call: types.CallbackQuery):
@bot.callback_query_handler(func=lambda c: c.data == "my_queues") @bot.callback_query_handler(func=lambda c: c.data == "my_queues")
async def my_queues_handler(call: types.CallbackQuery): async def my_queues_handler(call: types.CallbackQuery):
user = session.query(User).filter_by(id=call.from_user.id).first() user = session.query(User).filter_by(id=call.from_user.id).first()
queues = user.owns_queues queues = list(filter(lambda q: q.active, user.owns_queues))
await bot.edit_message_text( await bot.edit_message_text(
chat_id=call.message.chat.id, chat_id=call.message.chat.id,
message_id=call.message.id, message_id=call.message.id,
@ -290,7 +310,9 @@ async def my_queues_handler(call: types.CallbackQuery):
@bot.callback_query_handler(func=lambda c: c.data == "parts_queues") @bot.callback_query_handler(func=lambda c: c.data == "parts_queues")
async def parts_queues_handler(call: types.CallbackQuery): async def parts_queues_handler(call: types.CallbackQuery):
user = session.query(User).filter_by(id=call.from_user.id).first() user = session.query(User).filter_by(id=call.from_user.id).first()
queues = [qu.queue for qu in user.takes_part_in_queues] queues = [
qu.queue for qu in list(filter(lambda q: q.active, user.takes_part_in_queues))
]
await bot.edit_message_text( await bot.edit_message_text(
chat_id=call.message.chat.id, chat_id=call.message.chat.id,
message_id=call.message.id, message_id=call.message.id,
@ -327,7 +349,7 @@ async def about_handler(call: types.CallbackQuery):
@bot.callback_query_handler(func=lambda c: c.data[:2] == "t:") @bot.callback_query_handler(func=lambda c: c.data[:2] == "t:")
async def queue_parts_handler(call: types.CallbackQuery, queue_id: str = None): async def queue_parts_handler(call: types.CallbackQuery, queue_id: str = None):
queue_id = call.data[2:] if not queue_id else queue_id queue_id = call.data[2:] if not queue_id else queue_id
queue = session.query(Queue).filter_by(id=queue_id).first() queue = session.query(Queue).filter_by(id=queue_id, active=True).first()
if not queue: if not queue:
await bot.answer_callback_query(callback_query_id=call.id, text=textbook.error) await bot.answer_callback_query(callback_query_id=call.id, text=textbook.error)
return None return None
@ -335,7 +357,12 @@ async def queue_parts_handler(call: types.CallbackQuery, queue_id: str = None):
user_id=call.from_user.id, chat_id=call.message.chat.id user_id=call.from_user.id, chat_id=call.message.chat.id
) as state_data: ) as state_data:
state_data["part_queue_id"] = queue_id state_data["part_queue_id"] = queue_id
users_str = "\n".join([f"{qu.position}. {qu.user.name}" for qu in queue.users]) users_str = "\n".join(
[
f"{qu.position}. {qu.user.name}"
for qu in list(filter(lambda qu: qu.active, queue.users))
]
)
await bot.edit_message_text( await bot.edit_message_text(
chat_id=call.message.chat.id, chat_id=call.message.chat.id,
message_id=call.message.id, message_id=call.message.id,
@ -354,10 +381,10 @@ async def leave_queue_handler(call: types.CallbackQuery):
if queue := await get_parting_queue_from_state_data(call): if queue := await get_parting_queue_from_state_data(call):
queueuser = ( queueuser = (
session.query(QueueUser) session.query(QueueUser)
.filter_by(queue_id=queue.id, user_id=call.from_user.id) .filter_by(queue_id=queue.id, user_id=call.from_user.id, active=True)
.first() .first()
) )
session.delete(queueuser) setattr(queueuser, "active", False)
session.commit() session.commit()
normalize_queue(queue) normalize_queue(queue)
await bot.answer_callback_query( await bot.answer_callback_query(
@ -372,7 +399,12 @@ async def leave_queue_handler(call: types.CallbackQuery):
@bot.callback_query_handler(func=lambda c: c.data == "refresh_list") @bot.callback_query_handler(func=lambda c: c.data == "refresh_list")
async def refresh_list_handler(call: types.CallbackQuery): async def refresh_list_handler(call: types.CallbackQuery):
if queue := await get_parting_queue_from_state_data(call): if queue := await get_parting_queue_from_state_data(call):
users_str = "\n".join([f"{qu.position}. {qu.user.name}" for qu in queue.users]) users_str = "\n".join(
[
f"{qu.position}. {qu.user.name}"
for qu in list(filter(lambda q: q.active, queue.users))
]
)
try: try:
await bot.edit_message_text( await bot.edit_message_text(
chat_id=call.message.chat.id, chat_id=call.message.chat.id,
@ -392,23 +424,24 @@ async def refresh_list_handler(call: types.CallbackQuery):
@bot.callback_query_handler(func=lambda c: c.data[:2] == "q:") @bot.callback_query_handler(func=lambda c: c.data[:2] == "q:")
async def queue_handler(call: types.CallbackQuery, queue_id: str = None): async def queue_handler(call: types.CallbackQuery, queue_id: str = None):
queue_id = call.data[2:] if not queue_id else queue_id queue_id = call.data[2:] if not queue_id else queue_id
queue = session.query(Queue).filter_by(id=queue_id).first() queue = session.query(Queue).filter_by(id=queue_id, active=True).first()
if not queue: if queue:
async with bot.retrieve_data(
user_id=call.from_user.id, chat_id=call.message.chat.id
) as state_data:
state_data["queue_id"] = queue.id
await bot.edit_message_text(
chat_id=call.message.chat.id,
message_id=call.message.id,
text=get_queue_stats_text(queue),
reply_markup=keyboards.queue_menu(),
parse_mode="html",
)
else:
await bot.answer_callback_query(callback_query_id=call.id, text=textbook.error) await bot.answer_callback_query(callback_query_id=call.id, text=textbook.error)
return None return None
async with bot.retrieve_data(
user_id=call.from_user.id, chat_id=call.message.chat.id
) as state_data:
state_data["queue_id"] = queue_id
await bot.edit_message_text(
chat_id=call.message.chat.id,
message_id=call.message.id,
text=get_queue_stats_text(queue),
reply_markup=keyboards.queue_menu(),
parse_mode="html",
)
await bot.answer_callback_query(callback_query_id=call.id) await bot.answer_callback_query(callback_query_id=call.id)
# Queue menu # Queue menu
@ -416,7 +449,12 @@ async def queue_handler(call: types.CallbackQuery, queue_id: str = None):
@bot.callback_query_handler(func=lambda c: c.data == "get_queue_users") @bot.callback_query_handler(func=lambda c: c.data == "get_queue_users")
async def get_queue_users_handler(call: types.CallbackQuery): async def get_queue_users_handler(call: types.CallbackQuery):
if queue := await get_queue_from_state_data(call): if queue := await get_queue_from_state_data(call):
users_str = "\n".join([f"{qu.position}. {qu.user.name}" for qu in queue.users]) users_str = "\n".join(
[
f"{qu.position}. {qu.user.name}"
for qu in list(filter(lambda qu: qu.active, queue.users))
]
)
await bot.edit_message_text( await bot.edit_message_text(
chat_id=call.message.chat.id, chat_id=call.message.chat.id,
message_id=call.message.id, message_id=call.message.id,
@ -509,11 +547,23 @@ async def kick_first_handler(call: types.CallbackQuery):
@bot.callback_query_handler(func=lambda c: c.data == "swap_users") @bot.callback_query_handler(func=lambda c: c.data == "swap_users")
async def swap_users_handler(call: types.CallbackQuery): async def swap_users_handler(call: types.CallbackQuery):
await bot.answer_callback_query( if queue := await get_queue_from_state_data(call):
callback_query_id=call.id, pl = PaginatedList(queue.users, 8)
text=textbook.in_development_plug, async with bot.retrieve_data(
show_alert=True, user_id=call.from_user.id, chat_id=call.message.chat.id
) ) as state_data:
state_data["pl"] = pl
await bot.edit_message_text(
chat_id=call.message.chat.id,
message_id=call.message.id,
text=textbook.swap_users_first,
reply_markup=keyboards.swap_users_list(
queue_users=pl.get_current_page(),
current_page=pl.current_page,
total_pages=len(pl.divided_list),
),
parse_mode="html",
)
@bot.callback_query_handler(func=lambda c: c.data == "refresh_users") @bot.callback_query_handler(func=lambda c: c.data == "refresh_users")
@ -521,7 +571,10 @@ async def refresh_users_handler(call: types.CallbackQuery):
if queue := await get_queue_from_state_data(call): if queue := await get_queue_from_state_data(call):
try: try:
users_str = "\n".join( users_str = "\n".join(
[f"{qu.position}. {qu.user.name}" for qu in queue.users] [
f"{qu.position}. {qu.user.name}"
for qu in list(filter(lambda qu: qu.active, queue.users))
]
) )
await bot.edit_message_text( await bot.edit_message_text(
chat_id=call.message.chat.id, chat_id=call.message.chat.id,
@ -537,6 +590,14 @@ async def refresh_users_handler(call: types.CallbackQuery):
await bot.answer_callback_query(callback_query_id=call.id) await bot.answer_callback_query(callback_query_id=call.id)
# Swap users
@bot.callback_query_handler(func=lambda c: c.data == "swap_users_page_up")
async def swap_users_page_up_handler(call: types.CallbackQuery):
pass
# Queue settings # Queue settings
@ -568,7 +629,7 @@ async def update_queue_name(msg: Message):
user_id=msg.from_user.id, chat_id=msg.chat.id user_id=msg.from_user.id, chat_id=msg.chat.id
) as state_data: ) as state_data:
queue_id = state_data.get("queue_id", None) queue_id = state_data.get("queue_id", None)
queue = session.query(Queue).filter_by(id=queue_id).first() queue = session.query(Queue).filter_by(id=queue_id, active=True).first()
if not queue: if not queue:
await bot.send_message(chat_id=msg.chat.id, text=textbook.edit_name_error) await bot.send_message(chat_id=msg.chat.id, text=textbook.edit_name_error)
return None return None
@ -609,8 +670,10 @@ async def delete_queue_approve_handler(call: types.CallbackQuery):
async def delete_queue_handler(call: types.CallbackQuery): async def delete_queue_handler(call: types.CallbackQuery):
if queue := await get_queue_from_state_data(call): if queue := await get_queue_from_state_data(call):
for qu in queue.users: for qu in queue.users:
session.delete(qu) # TODO: Use SQLAlchemy to cascade-delete all users setattr(
session.delete(queue) qu, "active", False
) # TODO: Use SQLAlchemy to cascade-delete all users
setattr(queue, "active", False)
session.commit() session.commit()
await bot.answer_callback_query( await bot.answer_callback_query(
callback_query_id=call.id, text=textbook.queue_deleted callback_query_id=call.id, text=textbook.queue_deleted
@ -688,6 +751,53 @@ async def changelog(msg: Message):
await bot.send_message(chat_id=msg.chat.id, text=file.read(), parse_mode="html") await bot.send_message(chat_id=msg.chat.id, text=file.read(), parse_mode="html")
@bot.message_handler(commands=["mydata"])
async def mydata(msg: Message):
async with bot.retrieve_data(
user_id=msg.from_user.id, chat_id=msg.chat.id
) as state_data:
await bot.send_message(chat_id=msg.chat.id, text=str(state_data))
@bot.message_handler(commands=["broadcast"])
async def broadcast(msg: Message):
if msg.from_user.id in admins:
await bot.set_state(user_id=msg.from_user.id, state=States.admin_broadcasting)
await bot.send_message(
chat_id=msg.chat.id,
text=textbook.admin_broadcasting,
reply_markup=keyboards.edit_name(),
)
@bot.callback_query_handler(
func=lambda c: c.data == "cancel", state=States.admin_broadcasting
)
async def cancel_broadcast(call: types.CallbackQuery):
await to_menu_handler(call)
@bot.message_handler(content_types=["text"], state=States.admin_broadcasting)
async def broadcast_message_handler(msg: Message):
if msg.from_user.id in admins:
counter = 0
for user in session.query(User):
try:
await bot.send_message(
chat_id=user.id,
text=msg.text,
)
counter += 1
except Exception as e:
continue
await bot.send_message(
chat_id=msg.chat.id,
text=textbook.broadcast_completed.format(count=counter),
)
await asyncio.sleep(1)
await start(msg)
# Launch # Launch

View File

@ -1,3 +1,8 @@
<b>v0.2.1-beta</b>
- Добавлена возможность миграции базы данных с помощью alembic
- Небольшая персистентность данных
- Теперь я могу броадкастить сообщения всем юзерам бота
<b>v0.1.8-beta</b> <b>v0.1.8-beta</b>
- Исправлен баг, при котором после кика первого юзера очередь отображалась наоборот - Исправлен баг, при котором после кика первого юзера очередь отображалась наоборот
- При создании очереди сразу генерится ссылка и создателю предлагается в нее вступить - При создании очереди сразу генерится ссылка и создателю предлагается в нее вступить

View File

@ -1,4 +1,4 @@
MAX_QUEUES_OWN = 6 MAX_QUEUES_OWN = 4
MAX_QUEUES_PARTS_IN = 8 MAX_QUEUES_PARTS_IN = 8
MAX_NAME_LENGTH = 40 MAX_NAME_LENGTH = 40
MAX_QUEUE_NAME_LENGTH = 40 MAX_QUEUE_NAME_LENGTH = 40

0
bot/app/db/__init__.py Normal file
View File

View File

@ -1,5 +1,5 @@
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, BigInteger, ForeignKey from sqlalchemy import Column, Integer, String, BigInteger, ForeignKey, Boolean
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship, backref from sqlalchemy.orm import relationship, backref
@ -26,6 +26,7 @@ class Queue(Base):
name = Column(String(40), default="Новая очередь") name = Column(String(40), default="Новая очередь")
description = Column(String(120), default=None) description = Column(String(120), default=None)
owner_id = Column(BigInteger, ForeignKey("user.id")) owner_id = Column(BigInteger, ForeignKey("user.id"))
active = Column(Boolean, default=True)
users = relationship( users = relationship(
"QueueUser", backref="queue" "QueueUser", backref="queue"
@ -39,3 +40,4 @@ class QueueUser(Base):
user_id = Column(BigInteger, ForeignKey("user.id")) user_id = Column(BigInteger, ForeignKey("user.id"))
queue_id = Column(UUID(as_uuid=True), ForeignKey("queue.id")) queue_id = Column(UUID(as_uuid=True), ForeignKey("queue.id"))
position = Column(Integer) position = Column(Integer)
active = Column(Boolean, default=True)

View File

@ -2,7 +2,7 @@ from telebot.types import (
InlineKeyboardButton as button, InlineKeyboardButton as button,
InlineKeyboardMarkup as keyboard, InlineKeyboardMarkup as keyboard,
) )
from db.models import Queue from db.models import Queue, QueueUser
def menu() -> keyboard: def menu() -> keyboard:
@ -117,3 +117,28 @@ def to_menu_keyboard() -> keyboard:
[button(text="⬅️ В меню", callback_data="to_menu")], [button(text="⬅️ В меню", callback_data="to_menu")],
] ]
) )
def pagination_footer(current_page: int, total_pages: int) -> list:
return [
button(text="◀️", callback_data="page_down"),
button(text=f"{current_page}/{total_pages}", callback_data="dummy"),
button(text="▶️", callback_data="page_up"),
]
def swap_users_list(
queue_users: list[QueueUser], current_page: int, total_pages: int
) -> keyboard:
kb = [
[
button(
text=f"{qu.position}. {qu.user.name}",
callback_data=f"action:{qu.id}",
)
]
for qu in queue_users
]
kb.append(pagination_footer(current_page, total_pages))
kb.append([button(text="⬅️ Назад", callback_data="get_queue_users")])
return keyboard(kb)

34
bot/app/pagination.py Normal file
View File

@ -0,0 +1,34 @@
from typing import Union
class PaginatedList:
def __init__(self, data: list, page_size: int, current_page:int=0):
self.data = data
self.page_size = page_size
self.current_page = current_page
@property
def divided_list(self):
return [
self.data[x : x + self.page_size]
for x in range(0, len(self.data), self.page_size)
]
def get_page(self, page: int) -> Union[list, None]:
if page < len(self.divided_list):
return self.divided_list[page]
return None
def get_current_page(self) -> list:
return self.get_page(self.current_page)
def page_up(self) -> Union[list, None]:
if self.current_page + 1 < len(self.divided_list):
self.current_page += 1
return self.get_page(self.current_page)
return None
def page_down(self) -> Union[list, None]:
if self.current_page - 1 >= 0:
self.current_page -= 1
return self.get_page(self.current_page)
return None

View File

@ -28,6 +28,9 @@ kick_first_error = (
"Действие не выполнено, возможно вы уже вышли из очереди, или очередь пуста?" "Действие не выполнено, возможно вы уже вышли из очереди, или очередь пуста?"
) )
swap_users_first = "Выбери первого пользователя"
swap_users_second = "Выбран: {name}. Поменять с:"
parts_queues_menu = "Ты принимаешь участие в {count} очереди/ей" parts_queues_menu = "Ты принимаешь участие в {count} очереди/ей"
part_queue = "Очередь <b>{name}</b>\n\nУчастники:\n{users_str}" part_queue = "Очередь <b>{name}</b>\n\nУчастники:\n{users_str}"
leaved_queue = "Ты вышел из очереди {name}" leaved_queue = "Ты вышел из очереди {name}"
@ -43,3 +46,6 @@ stats = "Количество пользователей: {users_count}\nКол
about = "Бот для очередей.\n\nРазработчик - ollyhearn.\nЯ всегда открыт для вопросов и предложений: @OllyHearn\n\nv0.1.8-beta" about = "Бот для очередей.\n\nРазработчик - ollyhearn.\nЯ всегда открыт для вопросов и предложений: @OllyHearn\n\nv0.1.8-beta"
groups_plug = "Всем привет, я бот для очередей! В настоящее время идет активная разработка, так что я пока не могу полностью функционировать в группах, но вы всегда можете запустить меня в личном диалоге, создать очередь, и отправить ссылку на очередь сюда. Функционал будет доработан, а пока пользуйтесь мной в личке:\n\nhttps://t.me/queue_senko_bot" groups_plug = "Всем привет, я бот для очередей! В настоящее время идет активная разработка, так что я пока не могу полностью функционировать в группах, но вы всегда можете запустить меня в личном диалоге, создать очередь, и отправить ссылку на очередь сюда. Функционал будет доработан, а пока пользуйтесь мной в личке:\n\nhttps://t.me/queue_senko_bot"
in_development_plug = "Функция в разработке ¯\_(ツ)_/¯" in_development_plug = "Функция в разработке ¯\_(ツ)_/¯"
admin_broadcasting = "Введите сообщение, или нажмите Отменить"
broadcast_completed = "Сообщение разослано {count} юзерам!"

View File

@ -1,7 +1,8 @@
pytelegrambotapi==4.14.1 pytelegrambotapi
asyncio asyncio
aiohttp aiohttp
psycopg-binary psycopg-binary
pydantic pydantic
sqlalchemy sqlalchemy
psycopg2-binary psycopg2-binary
alembic

View File

@ -13,6 +13,8 @@ services:
interval: 2s interval: 2s
timeout: 2s timeout: 2s
retries: 5 retries: 5
ports:
- 5432
bot: bot:
build: build:
context: bot context: bot
@ -25,6 +27,11 @@ services:
HOST: postgres HOST: postgres
PORT: 5432 PORT: 5432
restart: always restart: always
volumes:
- ./persistent_data/.state-save:/app/.state-save:rw
- ./bot/app:/app:z
ports:
- "4444:4444" # debugger port
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy