use teloxide::Bot;
use teloxide::prelude::Message;
use teloxide::prelude::Requester;
use teloxide::types::User;
use crate::config::Config;
use super::state::TelegramState;
#[allow(clippy::too_many_arguments)]
pub(crate) async fn handle_member_event(
bot: &Bot,
msg: &Message,
user: &User,
cfg: &Config,
telegram_state: &TelegramState,
) -> bool {
let user_id = user.id.0 as i64;
if let Some(members) = msg.new_chat_members() {
let chat_title = msg.chat.title().unwrap_or("unknown");
let chat_id = msg.chat.id.0;
for member in members {
let uid = member.id.0;
let name = member.username.as_deref().unwrap_or(&member.first_name);
let is_bot = member.is_bot;
let adder_name_for_guard = user.username.as_deref().unwrap_or(&user.first_name);
tracing::info!(
"Telegram: member joined chat \"{}\" (chat_id={}) — user_id={} username={} \
is_bot={} added_by={} (user_id={})",
chat_title,
chat_id,
uid,
name,
is_bot,
adder_name_for_guard,
user_id,
);
if is_bot {
let tg_cfg = &cfg.channels.telegram;
if let Some(owner_id_str) = tg_cfg.allowed_users.first()
&& let Ok(owner_id) = owner_id_str.parse::<i64>()
{
let join = if telegram_state.bot_user_id().await == Some(uid as i64) {
BotJoin::Ourselves
} else {
BotJoin::Other {
username: name,
user_id: uid,
}
};
let notify = format_bot_join_notification(
join,
chat_title,
chat_id,
msg.chat.username(),
adder_name_for_guard,
user_id,
);
if let Err(e) = crate::channels::telegram::send::message_in_thread(
bot,
teloxide::types::ChatId(owner_id),
None,
notify,
)
.await
{
tracing::error!(
"Telegram: could not tell the owner about a bot joining \"{}\" \
(chat_id={}), so the join is unreported: {}",
chat_title,
chat_id,
e
);
}
}
if telegram_state.bot_user_id().await == Some(uid as i64)
&& !cfg.channels.telegram.is_owner(&user_id.to_string())
{
tracing::warn!(
"Telegram: {} (user_id={}) is not the owner and added me to \"{}\" \
(chat_id={}) — leaving",
adder_name_for_guard,
user_id,
chat_title,
chat_id,
);
let left = match bot.leave_chat(msg.chat.id).await {
Ok(_) => true,
Err(e) => {
tracing::error!(
"Telegram: could not leave \"{}\" (chat_id={}) after an \
unauthorized add, so I am still in it: {}",
chat_title,
chat_id,
e
);
false
}
};
let tg_cfg = &cfg.channels.telegram;
if let Some(owner_id_str) = tg_cfg.allowed_users.first()
&& let Ok(owner_id) = owner_id_str.parse::<i64>()
{
let notify = format_unauthorized_add_notification(
chat_title,
chat_id,
msg.chat.username(),
adder_name_for_guard,
user_id,
left,
);
if let Err(e) = crate::channels::telegram::send::message_in_thread(
bot,
teloxide::types::ChatId(owner_id),
None,
notify,
)
.await
{
tracing::error!(
"Telegram: could not warn the owner about an unauthorized add \
to \"{}\" (chat_id={}): {}",
chat_title,
chat_id,
e
);
}
}
continue;
}
if telegram_state.bot_user_id().await == Some(uid as i64) {
let cowork_join = telegram_state.get_cowork_state(user_id).await.is_some();
if cowork_join {
if let Some(state) = telegram_state.get_cowork_state(user_id).await {
let _ = telegram_state
.take_cowork_by_session(&state.session_id)
.await;
}
match super::cowork::set_group_open(chat_id) {
Ok(()) => tracing::info!(
"[cowork] Opened group {} via /cowork (added by {})",
chat_id,
user_id
),
Err(e) => {
tracing::warn!("[cowork] Failed to open group {chat_id}: {e}")
}
}
}
let opener = if cowork_join {
"\n\nThis is a cowork group — everyone here can @mention me and chat."
} else {
"\n\nNew fellas: smash /start and I'll get you on the crew."
};
let mut welcome = format!(
"🦀 BOOM. Look who just crawled in. OpenCrabs is in the building.{opener} \
Then just @mention me and let's cook. 🔥"
);
let is_admin = matches!(
bot.get_chat_member(teloxide::types::ChatId(chat_id), member.id)
.await
.map(|m| m.status()),
Ok(teloxide::types::ChatMemberStatus::Administrator)
| Ok(teloxide::types::ChatMemberStatus::Owner)
);
if !is_admin {
welcome.push_str(
"\n\n(Bump me to admin so I hear the whole room, not just the \
shout-outs.)",
);
}
crate::channels::telegram::send::best_effort_note(
bot,
teloxide::types::ChatId(chat_id),
None,
&welcome,
None,
"system",
"member_welcome",
"new member welcome",
)
.await;
}
}
let group_open = cfg
.channels
.telegram
.groups
.get(&chat_id.to_string())
.map(|g| g.open)
.unwrap_or(false);
if !is_bot && group_open {
match super::cowork::auto_register_to_group(uid as i64, chat_id) {
Ok(true) => {
tracing::info!(
"[cowork] Auto-registered user {} ({}) in group {}",
uid,
name,
chat_id
);
if let Some(owner_id_str) = cfg.channels.telegram.allowed_users.first()
&& let Ok(owner_id) = owner_id_str.parse::<i64>()
{
let join_note =
format!("✅ New member joined workspace: {} ({})", name, uid);
crate::channels::telegram::send::best_effort_note(
bot,
teloxide::types::ChatId(owner_id),
None,
&join_note,
None,
"system",
"member_join_owner_notify",
"owner join notification",
)
.await;
}
}
Ok(false) => {
tracing::debug!("[cowork] User {} already registered", uid);
}
Err(e) => {
tracing::warn!("[cowork] Failed to auto-register user {}: {}", uid, e);
}
}
}
}
return true;
}
if let Some(left) = msg.left_chat_member() {
let chat_title = msg.chat.title().unwrap_or("unknown");
let chat_id = msg.chat.id.0;
let uid = left.id.0;
let name = left.username.as_deref().unwrap_or(&left.first_name);
tracing::info!(
"Telegram: member left chat \"{}\" (chat_id={}) — user_id={} username={} is_bot={}",
chat_title,
chat_id,
uid,
name,
left.is_bot,
);
return true;
}
false
}
pub(crate) enum BotJoin<'a> {
Ourselves,
Other { username: &'a str, user_id: u64 },
}
fn chat_reference(chat_title: &str, chat_id: i64, chat_username: Option<&str>) -> String {
match chat_username {
Some(u) if !u.is_empty() => {
format!("\"{chat_title}\" (chat_id={chat_id}, https://t.me/{u})")
}
_ => format!("\"{chat_title}\" (chat_id={chat_id}, private chat with no public link)"),
}
}
pub(crate) fn format_unauthorized_add_notification(
chat_title: &str,
chat_id: i64,
chat_username: Option<&str>,
adder_name: &str,
adder_id: i64,
left: bool,
) -> String {
let where_ = chat_reference(chat_title, chat_id, chat_username);
let outcome = if left {
"I left immediately."
} else {
"I tried to leave and could not, so remove me manually or revoke my group access in \
BotFather."
};
format!(
"🚫 {adder_name} (user_id={adder_id}) added me to {where_} and is not the bot owner. \
{outcome}"
)
}
pub(crate) fn format_bot_join_notification(
join: BotJoin<'_>,
chat_title: &str,
chat_id: i64,
chat_username: Option<&str>,
adder_name: &str,
adder_id: i64,
) -> String {
let where_ = chat_reference(chat_title, chat_id, chat_username);
match join {
BotJoin::Ourselves => format!(
"🦀 I was added to {where_} by {adder_name} (user_id={adder_id}). \
Reply here or check the group's settings if this was not you."
),
BotJoin::Other { username, user_id } => format!(
"🤖 Another bot joined {where_}, a chat I am already in: @{username} \
(user_id={user_id}), added by {adder_name} (user_id={adder_id}). \
Add {user_id} to allowed_users if you want me to respond to it."
),
}
}