first commit

This commit is contained in:
2026-04-05 21:06:43 +07:00
commit 8a597f880f
28 changed files with 783 additions and 0 deletions

178
.gitignore vendored Normal file
View File

@@ -0,0 +1,178 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Static / IMG
static/img

149
alembic.ini Normal file
View File

@@ -0,0 +1,149 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts.
# this is typically a path given in POSIX (e.g. forward slashes)
# format, relative to the token %(here)s which refers to the location of this
# ini file
script_location = %(here)s/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
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory. for multiple paths, the path separator
# is defined by "path_separator" below.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the tzdata library which can be installed by adding
# `alembic[tz]` to the pip requirements.
# string value is passed to ZoneInfo()
# 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 <script_location>/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 "path_separator"
# below.
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
# path_separator; This indicates what character is used to split lists of file
# paths, including version_locations and prepend_sys_path within configparser
# files such as alembic.ini.
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
# to provide os-dependent path splitting.
#
# Note that in order to support legacy alembic.ini files, this default does NOT
# take place if path_separator is not present in alembic.ini. If this
# option is omitted entirely, fallback logic is as follows:
#
# 1. Parsing of the version_locations option falls back to using the legacy
# "version_path_separator" key, which if absent then falls back to the legacy
# behavior of splitting on spaces and/or commas.
# 2. Parsing of the prepend_sys_path option falls back to the legacy
# behavior of splitting on spaces, commas, or colons.
#
# Valid values for path_separator are:
#
# path_separator = :
# path_separator = ;
# path_separator = space
# path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
path_separator = os
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# database URL. This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py
# file.
sqlalchemy.url = driver://user:pass@localhost/dbname
[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
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
# hooks = ruff
# ruff.type = module
# ruff.module = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Alternatively, use the exec runner to execute a binary found on your PATH
# hooks = ruff
# ruff.type = exec
# ruff.executable = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Logging configuration. This is also consumed by the user-maintained
# env.py script only.
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
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
alembic/README Normal file
View File

@@ -0,0 +1 @@
Generic single-database configuration with an async dbapi.

97
alembic/env.py Normal file
View File

@@ -0,0 +1,97 @@
import asyncio
from logging.config import fileConfig
from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context
from config import settings
from db.base import Base
from db.models import * # noqa: F403
# 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
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.
url = settings.postgres_url
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.
"""
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def do_run_migrations(connection: Connection) -> None:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
async def run_async_migrations() -> None:
"""In this scenario we need to create an Engine
and associate a connection with the context.
"""
config.set_main_option("sqlalchemy.url", url)
connectable = async_engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode."""
asyncio.run(run_async_migrations())
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

28
alembic/script.py.mako Normal file
View File

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

View File

@@ -0,0 +1,39 @@
"""added a minimal user object.
Revision ID: 9692349958be
Revises:
Create Date: 2026-04-05 20:37:24.543241
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "9692349958be"
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"users",
sa.Column("id", sa.BIGINT(), nullable=False),
sa.Column("support_thread_id", sa.BIGINT(), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("id"),
sa.UniqueConstraint("support_thread_id"),
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("users")
# ### end Alembic commands ###

View File

@@ -0,0 +1,34 @@
"""referal_id added.
Revision ID: c447fdf8cc97
Revises: 9692349958be
Create Date: 2026-04-05 20:48:26.792824
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "c447fdf8cc97"
down_revision: Union[str, Sequence[str], None] = "9692349958be"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("users", sa.Column("referal_id", sa.BIGINT(), nullable=True))
op.create_unique_constraint(None, "users", ["id"])
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, "users", type_="unique")
op.drop_column("users", "referal_id")
# ### end Alembic commands ###

15
config.py Normal file
View File

@@ -0,0 +1,15 @@
from typing import Optional
from pydantic_settings import BaseSettings
from pydantic import Field
from dotenv import load_dotenv
load_dotenv(override=True)
class Settings(BaseSettings):
bot_token: str = Field(alias="BOT_TOKEN")
postgres_url: str = Field(alias="POSTGRES_URL")
proxy: Optional[str] = Field(None, alias="PROXY")
settings = Settings() # type: ignore

0
db/__init__.py Normal file
View File

4
db/base.py Normal file
View File

@@ -0,0 +1,4 @@
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase): ...

3
db/models/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from .users import User
__all__ = ["User"]

14
db/models/users.py Normal file
View File

@@ -0,0 +1,14 @@
from sqlalchemy import BIGINT
from sqlalchemy.orm import Mapped, mapped_column
from db.base import Base
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(
BIGINT, nullable=False, unique=True, primary_key=True
)
support_thread_id: Mapped[int] = mapped_column(BIGINT, nullable=True, unique=True)
referal_id: Mapped[int] = mapped_column(BIGINT, nullable=True)

6
db/session.py Normal file
View File

@@ -0,0 +1,6 @@
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from config import settings
engine = create_async_engine(settings.postgres_url, echo=True)
async_session = async_sessionmaker(bind=engine, expire_on_commit=False)

0
dto/__init__.py Normal file
View File

10
dto/di.py Normal file
View File

@@ -0,0 +1,10 @@
from dataclasses import dataclass
from repositories.users import UserRepository
from services.user_service import UserService
@dataclass
class DependenciesDTO:
user_repository: UserRepository
user_service: UserService

7
dto/users.py Normal file
View File

@@ -0,0 +1,7 @@
from dataclasses import dataclass
@dataclass
class NewUserDTO:
id: int
referal: int

8
entrypoints/startup.sh Normal file
View File

@@ -0,0 +1,8 @@
set -e
echo "[+] Formatting and checking..."
echo
ruff check --fix
black .
python main.py

3
handlers/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from .menus import router as menus_router
routers = [menus_router]

46
handlers/menus.py Normal file
View File

@@ -0,0 +1,46 @@
from aiogram import Router
from aiogram.filters import CommandStart, CommandObject
from aiogram.types import Message
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from dto.di import DependenciesDTO
from keyboards.client import main_menu
router = Router()
@router.message(CommandStart(deep_link=True, deep_link_encoded=False))
async def fetch_referal(
msg: Message,
command: CommandObject,
state: FSMContext,
session: AsyncSession,
deps: DependenciesDTO,
):
await state.clear()
data = command.args.split()
if not data:
await standard_start(msg, command, state)
return
referal = data[0]
await deps.user_service.add_user(session, user_id=msg.from_user.id, referal=referal)
await msg.reply(f"hi but....{referal}", reply_markup=main_menu)
@router.message(CommandStart(deep_link=False))
async def standard_start(msg: Message, command: CommandObject, state: FSMContext):
await msg.reply("hii!", reply_markup=main_menu)
# INSANE HOW UNSTABLE THIS FUCKING SYSTEM IS
# EVEN AFTER ROLLING EVERYTHING BACK
# HALF OF MY APPS STILL DONT WORK
# I FUCKING HATE THIS SHIT
# I CANT PACMAN -SYU CUZ IT BREAKS NVIDIA DRIVERS AND MY 240HZ MONITOR
# IS IN FUCKING 3 FPS **CONSTANTLY**
# I CANT MAKE IT UP AND I REALLY CANT TAE IT ANYMORE.
# wrap it up bud. we're gonna reset everything. bye. o7

15
keyboards/client.py Normal file
View File

@@ -0,0 +1,15 @@
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
main_menu: InlineKeyboardMarkup = InlineKeyboardBuilder(
[
[
InlineKeyboardButton(text="⚙️ Купить", callback_data="buy"),
InlineKeyboardButton(text="FAQ", callback_data="faq"),
],
[
InlineKeyboardButton(text="Наш Канал", url="https://goo.gle"),
InlineKeyboardButton(text="Тех. Поддержка", callback_data="support"),
],
]
).as_markup()

40
main.py Normal file
View File

@@ -0,0 +1,40 @@
import logging
import asyncio
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.client.session.aiohttp import AiohttpSession
from dto.di import DependenciesDTO
from handlers import routers
from middlewares import DIMiddleware
from config import settings
from repositories.users import UserRepository
from services.user_service import UserService
from db.session import async_session
logging.basicConfig(level=logging.DEBUG)
async def main():
print(settings.model_dump())
dp = Dispatcher()
user_repository = UserRepository()
user_service = UserService(user_repository)
deps = DependenciesDTO(user_repository=user_repository, user_service=user_service)
dp.update.middleware.register(DIMiddleware(deps, async_session))
aiohttp_session = AiohttpSession(proxy=settings.proxy)
default = DefaultBotProperties(parse_mode="HTML")
bot = Bot(token=settings.bot_token, default=default, session=aiohttp_session)
for router in routers:
dp.include_router(router)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())

3
middlewares/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from .di import DIMiddleware
__all__ = ["DIMiddleware"]

26
middlewares/di.py Normal file
View File

@@ -0,0 +1,26 @@
from typing import Callable, Awaitable, Any
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from sqlalchemy.ext.asyncio import async_sessionmaker
from dto.di import DependenciesDTO
class DIMiddleware(BaseMiddleware):
def __init__(
self, deps: DependenciesDTO, session_factory: async_sessionmaker
) -> None:
self.deps = deps
self.session_factory = session_factory
async def __call__(
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: dict[str, Any],
) -> Any:
data["deps"] = self.deps
async with self.session_factory() as session:
data["session"] = session
return await handler(event, data)

3
repositories/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from .users import UserRepository
__all__ = ["UserRepository"]

23
repositories/users.py Normal file
View File

@@ -0,0 +1,23 @@
from typing import Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from db.models import User
from dto.users import NewUserDTO
class UserRepository:
async def get_user_by_id(
self, session: AsyncSession, user_id: int
) -> Optional[User]:
stmt = select(User).where(User.id == user_id)
result = await session.execute(stmt)
return result.scalar_one_or_none()
async def create_user(self, session: AsyncSession, user_data: NewUserDTO) -> User:
user = User(id=user_data.id, referal_id=user_data.referal)
session.add(user)
await session.commit()
return user

9
requirements.txt Normal file
View File

@@ -0,0 +1,9 @@
aiogram
sqlalchemy[asyncio]
asyncpg
black
ruff
pydantic-settings
python-dotenv
aiohttp-socks
alembic

0
services/__init__.py Normal file
View File

22
services/user_service.py Normal file
View File

@@ -0,0 +1,22 @@
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from db.models.users import User
from dto.users import NewUserDTO
from repositories import UserRepository
class UserService:
def __init__(self, user_repository: UserRepository) -> None:
self.user_repository = user_repository
async def add_user(
self, session: AsyncSession, user_id: int, referal: Optional[int] = None
) -> User:
user = await self.user_repository.get_user_by_id(session, user_id)
if user:
return user
referal = int(referal) if str(referal).isdigit() else None
model = NewUserDTO(id=user_id, referal=referal)
return await self.user_repository.create_user(session, model)