feat: sync Telegram Login OIDC provider

This commit is contained in:
A 2026-07-21 15:46:24 +08:00
parent 30774f8c39
commit ebead9e98c
63 changed files with 11374 additions and 37 deletions

View file

@ -27,15 +27,27 @@ from aiogram.types import (
InlineKeyboardButton,
InlineKeyboardMarkup,
InputRichMessage,
LoginUrl,
Message,
)
from login_demo import (
LoginDemoConfig,
LoginDemoServer,
normalize_web_base,
parse_listen,
)
LOG = logging.getLogger("bedolagaformat")
MARKER_RE = re.compile(r"^[A-Za-z0-9-]{1,64}$")
MARKDOWN_V2_RESERVED_RE = re.compile(r"([_\*\[\]\(\)~`>#+\-=|{}\.!\\])")
def env_flag(name: str) -> bool:
return os.getenv(name, "").strip().lower() in {"1", "true", "yes", "on"}
@dataclass(frozen=True)
class FormatSample:
name: str
@ -121,6 +133,34 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--polling-timeout", type=int, default=10)
parser.add_argument("--marker", default=default_marker())
parser.add_argument("--log-level", default="INFO")
parser.add_argument(
"--login-demo",
action="store_true",
default=env_flag("TELESRV_BOT_LOGIN_DEMO"),
help="serve and send the Bedolaga Telegram Login/OIDC demo",
)
parser.add_argument(
"--login-issuer",
default=os.getenv("TELESRV_BOT_LOGIN_ISSUER", "http://127.0.0.1:2401"),
)
parser.add_argument(
"--login-client-id",
default=os.getenv("TELESRV_BOT_LOGIN_CLIENT_ID", ""),
)
parser.add_argument(
"--login-client-secret",
default=os.getenv("TELESRV_BOT_LOGIN_CLIENT_SECRET", ""),
help="confidential OIDC secret; never printed (optional for JS-only demo)",
)
parser.add_argument(
"--login-public-url",
default=os.getenv("TELESRV_BOT_LOGIN_PUBLIC_URL", "http://127.0.0.1:3000"),
help="registered origin where this demo is reachable",
)
parser.add_argument(
"--login-listen",
default=os.getenv("TELESRV_BOT_LOGIN_LISTEN", "127.0.0.1:3000"),
)
args = parser.parse_args()
if not args.token:
parser.error("missing --token or TELESRV_BOT_TOKEN")
@ -132,6 +172,24 @@ def parse_args() -> argparse.Namespace:
parser.error("--marker must contain 1-64 ASCII letters, digits, or hyphens")
if not 0 <= args.polling_timeout <= 50:
parser.error("--polling-timeout must be between 0 and 50")
args.login_config = None
if args.login_demo:
if not re.fullmatch(r"[0-9]{1,64}", args.login_client_id):
parser.error("--login-demo requires a numeric --login-client-id")
try:
issuer = normalize_web_base(args.login_issuer, name="login issuer")
public_url = normalize_web_base(args.login_public_url, name="login public URL")
listen_host, listen_port = parse_listen(args.login_listen)
except ValueError as exc:
parser.error(str(exc))
args.login_config = LoginDemoConfig(
issuer=issuer,
client_id=args.login_client_id,
client_secret=args.login_client_secret,
public_url=public_url,
listen_host=listen_host,
listen_port=listen_port,
)
return args
@ -207,6 +265,39 @@ def rich_menu_keyboard() -> InlineKeyboardMarkup:
)
def login_demo_keyboard(config: LoginDemoConfig) -> InlineKeyboardMarkup:
"""Exercise both Telegram's login_url button and a plain OIDC page URL."""
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="Log in with Telegram",
login_url=LoginUrl(
url=config.public_url + "/",
forward_text="Bedolaga Login",
request_write_access=True,
),
)
],
[InlineKeyboardButton(text="Open OIDC test page", url=config.public_url + "/")],
]
)
async def send_login_demo(bot: Bot, chat_id: int, marker: str, config: LoginDemoConfig) -> int:
message = await bot.send_message(
chat_id=chat_id,
text=(
f"<b>{marker} Telegram Login</b>\n"
"The first button validates Bot API <code>login_url</code>; "
"the second page validates the local JS SDK and OIDC + PKCE."
),
reply_markup=login_demo_keyboard(config),
)
LOG.info("sent Telegram Login demo chat_id=%s message_id=%s", chat_id, message.message_id)
return message.message_id
def is_rich_media_retry_error(exc: TelegramBadRequest) -> bool:
message = str(exc).lower()
return "webpage_" in message or "media_empty" in message or "media_invalid" in message
@ -259,7 +350,7 @@ async def send_rich_suite(bot: Bot, chat_id: int, marker: str) -> list[int]:
return ids
def build_dispatcher(marker: str) -> Dispatcher:
def build_dispatcher(marker: str, login_config: LoginDemoConfig | None = None) -> Dispatcher:
router = Router(name="telesrv-bedolaga-format")
@router.message(CommandStart())
@ -291,6 +382,22 @@ def build_dispatcher(marker: str) -> Dispatcher:
ids,
)
@router.message(Command("logindemo"))
async def login_demo(message: Message) -> None:
if login_config is None:
await message.answer(
"<b>Telegram Login demo is disabled.</b> Start this program with "
"<code>--login-demo</code>."
)
return
message_id = await send_login_demo(message.bot, message.chat.id, marker, login_config)
LOG.info(
"handled /logindemo chat_id=%s incoming_message_id=%s sent_message_id=%s",
message.chat.id,
message.message_id,
message_id,
)
dispatcher = Dispatcher()
dispatcher.include_router(router)
return dispatcher
@ -298,7 +405,10 @@ def build_dispatcher(marker: str) -> Dispatcher:
async def run(args: argparse.Namespace) -> None:
bot = create_bot(args.token, args.base_url)
login_server = LoginDemoServer(args.login_config, args.token) if args.login_config else None
try:
if login_server is not None:
await login_server.start()
me = await bot.get_me()
LOG.info(
"authenticated bot_id=%s username=@%s bot_api=%s marker=%s",
@ -312,12 +422,17 @@ async def run(args: argparse.Namespace) -> None:
await send_format_suite(bot, args.send_chat_id, args.marker)
if args.rich_menu or args.rich_only:
await send_rich_suite(bot, args.send_chat_id, args.marker)
if args.login_config is not None:
await send_login_demo(bot, args.send_chat_id, args.marker, args.login_config)
if args.send_only:
return
await bot.delete_webhook(drop_pending_updates=args.drop_pending)
dispatcher = build_dispatcher(args.marker)
LOG.info("polling started; send /start, /formatdemo or /richdemo to @%s", me.username or me.id)
dispatcher = build_dispatcher(args.marker, args.login_config)
LOG.info(
"polling started; send /start, /formatdemo, /richdemo or /logindemo to @%s",
me.username or me.id,
)
await dispatcher.start_polling(
bot,
allowed_updates=["message"],
@ -325,6 +440,8 @@ async def run(args: argparse.Namespace) -> None:
close_bot_session=False,
)
finally:
if login_server is not None:
await login_server.close()
await bot.session.close()