mirror of
https://github.com/TheFunny/TelegramTwitterMediaBot.git
synced 2026-10-05 01:12:06 +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;
|
let _ = reply(ctx.sender, chat_id, reply_to, fetch_error_message(e)).await;
|
||||||
}
|
}
|
||||||
Ok(Some(fetched)) => {
|
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;
|
let chat_data = ctx.chat_store.get(chat_id).await;
|
||||||
// Per-site caption format override (empty -> built-in caption).
|
// Per-site caption format override (empty -> built-in caption).
|
||||||
let format = chat_data.format_for(fetched.site_id);
|
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);
|
let caption = fetched.caption_with(&format);
|
||||||
// Raw render data for the link cache; the send fills in the
|
// Raw render data for the link cache; the send fills in the
|
||||||
// Telegram file ids and persists the entry.
|
// 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]
|
#[tokio::test]
|
||||||
async fn unsupported_url_is_ignored_silently() {
|
async fn unsupported_url_is_ignored_silently() {
|
||||||
let stores = TestStores::new();
|
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:
|
/// Copies already-sent messages to the forward channel. No download fallback:
|
||||||
/// the files are already on Telegram's servers.
|
/// the files are already on Telegram's servers.
|
||||||
pub async fn forward_messages(ctx: &AppContext<'_>, task: &Task) -> Result<(), SendError> {
|
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::post_send::{build_edit_markup, cache_sent_task};
|
||||||
use super::upload::sniff_ext;
|
use super::upload::sniff_ext;
|
||||||
use super::*;
|
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::collections::HashMap;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use teloxide::ApiError;
|
use teloxide::ApiError;
|
||||||
@@ -785,6 +823,40 @@ mod tests {
|
|||||||
assert!(matches!(photos_first(items)[0], Photo { .. }));
|
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]
|
#[test]
|
||||||
fn retry_delay_seconds_bounds() {
|
fn retry_delay_seconds_bounds() {
|
||||||
for attempts in 0..10 {
|
for attempts in 0..10 {
|
||||||
|
|||||||
Reference in New Issue
Block a user