use crate::channels::telegram::state::TelegramState;
use crate::config::Config;
use teloxide::Bot;
use teloxide::prelude::Requester;
use teloxide::types::{BotCommandScope, ChatId, Recipient, UserId};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct MemberView {
pub user_id: i64,
pub is_bot: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum SoloEval {
Eligible,
OtherHumans(Vec<i64>),
OwnerAbsent,
}
pub(crate) fn evaluate_solo_group(members: &[MemberView], owner_id: i64) -> SoloEval {
let mut other_humans = Vec::new();
let mut owner_present = false;
for m in members {
if m.is_bot {
continue;
}
if m.user_id == owner_id {
owner_present = true;
continue;
}
other_humans.push(m.user_id);
}
if !owner_present {
SoloEval::OwnerAbsent
} else if other_humans.is_empty() {
SoloEval::Eligible
} else {
SoloEval::OtherHumans(other_humans)
}
}
pub(crate) async fn maybe_auto_register(
bot: &Bot,
chat_id: i64,
cfg: &Config,
telegram_state: &TelegramState,
) {
let tg = &cfg.channels.telegram;
if tg.groups.contains_key(&chat_id.to_string()) {
return;
}
if telegram_state.solo_evaluated(chat_id).await.is_some() {
return;
}
let Some(owner_id) = tg.allowed_users.first().and_then(|s| s.parse::<i64>().ok()) else {
return;
};
let members = collect_members(bot, chat_id, tg.allowed_users.as_slice()).await;
let eval = evaluate_solo_group(&members, owner_id);
match eval {
SoloEval::Eligible => {
telegram_state.set_solo_evaluated(chat_id, true).await;
let commands = super::agent::collect_command_catalog();
publish_owner_menu(bot, chat_id, owner_id, &commands).await;
}
SoloEval::OtherHumans(humans) => {
tracing::debug!(
"Telegram: group {chat_id} not solo-owner (other humans present: {humans:?}) — \
keeping default menu floor"
);
}
SoloEval::OwnerAbsent => {
tracing::debug!(
"Telegram: group {chat_id} has no owner among members — nothing to register"
);
}
}
}
async fn collect_members(bot: &Bot, chat_id: i64, allowed_users: &[String]) -> Vec<MemberView> {
let mut members = Vec::new();
match bot.get_chat_administrators(ChatId(chat_id)).await {
Ok(admins) => {
for cm in admins {
members.push(MemberView {
user_id: cm.user.id.0 as i64,
is_bot: cm.user.is_bot,
});
}
}
Err(e) => {
tracing::debug!("Telegram: get_chat_administrators({chat_id}) failed: {e}");
return members;
}
}
for uid_s in allowed_users {
let Some(uid) = uid_s.parse::<u64>().ok() else {
continue;
};
if let Ok(cm) = bot.get_chat_member(ChatId(chat_id), UserId(uid)).await {
members.push(MemberView {
user_id: cm.user.id.0 as i64,
is_bot: cm.user.is_bot,
});
}
}
members
}
async fn publish_owner_menu(
bot: &Bot,
chat_id: i64,
owner_id: i64,
commands: &[teloxide::types::BotCommand],
) {
use teloxide::payloads::SetMyCommandsSetters;
match bot
.set_my_commands(commands.to_vec())
.scope(BotCommandScope::ChatMember {
chat_id: Recipient::Id(ChatId(chat_id)),
user_id: UserId(owner_id as u64),
})
.await
{
Ok(_) => tracing::info!(
"Telegram: solo-owner group {chat_id} auto-registered — full menu published \
for the owner ({commands_len} commands)",
commands_len = commands.len()
),
Err(e) => {
tracing::warn!("Telegram: solo-owner menu registration for {chat_id} failed: {e}")
}
}
}
pub(crate) async fn handle_membership_change(
bot: &Bot,
chat_id: i64,
departed_user_id: i64,
cfg: &Config,
telegram_state: &TelegramState,
) {
let tg = &cfg.channels.telegram;
if tg.groups.contains_key(&chat_id.to_string()) {
return;
}
let Some(owner_id) = tg.allowed_users.first().and_then(|s| s.parse::<i64>().ok()) else {
return;
};
telegram_state.clear_solo_evaluated(chat_id).await;
if departed_user_id != owner_id {
return;
}
use teloxide::payloads::SetMyCommandsSetters;
match bot
.set_my_commands(Vec::<teloxide::types::BotCommand>::new())
.scope(BotCommandScope::ChatMember {
chat_id: Recipient::Id(ChatId(chat_id)),
user_id: UserId(owner_id as u64),
})
.await
{
Ok(_) => tracing::info!(
"Telegram: owner left group {chat_id} — scoped menu cleared (#1155 lifecycle)"
),
Err(e) => tracing::debug!(
"Telegram: clearing owner menu after departure from {chat_id} failed (harmless if \
the bot itself is gone): {e}"
),
}
}