V0.1.1 release, close to actual release. Bug & security fixes/improvements.

This commit is contained in:
unknown
2026-05-24 19:29:41 +02:00
parent a7b44af91a
commit b004e15948
38 changed files with 3145 additions and 137 deletions

View File

@@ -6,7 +6,7 @@ use teloxide::{
prelude::*,
types::{
InlineKeyboardButton, InlineKeyboardMarkup, Message, MessageId, ParseMode, CallbackQuery,
ChatMemberStatus, UserId, ChatPermissions, InputMedia, InputMediaPhoto, InputMediaDocument, InputFile,
ChatMemberStatus, UserId, ChatPermissions, InputMedia, InputMediaPhoto, InputMediaDocument, InputMediaVideo, InputMediaAudio, InputFile,
},
RequestError,
utils::command::BotCommands,
@@ -37,15 +37,14 @@ pub enum BotState {
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[derive(Default)]
pub enum UploadType {
#[default]
Media,
Document,
Text,
}
impl Default for UploadType {
fn default() -> Self { UploadType::Media }
}
#[derive(Clone, Serialize, Deserialize)]
pub struct StagedItem {
@@ -402,7 +401,7 @@ async fn handle_message_inner(
}
// Admin commands in groups
if msg.chat.is_group() || msg.chat.is_supergroup() {
if msg.chat.is_group() || msg.chat.is_supergroup() || msg.chat.is_channel() {
if let Some(text) = msg.text() {
let cmd = text.split_whitespace().next().unwrap_or("");
match cmd {
@@ -418,14 +417,14 @@ async fn handle_message_inner(
}
"/blacklist_uid" => {
tracing::info!("admin command /blacklist_uid chat={} user={}", chat_id, user_id);
if is_admin(&bot, msg.chat.id, user.id).await {
if ctx.config.groups.admin_group_ids.contains(&chat_id.0) && is_admin(&bot, msg.chat.id, user.id).await {
handle_admin_blacklist_uid(&bot, chat_id, text, &ctx).await?;
}
return Ok(());
}
"/whitelist_uid" => {
tracing::info!("admin command /whitelist_uid chat={} user={}", chat_id, user_id);
if is_admin(&bot, msg.chat.id, user.id).await {
if ctx.config.groups.admin_group_ids.contains(&chat_id.0) && is_admin(&bot, msg.chat.id, user.id).await {
handle_admin_whitelist_uid(&bot, chat_id, text, &ctx).await?;
}
return Ok(());
@@ -436,18 +435,18 @@ async fn handle_message_inner(
let help_text = r#"<b>Admin Commands</b>
/reload — Reload moderation lists.
/blacklist_uid <ID> — Blacklist a user ID.
/whitelist_uid <ID> — Remove a user from blacklist.
/blacklist_uid [ID] — Blacklist a user ID.
/whitelist_uid [ID] — Remove a user from blacklist.
/help — Show this message.
/get_id — Get current chat ID.
/get_id <@username> — Search administrators by username.
/get_id <displayname> — Search members in this chat by display name.
/get_id [@username] — Search administrators by username.
/get_id [displayname] — Search members in this chat by display name.
/create_submit_forward &lt;dest&gt; &lt;review&gt; [msg] — Create a submission forward.
/show_c_forward [page] — List forward links.
/add_blacklist <user_id> — Blacklist a user in all active forwards.
/rm_blacklist <user_id> — Remove a user from blacklist in all active forwards.
/sban @user <dur> <unit> [reason] — Ban for duration
/smute @user <dur> <unit> [reason] — Mute for duration
/add_blacklist [user_id] — Blacklist a user in all active forwards.
/rm_blacklist [user_id] — Remove a user from blacklist in all active forwards.
/sban @user [dur] [unit] [reason] — Ban for duration
/smute @user [dur] [unit] [reason] — Mute for duration
/mute @user [reason] — Mute indefinitely
/pban @user [reason] — Permanent ban
/kick @user [reason] — Kick from group
@@ -769,20 +768,18 @@ async fn handle_message_inner(
if let Some(def) = forward_repo.get_by_code(code).await? {
if def.revoked_at.is_some() {
bot.send_message(chat_id, "This submission link has been revoked.").await?;
} else if forward_repo.is_allowed(def.id, user_id).await? {
dialogue.update(BotState::SubmitMode { forward_id: def.id, code: code.to_string() }).await?;
let keyboard = InlineKeyboardMarkup::new(vec![vec![
InlineKeyboardButton::callback("[ Continue ]", "v1:submit:continue"),
InlineKeyboardButton::callback("[ Exit ]", "v1:submit:exit"),
]]);
bot.send_message(chat_id, "<b>[ Submission Mode ]</b>\n\nYou are about to submit content to a forward.\n\n<i>Continue to upload content for submission, or exit to return to the main menu.</i>")
.parse_mode(ParseMode::Html)
.reply_markup(keyboard)
.await?;
} else {
if forward_repo.is_allowed(def.id, user_id).await? {
dialogue.update(BotState::SubmitMode { forward_id: def.id, code: code.to_string() }).await?;
let keyboard = InlineKeyboardMarkup::new(vec![vec![
InlineKeyboardButton::callback("[ Continue ]", "v1:submit:continue"),
InlineKeyboardButton::callback("[ Exit ]", "v1:submit:exit"),
]]);
bot.send_message(chat_id, "<b>[ Submission Mode ]</b>\n\nYou are about to submit content to a forward.\n\n<i>Continue to upload content for submission, or exit to return to the main menu.</i>")
.parse_mode(ParseMode::Html)
.reply_markup(keyboard)
.await?;
} else {
bot.send_message(chat_id, "You are not allowed to use this submission link.").await?;
}
bot.send_message(chat_id, "You are not allowed to use this submission link.").await?;
}
} else {
bot.send_message(chat_id, "Invalid submission link.").await?;
@@ -968,7 +965,7 @@ async fn handle_callback_inner(
..Default::default()
};
dialogue.update(BotState::UploadOptions { items, options: options.clone() }).await?;
refresh_options_message(&bot, chat_id, &vec![], &options).await?;
refresh_options_message(&bot, chat_id, &[], &options).await?;
}
}
}
@@ -1556,6 +1553,10 @@ async fn finalize_upload(
let input_file = InputFile::memory(bytes.clone());
let media = if mime_type.starts_with("image/") {
InputMedia::Photo(InputMediaPhoto::new(input_file))
} else if mime_type.starts_with("video/") {
InputMedia::Video(InputMediaVideo::new(input_file))
} else if mime_type.starts_with("audio/") {
InputMedia::Audio(InputMediaAudio::new(input_file))
} else {
InputMedia::Document(InputMediaDocument::new(input_file))
};
@@ -1572,6 +1573,14 @@ async fn finalize_upload(
d.caption = Some(review_text.clone());
d.parse_mode = Some(ParseMode::Html);
}
InputMedia::Video(v) => {
v.caption = Some(review_text.clone());
v.parse_mode = Some(ParseMode::Html);
}
InputMedia::Audio(a) => {
a.caption = Some(review_text.clone());
a.parse_mode = Some(ParseMode::Html);
}
_ => {}
}
}
@@ -1630,7 +1639,7 @@ async fn show_previous_uploads(
let repo = ContentRepo::new(ctx.db.conn());
let total = repo.count_by_user(user_id).await?;
let items = repo.list_by_user(user_id, 10, page * 10).await?;
let total_pages = (total + 9) / 10;
let total_pages = total.div_ceil(10);
if items.is_empty() {
bot.send_message(chat_id, "<i>You have no uploads.</i>")
@@ -1765,32 +1774,29 @@ async fn handle_admin_callback(
ctx: &BotContext,
) -> HandlerResult {
tracing::info!("handle_admin_callback user={} action={}", user_id, parts[2]);
match parts[2] {
"delcontent" => {
let cxid = parts[3];
let content_id = ContentId::try_from(cxid)?;
let content_repo = ContentRepo::new(ctx.db.conn());
let content = match content_repo.get(&content_id).await? {
Some(c) => c,
None => {
bot.send_message(chat_id, "<b>Content not found.</b>")
.parse_mode(ParseMode::Html).await?;
return Ok(());
}
};
let is_admin = is_admin_in_chat(bot, chat_id, UserId(user_id as u64)).await;
if !is_admin && content.user_id != user_id {
bot.send_message(chat_id, "<b>Unauthorized.</b>")
if parts[2] == "delcontent" {
let cxid = parts[3];
let content_id = ContentId::try_from(cxid)?;
let content_repo = ContentRepo::new(ctx.db.conn());
let content = match content_repo.get(&content_id).await? {
Some(c) => c,
None => {
bot.send_message(chat_id, "<b>Content not found.</b>")
.parse_mode(ParseMode::Html).await?;
return Ok(());
}
ctx.pipeline.delete_content(&content_id, ctx.config.content.keep_content).await.ok();
content_repo.set_status(&content_id, ContentStatus::Deleted).await.ok();
bot.send_message(chat_id, format!("Content <code>{}</code> deleted.", cxid))
};
let is_admin = is_admin_in_chat(bot, chat_id, UserId(user_id as u64)).await;
if !is_admin && content.user_id != user_id {
bot.send_message(chat_id, "<b>Unauthorized.</b>")
.parse_mode(ParseMode::Html).await?;
return Ok(());
}
_ => {}
ctx.pipeline.delete_content(&content_id, ctx.config.content.keep_content).await.ok();
content_repo.set_status(&content_id, ContentStatus::Deleted).await.ok();
bot.send_message(chat_id, format!("Content <code>{}</code> deleted.", cxid))
.parse_mode(ParseMode::Html).await?;
return Ok(());
}
if !is_admin_in_chat(bot, chat_id, UserId(user_id as u64)).await {
@@ -1936,13 +1942,17 @@ async fn handle_forward_callback(
"<i>anonymous</i>".to_string()
};
let caption = format!(
let mut caption = format!(
"{}\n\nSubmitted by: {}\nDirect link: <code>{}</code>\nForward link: <code>{}</code>",
escape_html(&forward_def.forward_message),
author_line,
link,
forward_link
);
// Telegram media caption limit is 1024 characters
if caption.chars().count() > 1024 {
caption = caption.chars().take(1024).collect();
}
// 6. Forward content to destination (media batching or text-only)
let file_repo = ContentFileRepo::new(ctx.db.conn());
@@ -1983,6 +1993,10 @@ async fn handle_forward_callback(
let input_file = InputFile::memory(bytes.clone());
let media = if mime_type.starts_with("image/") {
InputMedia::Photo(InputMediaPhoto::new(input_file))
} else if mime_type.starts_with("video/") {
InputMedia::Video(InputMediaVideo::new(input_file))
} else if mime_type.starts_with("audio/") {
InputMedia::Audio(InputMediaAudio::new(input_file))
} else {
InputMedia::Document(InputMediaDocument::new(input_file))
};
@@ -1999,6 +2013,14 @@ async fn handle_forward_callback(
d.caption = Some(caption.clone());
d.parse_mode = Some(ParseMode::Html);
}
InputMedia::Video(v) => {
v.caption = Some(caption.clone());
v.parse_mode = Some(ParseMode::Html);
}
InputMedia::Audio(a) => {
a.caption = Some(caption.clone());
a.parse_mode = Some(ParseMode::Html);
}
_ => {}
}
}
@@ -2131,11 +2153,7 @@ async fn handle_get_id_search(
_ctx: &BotContext,
) -> HandlerResult {
let query_lower = query.to_lowercase();
let search_term = if query_lower.starts_with('@') {
&query_lower[1..]
} else {
&query_lower
};
let search_term = query_lower.strip_prefix('@').unwrap_or(&query_lower);
let mut matches = vec![];
// Try administrators first (bots usually can see them)
@@ -2168,11 +2186,6 @@ async fn handle_admin_blacklist_uid(
text: &str,
ctx: &BotContext,
) -> HandlerResult {
if !ctx.config.groups.admin_group_ids.contains(&chat_id.0) {
bot.send_message(chat_id, "This command is only available in the admin group.")
.await?;
return Ok(());
}
let uid = text.split_whitespace().nth(1).and_then(|s| s.parse::<i64>().ok());
let Some(uid) = uid else {
bot.send_message(chat_id, "Usage: /blacklist_uid <user_id>").await?;
@@ -2192,11 +2205,6 @@ async fn handle_admin_whitelist_uid(
text: &str,
ctx: &BotContext,
) -> HandlerResult {
if !ctx.config.groups.admin_group_ids.contains(&chat_id.0) {
bot.send_message(chat_id, "This command is only available in the admin group.")
.await?;
return Ok(());
}
let uid = text.split_whitespace().nth(1).and_then(|s| s.parse::<i64>().ok());
let Some(uid) = uid else {
bot.send_message(chat_id, "Usage: /whitelist_uid <user_id>").await?;
@@ -2241,6 +2249,7 @@ fn parse_duration(parts: &[&str]) -> Result<Option<i64>, String> {
Ok(Some(total as i64))
}
#[allow(clippy::too_many_arguments)]
async fn propagate_punishment(
bot: &Bot,
ctx: &BotContext,