diff --git a/.gitignore b/.gitignore
index 2a16037..8832cac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
config.py
*.pyc
**/__pycache__
+**/.state-save
diff --git a/bot/app/__init__.py b/bot/app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bot/app/alembic.ini b/bot/app/alembic.ini
new file mode 100644
index 0000000..1e1c37f
--- /dev/null
+++ b/bot/app/alembic.ini
@@ -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
diff --git a/bot/app/alembic/README b/bot/app/alembic/README
new file mode 100644
index 0000000..98e4f9c
--- /dev/null
+++ b/bot/app/alembic/README
@@ -0,0 +1 @@
+Generic single-database configuration.
\ No newline at end of file
diff --git a/bot/app/alembic/env.py b/bot/app/alembic/env.py
new file mode 100644
index 0000000..79a538e
--- /dev/null
+++ b/bot/app/alembic/env.py
@@ -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()
diff --git a/bot/app/alembic/script.py.mako b/bot/app/alembic/script.py.mako
new file mode 100644
index 0000000..55df286
--- /dev/null
+++ b/bot/app/alembic/script.py.mako
@@ -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"}
diff --git a/bot/app/alembic/versions/9d20a4ff1eab_add_active.py b/bot/app/alembic/versions/9d20a4ff1eab_add_active.py
new file mode 100644
index 0000000..b5906d9
--- /dev/null
+++ b/bot/app/alembic/versions/9d20a4ff1eab_add_active.py
@@ -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 ###
diff --git a/bot/app/bot.py b/bot/app/bot.py
index 9cf563d..6e85b1b 100644
--- a/bot/app/bot.py
+++ b/bot/app/bot.py
@@ -17,10 +17,11 @@ from datetime import datetime
import math
import socket
import os
+import json
from typing import Union
# Local imports
-from config import token
+from config import token, admins
from constants import (
MAX_QUEUES_OWN,
MAX_QUEUES_PARTS_IN,
@@ -29,10 +30,12 @@ from constants import (
)
import textbook
import keyboards
+from pagination import PaginatedList
# DB
from db.base import Session, engine, Base
from db.models import User, Queue, QueueUser
+from sqlalchemy import select
bot = AsyncTeleBot(token, state_storage=StatePickleStorage())
@@ -42,13 +45,17 @@ class States(StatesGroup):
default = State()
changing_name = State()
changing_queue_name = State()
+ admin_broadcasting = State()
# Utils
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
@@ -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
)
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:
await bot.answer_callback_query(
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:
if len(queue.users):
first_user = get_first_queue_user(queue)
- session.delete(first_user)
+ setattr(first_user, "active", False)
session.commit()
normalize_queue(queue)
return True
@@ -118,15 +125,19 @@ def normalize_queue(queue: Queue) -> Queue:
# setattr(first_user, "position", 0)
# session.commit()
# queue = session.query(Queue).filter_by(id=queue.id).first()
- for i, qu in enumerate(sorted(queue.users, key=lambda qu: qu.position)):
- setattr(qu, "position", i)
+ queue_users = queue.users.filter_by(active= True)
+ for i, qu in enumerate(sorted(queue_users, key=lambda qu: qu.position)):
+ # setattr(qu, "position", i)
+ pass
session.commit()
return queue
async def update_queue_users_message(msg: Message, 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(
chat_id=msg.chat.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)
if len(msg.text.split()) > 1:
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 (
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()
) and not len(user.takes_part_in_queues) > MAX_QUEUES_PARTS_IN:
last_user = (
session.query(QueueUser)
- .filter_by(queue_id=queue.id)
+ .filter_by(queue_id=queue.id, active=True)
.order_by(QueueUser.position.desc())
.first()
)
@@ -215,7 +230,7 @@ async def to_menu_handler(call: types.CallbackQuery):
@bot.callback_query_handler(func=lambda c: c.data[:2] == "p:")
async def proceed_user_handler(call: types.CallbackQuery):
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()
if next_queue_user := proceed_queue_user(queue, user):
try:
@@ -250,6 +265,11 @@ async def proceed_user_handler(call: types.CallbackQuery):
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
@@ -257,7 +277,7 @@ async def proceed_user_handler(call: types.CallbackQuery):
async def new_queue_handler(call: types.CallbackQuery):
user = session.query(User).filter_by(id=call.from_user.id).first()
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)
session.add(queue)
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")
async def my_queues_handler(call: types.CallbackQuery):
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(
chat_id=call.message.chat.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")
async def parts_queues_handler(call: types.CallbackQuery):
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(
chat_id=call.message.chat.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:")
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 = session.query(Queue).filter_by(id=queue_id).first()
+ queue = session.query(Queue).filter_by(id=queue_id, active=True).first()
if not queue:
await bot.answer_callback_query(callback_query_id=call.id, text=textbook.error)
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
) as state_data:
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(
chat_id=call.message.chat.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):
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()
)
- session.delete(queueuser)
+ setattr(queueuser, "active", False)
session.commit()
normalize_queue(queue)
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")
async def refresh_list_handler(call: types.CallbackQuery):
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:
await bot.edit_message_text(
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:")
async def queue_handler(call: types.CallbackQuery, queue_id: str = None):
queue_id = call.data[2:] if not queue_id else queue_id
- queue = session.query(Queue).filter_by(id=queue_id).first()
- if not queue:
+ queue = session.query(Queue).filter_by(id=queue_id, active=True).first()
+ 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)
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)
-
+
# 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")
async def get_queue_users_handler(call: types.CallbackQuery):
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(
chat_id=call.message.chat.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")
async def swap_users_handler(call: types.CallbackQuery):
- await bot.answer_callback_query(
- callback_query_id=call.id,
- text=textbook.in_development_plug,
- show_alert=True,
- )
+ if queue := await get_queue_from_state_data(call):
+ pl = PaginatedList(queue.users, 8)
+ async with bot.retrieve_data(
+ 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")
@@ -521,7 +571,10 @@ async def refresh_users_handler(call: types.CallbackQuery):
if queue := await get_queue_from_state_data(call):
try:
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(
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)
+# 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
@@ -568,7 +629,7 @@ async def update_queue_name(msg: Message):
user_id=msg.from_user.id, chat_id=msg.chat.id
) as state_data:
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:
await bot.send_message(chat_id=msg.chat.id, text=textbook.edit_name_error)
return None
@@ -609,8 +670,10 @@ async def delete_queue_approve_handler(call: types.CallbackQuery):
async def delete_queue_handler(call: types.CallbackQuery):
if queue := await get_queue_from_state_data(call):
for qu in queue.users:
- session.delete(qu) # TODO: Use SQLAlchemy to cascade-delete all users
- session.delete(queue)
+ setattr(
+ qu, "active", False
+ ) # TODO: Use SQLAlchemy to cascade-delete all users
+ setattr(queue, "active", False)
session.commit()
await bot.answer_callback_query(
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")
+@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
diff --git a/bot/app/changelog.txt b/bot/app/changelog.txt
index d2400aa..efd230c 100644
--- a/bot/app/changelog.txt
+++ b/bot/app/changelog.txt
@@ -1,3 +1,8 @@
+v0.2.1-beta
+- Добавлена возможность миграции базы данных с помощью alembic
+- Небольшая персистентность данных
+- Теперь я могу броадкастить сообщения всем юзерам бота
+
v0.1.8-beta
- Исправлен баг, при котором после кика первого юзера очередь отображалась наоборот
- При создании очереди сразу генерится ссылка и создателю предлагается в нее вступить
diff --git a/bot/app/db/__init__.py b/bot/app/db/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bot/app/db/models.py b/bot/app/db/models.py
index e248d2e..dc27273 100644
--- a/bot/app/db/models.py
+++ b/bot/app/db/models.py
@@ -1,5 +1,5 @@
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.orm import relationship, backref
@@ -26,6 +26,7 @@ class Queue(Base):
name = Column(String(40), default="Новая очередь")
description = Column(String(120), default=None)
owner_id = Column(BigInteger, ForeignKey("user.id"))
+ active = Column(Boolean, default=True)
users = relationship(
"QueueUser", backref="queue"
@@ -39,3 +40,4 @@ class QueueUser(Base):
user_id = Column(BigInteger, ForeignKey("user.id"))
queue_id = Column(UUID(as_uuid=True), ForeignKey("queue.id"))
position = Column(Integer)
+ active = Column(Boolean, default=True)
diff --git a/bot/app/keyboards.py b/bot/app/keyboards.py
index 9c34019..0fbebd3 100644
--- a/bot/app/keyboards.py
+++ b/bot/app/keyboards.py
@@ -2,7 +2,7 @@ from telebot.types import (
InlineKeyboardButton as button,
InlineKeyboardMarkup as keyboard,
)
-from db.models import Queue
+from db.models import Queue, QueueUser
def menu() -> keyboard:
@@ -117,3 +117,28 @@ def to_menu_keyboard() -> keyboard:
[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)
diff --git a/bot/app/pagination.py b/bot/app/pagination.py
new file mode 100644
index 0000000..b13f9f4
--- /dev/null
+++ b/bot/app/pagination.py
@@ -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
diff --git a/bot/app/textbook.py b/bot/app/textbook.py
index 81a2cc9..091075b 100644
--- a/bot/app/textbook.py
+++ b/bot/app/textbook.py
@@ -28,6 +28,9 @@ kick_first_error = (
"Действие не выполнено, возможно вы уже вышли из очереди, или очередь пуста?"
)
+swap_users_first = "Выбери первого пользователя"
+swap_users_second = "Выбран: {name}. Поменять с:"
+
parts_queues_menu = "Ты принимаешь участие в {count} очереди/ей"
part_queue = "Очередь {name}\n\nУчастники:\n{users_str}"
leaved_queue = "Ты вышел из очереди {name}"
@@ -43,3 +46,6 @@ stats = "Количество пользователей: {users_count}\nКол
about = "Бот для очередей.\n\nРазработчик - ollyhearn.\nЯ всегда открыт для вопросов и предложений: @OllyHearn\n\nv0.1.8-beta"
groups_plug = "Всем привет, я бот для очередей! В настоящее время идет активная разработка, так что я пока не могу полностью функционировать в группах, но вы всегда можете запустить меня в личном диалоге, создать очередь, и отправить ссылку на очередь сюда. Функционал будет доработан, а пока пользуйтесь мной в личке:\n\nhttps://t.me/queue_senko_bot"
in_development_plug = "Функция в разработке ¯\_(ツ)_/¯"
+
+admin_broadcasting = "Введите сообщение, или нажмите Отменить"
+broadcast_completed = "Сообщение разослано {count} юзерам!"
\ No newline at end of file
diff --git a/bot/requirements.txt b/bot/requirements.txt
index 7c75ce6..884e9fc 100644
--- a/bot/requirements.txt
+++ b/bot/requirements.txt
@@ -5,3 +5,4 @@ psycopg-binary
pydantic
sqlalchemy
psycopg2-binary
+alembic
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index cc7ccfe..4a8d149 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -13,6 +13,8 @@ services:
interval: 2s
timeout: 2s
retries: 5
+ ports:
+ - 5432
bot:
build:
context: bot
@@ -25,6 +27,11 @@ services:
HOST: postgres
PORT: 5432
restart: always
+ volumes:
+ - ./persistent_data/.state-save:/app/.state-save:rw
+ - ./bot/app:/app:z
+ ports:
+ - "4444:4444" # debugger port
depends_on:
postgres:
condition: service_healthy