feat: sync Telegram Login OIDC provider
This commit is contained in:
parent
30774f8c39
commit
ebead9e98c
63 changed files with 11374 additions and 37 deletions
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue