Initial release
Build and Push Docker Images / build (push) Successful in 17s

This commit is contained in:
Stardream
2026-06-02 10:58:24 +10:00
commit eace48732d
88 changed files with 11140 additions and 0 deletions
View File
+92
View File
@@ -0,0 +1,92 @@
from aiogram import F, Router
from aiogram.types import ChatMemberUpdated, Message
from app.core.database import AsyncSessionLocal
from app.models.managed_chat import ManagedChat
from app.models.lottery import Lottery, LotteryStatus
from app.models.passphrase_verification import PassphraseVerification
from sqlalchemy import select
router = Router()
@router.my_chat_member(F.chat.type.in_({"group", "supergroup", "channel"}))
async def bot_chat_member_updated(event: ChatMemberUpdated) -> None:
"""Record or update the bot's presence in a group/channel."""
async with AsyncSessionLocal() as db:
chat = event.chat
new_status = event.new_chat_member.status
result = await db.execute(
select(ManagedChat).where(ManagedChat.id == chat.id)
)
managed = result.scalar_one_or_none()
if new_status in ("member", "administrator"):
if managed is None:
managed = ManagedChat(
id=chat.id,
title=chat.title or "",
username=getattr(chat, 'username', None),
chat_type=chat.type,
)
db.add(managed)
else:
managed.is_active = True
managed.title = chat.title or managed.title
managed.username = getattr(chat, 'username', None)
await db.commit()
elif new_status in ("left", "kicked") and managed is not None:
managed.is_active = False
await db.commit()
@router.channel_post(F.chat.type == "channel")
@router.message(F.chat.type.in_({"group", "supergroup"}))
async def on_chat_message(message: Message) -> None:
"""Track latest message ID and detect passphrase submissions."""
async with AsyncSessionLocal() as db:
result = await db.execute(select(ManagedChat).where(ManagedChat.id == message.chat.id))
managed = result.scalar_one_or_none()
if managed is None:
managed = ManagedChat(
id=message.chat.id,
title=message.chat.title or "",
username=getattr(message.chat, "username", None),
chat_type=message.chat.type,
last_message_id=message.message_id,
)
db.add(managed)
elif message.message_id > (managed.last_message_id or 0):
managed.last_message_id = message.message_id
await db.commit()
if message.text and message.from_user:
text = message.text.strip()
uid = message.from_user.id
cid = message.chat.id
async with AsyncSessionLocal() as db2:
r = await db2.execute(
select(Lottery).where(
Lottery.status == LotteryStatus.ACTIVE,
Lottery.require_membership == True,
)
)
for lottery in r.scalars().all():
ids = lottery.required_chat_ids or []
phrases = lottery.required_chat_passphrases or []
for i, chat_id in enumerate(ids):
if chat_id == cid and i < len(phrases) and phrases[i] and text == phrases[i]:
existing = await db2.execute(
select(PassphraseVerification).where(
PassphraseVerification.lottery_id == lottery.id,
PassphraseVerification.chat_id == cid,
PassphraseVerification.user_id == uid,
)
)
if existing.scalar_one_or_none() is None:
db2.add(PassphraseVerification(
lottery_id=lottery.id, chat_id=cid, user_id=uid
))
await db2.commit()
break
+50
View File
@@ -0,0 +1,50 @@
from aiogram import Router
from aiogram.filters import Command, CommandObject, CommandStart
from aiogram.types import Message
from app.bot.keyboards.inline import create_lottery_keyboard, lottery_detail_keyboard, main_menu_keyboard
router = Router()
@router.message(CommandStart())
async def cmd_start(message: Message, command: CommandObject) -> None:
# Deep-link from group announcement: /start lottery_<uuid>
if command.args and command.args.startswith("lottery_"):
lottery_id = command.args[len("lottery_"):]
await message.answer(
"🎰 Tap the button below to join the lottery:",
reply_markup=lottery_detail_keyboard(lottery_id),
)
return
await message.answer(
"👋 Welcome to <b>TG Lottery Bot</b>!\n\n"
"Tap the button below to get started. Use /help for more information.",
reply_markup=main_menu_keyboard(),
parse_mode="HTML",
)
@router.message(Command("newlottery"))
async def cmd_new_lottery(message: Message) -> None:
await message.answer("Create a new lottery 👇", reply_markup=create_lottery_keyboard())
@router.message(Command("mylotteries"))
async def cmd_my_lotteries(message: Message) -> None:
await message.answer("View my lotteries 👇", reply_markup=main_menu_keyboard())
@router.message(Command("help"))
async def cmd_help(message: Message) -> None:
await message.answer(
"📖 <b>TG Lottery Bot</b>\n\n"
"<b>Commands</b>\n"
"/start - Open lottery manager\n"
"/newlottery - Create a new lottery\n"
"/mylotteries - View my lotteries\n"
"/help - Show this help message\n\n"
"📦 <b>Powered by</b> git.stdm.moe/Stardream/TGLotteryBot",
parse_mode="HTML",
)
+50
View File
@@ -0,0 +1,50 @@
from urllib.parse import urljoin
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, WebAppInfo
from app.core.config import settings
def _mini_app_url(path: str = "") -> str:
base = settings.mini_app_url.rstrip("/") + "/"
return urljoin(base, path.lstrip("/"))
def main_menu_keyboard() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[[
InlineKeyboardButton(
text="Open lottery app",
web_app=WebAppInfo(url=_mini_app_url()),
)
]])
def create_lottery_keyboard() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[[
InlineKeyboardButton(
text="Create lottery",
web_app=WebAppInfo(url=_mini_app_url("/create")),
)
]])
def lottery_detail_keyboard(lottery_id: str, text: str = "🎰 Join lottery") -> InlineKeyboardMarkup:
"""For private chat messages - web_app is allowed here."""
return InlineKeyboardMarkup(inline_keyboard=[[
InlineKeyboardButton(
text=text,
web_app=WebAppInfo(url=_mini_app_url(f"/lottery/{lottery_id}")),
)
]])
def lottery_announce_keyboard(lottery_id: str) -> InlineKeyboardMarkup:
"""For group/channel announcements - web_app is not allowed; use t.me deep link."""
from app.bot.router import bot_username
if bot_username:
url = f"https://t.me/{bot_username}?start=lottery_{lottery_id}"
else:
url = _mini_app_url(f"/lottery/{lottery_id}")
return InlineKeyboardMarkup(inline_keyboard=[[
InlineKeyboardButton(text="🎰 Join lottery", url=url)
]])
+17
View File
@@ -0,0 +1,17 @@
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from app.core.config import settings
from app.bot.handlers import start, member
bot = Bot(
token=settings.bot_token,
default=DefaultBotProperties(parse_mode="HTML"),
)
dp = Dispatcher()
dp.include_router(start.router)
dp.include_router(member.router)
# Populated at startup by main.py after bot.get_me()
bot_username: str = ""