use crate::config::{Config, opencrabs_home};
use super::TelegramState;
use super::send::{message_in_thread, photo_in_thread};
use teloxide::prelude::*;
use teloxide::types::{ChatId, InlineKeyboardButton, InlineKeyboardMarkup, InputFile};
const COWORK_PREFIX: &str = "cowork_";
#[derive(Debug, Clone)]
pub struct CoworkState {
pub user_id: i64,
pub chat_id: i64,
pub session_id: String,
pub created_at: std::time::Instant,
}
const COWORK_TIMEOUT_SECS: u64 = 120;
impl CoworkState {
pub fn new(user_id: i64, chat_id: i64, session_id: String) -> Self {
Self {
user_id,
chat_id,
session_id,
created_at: std::time::Instant::now(),
}
}
pub fn is_expired(&self) -> bool {
self.created_at.elapsed().as_secs() > COWORK_TIMEOUT_SECS
}
}
pub fn is_cowork_session(param: &str) -> bool {
param.starts_with(COWORK_PREFIX) && param.len() > COWORK_PREFIX.len()
}
pub fn parse_startgroup_param(param: &str) -> Option<&str> {
if is_cowork_session(param) {
Some(¶m[COWORK_PREFIX.len()..])
} else {
None
}
}
pub fn build_cowork_deep_link(bot_username: &str, session_id: &str) -> String {
format!(
"https://t.me/{}?startgroup={}{}",
bot_username, COWORK_PREFIX, session_id
)
}
pub fn build_invite_qr(invite_link: &str) -> Option<(Vec<u8>, std::path::PathBuf)> {
let png_bytes = crate::brain::tools::whatsapp_connect::render_qr_png(invite_link)?;
let dir = opencrabs_home().join("tmp");
std::fs::create_dir_all(&dir).ok()?;
let path = dir.join("cowork_invite_qr.png");
std::fs::write(&path, &png_bytes).ok()?;
Some((png_bytes, path))
}
pub fn auto_register_user(user_id: i64) -> Result<bool, String> {
let config = Config::load().map_err(|e| format!("Failed to load config: {e}"))?;
let id_str = user_id.to_string();
if config.channels.telegram.allowed_users.contains(&id_str) {
return Ok(false);
}
let mut users = config.channels.telegram.allowed_users.clone();
users.push(id_str);
Config::write_array("channels.telegram", "allowed_users", &users)
.map_err(|e| format!("Failed to write allowed_users: {e}"))?;
Ok(true)
}
pub async fn is_cowork_group(chat_id: i64, state: &TelegramState) -> bool {
state.is_cowork_group(chat_id).await
}
pub async fn handle_cowork_command(
bot: &Bot,
_msg: &Message,
state: &TelegramState,
user_id: i64,
chat_id: i64,
thread_id: Option<teloxide::types::ThreadId>,
) -> Result<(), teloxide::RequestError> {
let bot_username = state
.bot_username()
.await
.unwrap_or_else(|| "opencrabsbot".to_string());
let session_id = if let Some(existing) = state.get_cowork_state(user_id).await {
existing.session_id
} else {
let sid = uuid::Uuid::new_v4().to_string()[..8].to_string();
state.start_cowork(user_id, chat_id, sid.clone()).await;
sid
};
let deep_link = build_cowork_deep_link(&bot_username, &session_id);
let keyboard = InlineKeyboardMarkup::new(vec![vec![InlineKeyboardButton::url(
"🦀 Add to Group".to_string(),
deep_link.parse().unwrap(),
)]]);
let text = "Tap below to add me to a Telegram group.\n\n\
After joining, I'll check if I have admin access. \
If not, I'll send you instructions to promote me.";
message_in_thread(bot, ChatId(chat_id), thread_id, text)
.reply_markup(keyboard)
.await?;
Ok(())
}
pub async fn handle_cowork_group_join(
bot: &Bot,
msg: &Message,
state: &TelegramState,
param: &str,
thread_id: Option<teloxide::types::ThreadId>,
) -> Result<(), teloxide::RequestError> {
let Some(session_id) = parse_startgroup_param(param) else {
return Ok(());
};
let group_chat_id = msg.chat.id.0;
let cowork = state.take_cowork_by_session(session_id).await;
state.add_cowork_group(group_chat_id).await;
let bot_info = bot.get_me().await?;
let bot_id = bot_info.id;
let bot_member = bot.get_chat_member(ChatId(group_chat_id), bot_id).await?;
let is_admin = matches!(
bot_member.status(),
teloxide::types::ChatMemberStatus::Administrator | teloxide::types::ChatMemberStatus::Owner
);
if !is_admin {
let promote_msg = "🦀 I need **admin privileges** to create invite links and read messages.\n\n\
**To fix this:**\n\
1. Open Group Settings → Administrators → Add Admin\n\
2. Select me and confirm\n\
3. Disable privacy mode: send `/setprivacy` to @BotFather, choose this bot, set to Disabled\n\n\
Then send `/cowork` in your DM with me again to generate the invite link.";
let _ = message_in_thread(bot, ChatId(group_chat_id), thread_id, promote_msg).await;
if let Some(ref cowork_state) = cowork {
let user_chat = ChatId(cowork_state.chat_id);
let dm_msg = "🦀 I was added to the group but I'm **not an admin** yet.\n\n\
**What to do:**\n\
1. Go to the group → Group Settings → Administrators\n\
2. Add me as admin\n\
3. Disable privacy mode: send `/setprivacy` to @BotFather, choose this bot, set to Disabled\n\n\
Then send `/cowork` here again to generate the invite link and QR code.";
let _ = message_in_thread(bot, user_chat, None, dm_msg).await;
}
return Ok(());
}
let invite_result = bot.create_chat_invite_link(ChatId(group_chat_id)).await;
match invite_result {
Ok(link) => {
let invite_url = &link.invite_link;
if let (Some((_png_bytes, qr_path)), Some(cowork_state)) =
(build_invite_qr(invite_url), cowork.as_ref())
{
let user_chat = ChatId(cowork_state.chat_id);
let _ = photo_in_thread(bot, user_chat, None, InputFile::file(qr_path)).await;
let _ = message_in_thread(
bot,
user_chat,
None,
format!(
"🦀 **All set!**\n\n\
Invite link: {}\n\n\
Share the QR or link. \
Everyone auto-registers when they join and send a message.",
invite_url
),
)
.await;
}
let _ = message_in_thread(
bot,
ChatId(group_chat_id),
thread_id,
"🦀 I'm in! @mention me anytime to chat.\n\n\
Everyone here is auto-registered. No setup needed.",
)
.await;
}
Err(e) => {
tracing::error!("Cowork: failed to create invite link: {}", e);
let _ = message_in_thread(
bot,
ChatId(group_chat_id),
thread_id,
"Failed to generate invite link. Make sure I'm an admin in this group.",
)
.await;
}
}
Ok(())
}