mirror of
https://github.com/TheFunny/TelegramTwitterMediaBot.git
synced 2026-09-26 23:52:05 +00:00
feat(urls): deliver a media-less post as its text
A post with no media was answered with "No media found or media type is not supported.", throwing away text the fetch had already parsed, escaped and built a caption for (the per-site format and the long-post quoting included). It now goes out as a message through the same caption the media path would attach — the senders' own quoting is applied here, since there is no sender to do it. No queue entry: there is no Task shape for text and a post with nothing to download is cheap to paste again, so a failure is reported (send::send_text_post) rather than retried. Proven live: live_a_text_only_link_is_sent_as_text fetches a real text-only tweet through url_media and asserts one send_message carrying the post link and no media send.
This commit is contained in:
@@ -615,19 +615,25 @@ async fn url_media_inner(
|
||||
let _ = reply(ctx.sender, chat_id, reply_to, fetch_error_message(e)).await;
|
||||
}
|
||||
Ok(Some(fetched)) => {
|
||||
if fetched.media.is_empty() {
|
||||
let _ = reply(
|
||||
ctx.sender,
|
||||
chat_id,
|
||||
reply_to,
|
||||
"No media found or media type is not supported.",
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
let chat_data = ctx.chat_store.get(chat_id).await;
|
||||
// Per-site caption format override (empty -> built-in caption).
|
||||
let format = chat_data.format_for(fetched.site_id);
|
||||
if fetched.media.is_empty() {
|
||||
// A post with no media is still a post: its text goes out as a
|
||||
// message (through the same caption the media path would
|
||||
// attach, plus the long-post quoting the senders apply to
|
||||
// their own), instead of answering "No media found" to text
|
||||
// the fetch already parsed.
|
||||
let text = fetched
|
||||
.render_fields()
|
||||
.map(|(_, _, title, content, _)| x_media::site::compose_text(title, content))
|
||||
.unwrap_or_default();
|
||||
let caption = fetched.caption_with(&format);
|
||||
let caption =
|
||||
send::quote_long_caption(&caption, &text, ctx.config.caption_quote_text_chars);
|
||||
send::send_text_post(ctx, chat_id, reply_to.0 as i64, caption.into_owned()).await;
|
||||
return;
|
||||
}
|
||||
let caption = fetched.caption_with(&format);
|
||||
// Raw render data for the link cache; the send fills in the
|
||||
// Telegram file ids and persists the entry.
|
||||
@@ -820,6 +826,36 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A post with no media of its own is delivered as its text instead of
|
||||
/// "No media found" (live: reaching the branch needs a real fetch, since
|
||||
/// `Fetched` cannot be built outside x-media). Run with
|
||||
/// `cargo test -p xmedia-bot -- --ignored live`.
|
||||
#[tokio::test]
|
||||
#[ignore = "live network: fetches the post from its site"]
|
||||
async fn live_a_text_only_link_is_sent_as_text() {
|
||||
let stores = TestStores::new();
|
||||
let sender = MockSender::scripted(vec![Outcome::MessageOk], permanent_error);
|
||||
let ctx = stores.ctx(&sender);
|
||||
|
||||
url_media(
|
||||
&ctx,
|
||||
1,
|
||||
2,
|
||||
"https://x.com/i/status/1992471125734142256",
|
||||
PostSend::FromChat,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
sender.calls(),
|
||||
vec!["send_chat_action", "send_message"],
|
||||
"a media-less post is one message, not a media send"
|
||||
);
|
||||
let text = &sender.messages()[0];
|
||||
assert!(text.contains("https://x.com/"), "{text}");
|
||||
assert!(text.contains("1992471125734142256"), "{text}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unsupported_url_is_ignored_silently() {
|
||||
let stores = TestStores::new();
|
||||
|
||||
@@ -657,6 +657,44 @@ pub async fn send_animation(ctx: &AppContext<'_>, task: &Task) -> Result<Vec<i64
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a post that has no media of its own: its caption — the post's URL,
|
||||
/// author and text, in the chat's per-site format, long-post quoting included
|
||||
/// — becomes the message. Such a post used to be answered with "No media
|
||||
/// found", throwing away text the fetch had already parsed and escaped.
|
||||
///
|
||||
/// No queue entry: there is no `Task` shape for text, and a post with nothing
|
||||
/// to download is cheap for the user to paste again, so a failure is reported
|
||||
/// rather than retried.
|
||||
pub(crate) async fn send_text_post(
|
||||
ctx: &AppContext<'_>,
|
||||
chat_id: i64,
|
||||
reply_to: i64,
|
||||
caption: String,
|
||||
) {
|
||||
match ctx
|
||||
.sender
|
||||
.send_message(
|
||||
ChatId(chat_id),
|
||||
caption,
|
||||
Some(MessageId(reply_to as i32)),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => log::info!("sent the post's text for chat={chat_id}"),
|
||||
Err(e) => {
|
||||
log::warn!("could not send the post's text for chat={chat_id}: {e}");
|
||||
notify_failure(
|
||||
ctx.sender,
|
||||
Some(chat_id),
|
||||
Some(reply_to),
|
||||
"Could not send this post's text.",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies already-sent messages to the forward channel. No download fallback:
|
||||
/// the files are already on Telegram's servers.
|
||||
pub async fn forward_messages(ctx: &AppContext<'_>, task: &Task) -> Result<(), SendError> {
|
||||
@@ -704,7 +742,7 @@ mod tests {
|
||||
use super::post_send::{build_edit_markup, cache_sent_task};
|
||||
use super::upload::sniff_ext;
|
||||
use super::*;
|
||||
use crate::ctx::test_support::{TestStores, cached_photo, photo_item};
|
||||
use crate::ctx::test_support::{TestStores, api_error, cached_photo, photo_item};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use teloxide::ApiError;
|
||||
@@ -785,6 +823,40 @@ mod tests {
|
||||
assert!(matches!(photos_first(items)[0], Photo { .. }));
|
||||
}
|
||||
|
||||
/// A media-less post goes out as a message; a failure is reported rather
|
||||
/// than swallowed (there is no task to retry).
|
||||
#[tokio::test]
|
||||
async fn a_text_post_is_sent_or_reported() {
|
||||
let sender = MockSender::scripted(vec![Outcome::MessageOk], || api_error("boom"));
|
||||
let stores = TestStores::new();
|
||||
let ctx = stores.ctx(&sender);
|
||||
|
||||
send_text_post(
|
||||
&ctx,
|
||||
1,
|
||||
2,
|
||||
"https://x.com/u/status/1\n<a>u</a>: hello".to_string(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(sender.calls(), vec!["send_message"]);
|
||||
assert_eq!(
|
||||
sender.messages(),
|
||||
vec!["https://x.com/u/status/1\n<a>u</a>: hello"]
|
||||
);
|
||||
|
||||
// The failure path: the send fails, the notice follows.
|
||||
let sender = MockSender::scripted(vec![Outcome::MessageErr, Outcome::MessageOk], || {
|
||||
api_error("Bad Request: chat not found")
|
||||
});
|
||||
let ctx = stores.ctx(&sender);
|
||||
|
||||
send_text_post(&ctx, 1, 2, "text".into()).await;
|
||||
|
||||
assert_eq!(sender.calls(), vec!["send_message", "send_message"]);
|
||||
assert!(sender.messages()[1].contains("Could not send this post's text"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retry_delay_seconds_bounds() {
|
||||
for attempts in 0..10 {
|
||||
|
||||
Reference in New Issue
Block a user