Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fec9ce0bca |
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
|||||||
config.py
|
config.py
|
||||||
*.pyc
|
*.pyc
|
||||||
**/__pycache__
|
**/__pycache__
|
||||||
|
**/.state-save
|
||||||
|
|||||||
0
bot/app/__init__.py
Normal file
0
bot/app/__init__.py
Normal file
105
bot/app/alembic.ini
Normal file
105
bot/app/alembic.ini
Normal 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
1
bot/app/alembic/README
Normal file
@ -0,0 +1 @@
|
|||||||
|
Generic single-database configuration.
|
||||||
84
bot/app/alembic/env.py
Normal file
84
bot/app/alembic/env.py
Normal 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()
|
||||||
24
bot/app/alembic/script.py.mako
Normal file
24
bot/app/alembic/script.py.mako
Normal 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"}
|
||||||
30
bot/app/alembic/versions/9d20a4ff1eab_add_active.py
Normal file
30
bot/app/alembic/versions/9d20a4ff1eab_add_active.py
Normal 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 ###
|
||||||
194
bot/app/bot.py
194
bot/app/bot.py
@ -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,21 +424,22 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,8 @@
|
|||||||
|
<b>v0.2.1-beta</b>
|
||||||
|
- Добавлена возможность миграции базы данных с помощью alembic
|
||||||
|
- Небольшая персистентность данных
|
||||||
|
- Теперь я могу броадкастить сообщения всем юзерам бота
|
||||||
|
|
||||||
<b>v0.1.8-beta</b>
|
<b>v0.1.8-beta</b>
|
||||||
- Исправлен баг, при котором после кика первого юзера очередь отображалась наоборот
|
- Исправлен баг, при котором после кика первого юзера очередь отображалась наоборот
|
||||||
- При создании очереди сразу генерится ссылка и создателю предлагается в нее вступить
|
- При создании очереди сразу генерится ссылка и создателю предлагается в нее вступить
|
||||||
|
|||||||
0
bot/app/db/__init__.py
Normal file
0
bot/app/db/__init__.py
Normal 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)
|
||||||
|
|||||||
@ -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
34
bot/app/pagination.py
Normal 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
|
||||||
@ -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} юзерам!"
|
||||||
@ -5,3 +5,4 @@ psycopg-binary
|
|||||||
pydantic
|
pydantic
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
psycopg2-binary
|
psycopg2-binary
|
||||||
|
alembic
|
||||||
@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user