mirror of
https://github.com/kristoferssolo/tg-relay-rs.git
synced 2025-12-20 11:04:41 +00:00
feat: add commentary
This commit is contained in:
parent
36270c7a3f
commit
7a9ef2c48c
41
Cargo.lock
generated
41
Cargo.lock
generated
@ -1149,6 +1149,15 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr2"
|
||||
version = "2.0.0"
|
||||
@ -1203,6 +1212,35 @@ version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rc-box"
|
||||
version = "1.3.0"
|
||||
@ -1705,8 +1743,9 @@ dependencies = [
|
||||
"dotenv",
|
||||
"futures",
|
||||
"infer",
|
||||
"once_cell",
|
||||
"rand",
|
||||
"regex",
|
||||
"serde",
|
||||
"teloxide",
|
||||
"tempfile",
|
||||
"thiserror 2.0.16",
|
||||
|
||||
@ -11,8 +11,9 @@ color-eyre = "0.6"
|
||||
dotenv = "0.15"
|
||||
futures = "0.3.31"
|
||||
infer = "0.19"
|
||||
once_cell = "1.21.3"
|
||||
rand = "0.9"
|
||||
regex = "1.11"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
teloxide = { version = "0.17", features = ["macros"] }
|
||||
tempfile = "3"
|
||||
thiserror = "2.0"
|
||||
|
||||
141
comments.txt
Normal file
141
comments.txt
Normal file
@ -0,0 +1,141 @@
|
||||
P-foking-18. Again. Are we even trying?
|
||||
The car is a piece of shit. What do you want from me? A miracle?
|
||||
He missed the apex by a foking mile. Was he looking at the birds?
|
||||
We have the pace of a foking milk float. Honestly.
|
||||
Did we put fuel in the car? It looks like he is pushing it.
|
||||
This is not a qualifying lap, this is a foking parade lap.
|
||||
Both of them! Not just one, both! Unbelievable.
|
||||
Tomorrow we start from the back. At least we cannot go further back.
|
||||
What was that? He braked in the wrong foking country.
|
||||
The data says we should be faster. The data is a foking liar.
|
||||
What the fok was that strategy? Did we let a fan decide?
|
||||
Tell him to shut up and drive the foking car.
|
||||
A 5-second pitstop? Are we having a picnic out there?
|
||||
He’s complaining about graining? The race foking started two laps ago!
|
||||
Blue flags! Just get out of the foking way! We are not racing him!
|
||||
The deg is foking ridiculous on this tyre. Box. Now.
|
||||
Don't tell me about the gap. I can see the gap. It's getting foking bigger.
|
||||
Why is he fighting him? He will destroy the tyres for nothing!
|
||||
Fok, we look like a bunch of wankers.
|
||||
Is the engine on fire? No? Then tell him to keep foking pushing.
|
||||
So, to summarize: the start was shit, the middle was shit, and the end was shit.
|
||||
We scored zero points. Again. I am not even angry, I am just… tired.
|
||||
Who is responsible for this? Everyone. Everyone is foking responsible.
|
||||
Do not show me the positives. There are no foking positives today.
|
||||
We got lapped. Twice. By a Sauber. A FOKING SAUBER.
|
||||
Next race we try something new: we try to be less shit.
|
||||
This performance was unacceptable. Go home, think about it.
|
||||
I need a drink. Or maybe five drinks.
|
||||
Don't talk to me. Honestly, nobody talk to me for an hour.
|
||||
We are the slowest foking team on the grid. Fact.
|
||||
Look at that! He foksmashed the car. Again.
|
||||
That's another 200,000 dollars. Fok. Gene is going to love this.
|
||||
What a foking idiot. He had the whole track to himself.
|
||||
Are you okay? ... *[long pause]* ... Okay. Now, what the fok happened?
|
||||
The suspension is gone, the wing is gone... he brought back half a car.
|
||||
He is a foking rockstar for crashing.
|
||||
Was the wall moving? I don't think so.
|
||||
He blames the car. Of course he blames the foking car.
|
||||
Two drivers, one corner. And now, zero foking cars.
|
||||
Get the spare parts. All of them. We will need them.
|
||||
You saw the race, no? Why are you asking me? It was shit.
|
||||
We are not here to be last. But today, we are. So, what is your question?
|
||||
Yes, it was a fantastic result. We finished. That is the highlight.
|
||||
The drivers did their best. Their best was just not very good.
|
||||
We need a bigger budget, a better car, and maybe some foking luck.
|
||||
I am not going to stand here and say we are happy. We are foking not.
|
||||
Next year will be better. It cannot be foking worse.
|
||||
He is a young driver. He makes mistakes. Expensive, foking stupid mistakes.
|
||||
Why did we pit then? Because the crystal ball was broken today.
|
||||
Look, we are a small team. We do our best. Sometimes our best is just shit.
|
||||
The gearbox is foked. Just foked.
|
||||
It's not smoke, it's 'unscheduled pyrotechnics'.
|
||||
We build a car for a whole year and a foking $10 sensor fails.
|
||||
Engine problem. Of course. It's a Ferrari engine, what do you expect?
|
||||
Hydraulics are gone. Park the foking thing before it burns.
|
||||
It just went 'bang'. A very expensive 'bang'.
|
||||
He says he has no power. Tell him welcome to my foking world.
|
||||
The brakes failed. FOKING BRAKES. That is not a small problem.
|
||||
Something is loose in the cockpit. I hope it's not his foking brain.
|
||||
This part should last 5 races. It lasted 5 laps.
|
||||
This is not a foking kindergarten.
|
||||
We need to pull our foking finger out. Now.
|
||||
Honestly, some days I think I'd be happier farming. At least the potatoes don't crash.
|
||||
Everyone is a wanker until they prove otherwise.
|
||||
I don't have time for this bullshit.
|
||||
Focus. Just foking focus on your job.
|
||||
Hope is not a strategy.
|
||||
This sport will give you a foking heart attack.
|
||||
If you don't want pressure, go sell ice cream.
|
||||
Sometimes you just have to say 'fok it'.
|
||||
I am surrounded by foking idiots.
|
||||
What are we doing here? Seriously, what is the foking point?
|
||||
No more excuses. I am sick of the foking excuses.
|
||||
It is what it is. A foking mess.
|
||||
This is racing. It's brutal.
|
||||
Right. Last race was shit. This race, we will be less shit. Go.
|
||||
Do not fok this up. That is all I ask.
|
||||
Points. We need foking points. I don't care how.
|
||||
Drive like you foking mean it for once.
|
||||
Let's show them we are not a bunch of wankers.
|
||||
If you crash, don't foking call me.
|
||||
Be aggressive. Smart, but foking aggressive.
|
||||
Forget the pressure. Just drive the damn car.
|
||||
We are the underdog. Let's go bite someone in the foking ankles.
|
||||
Okay guys. Let's go have some fun... and by fun, I mean don't finish last.
|
||||
The catering is good today. That is the only good thing.
|
||||
He wants a new contract? Show me some foking results first.
|
||||
My patience is thinner than a foking razor blade right now.
|
||||
I need more coffee and less stupid people.
|
||||
This regulation change is a foking joke.
|
||||
Don't smile at me. We have nothing to smile about.
|
||||
Who designed this part? I want to have a word with him. A very loud word.
|
||||
Unbelievable. Just... foking unbelievable.
|
||||
For foks sake.
|
||||
I'm too old for this shit.
|
||||
Can we just go home now?
|
||||
Another day, another foking drama.
|
||||
The drivers' briefing was a waste of foking time.
|
||||
He is now officially a member of the wanker club.
|
||||
Everything is foked. Completely foked.
|
||||
Call Gene. Tell him... actually, don't foking call Gene.
|
||||
Narrator: It was not, in fact, fine.
|
||||
And at that moment, they knew... they messed up.
|
||||
My entire life is a series of these moments.
|
||||
Task failed successfully.
|
||||
I have so many questions, and all of them start with "why?"
|
||||
Well, that escalated quickly.
|
||||
This is fine. I'm okay with the events that are unfolding currently.
|
||||
We're all just one bad decision away from being in this video.
|
||||
Calculated. But man, am I bad at math.
|
||||
And the Darwin Award goes to...
|
||||
Me trying to get my life together.
|
||||
My last two brain cells trying to function.
|
||||
Monday hitting me like:
|
||||
Me pretending to be productive at work.
|
||||
My motivation leaving my body at 2 PM.
|
||||
Another meeting that could have been an email.
|
||||
Me running on 3 hours of sleep and 6 shots of espresso.
|
||||
Adulting is a scam.
|
||||
My bank account looking at me after I buy one (1) coffee.
|
||||
Me trying to follow a recipe.
|
||||
No thoughts, just vibes.
|
||||
The lights are on but nobody's home.
|
||||
He's a little confused, but he's got the spirit.
|
||||
Someone come get their cat, it's broken.
|
||||
This is what peak performance looks like.
|
||||
Not a single thought behind those eyes.
|
||||
The internal monologue here must be fascinating.
|
||||
He understood the assignment.
|
||||
My spirit animal.
|
||||
What the dog doin?
|
||||
The internet was a mistake.
|
||||
This is my entire personality now.
|
||||
Just a normal day on planet Earth.
|
||||
Explain this in historical terms.
|
||||
The vibe I'm trying to bring into 2025.
|
||||
And I'll f*cking do it again.
|
||||
Zero chaos in this video. None at all.
|
||||
This has been living in my head rent-free.
|
||||
POV: You gave them one (1) ounce of confidence.
|
||||
I don't get paid enough for this shit.
|
||||
94
src/comments.rs
Normal file
94
src/comments.rs
Normal file
@ -0,0 +1,94 @@
|
||||
use crate::error::{Error, Result};
|
||||
use rand::{rng, seq::IndexedRandom};
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
use tokio::fs::read_to_string;
|
||||
static DISCLAIMER: &str = "(Roleplay — fictional messages for entertainment.)";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Comments {
|
||||
pub disclaimer: String,
|
||||
lines: Arc<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Comments {
|
||||
/// Create a small dummy/default Comments instance (useful for tests or fallback).
|
||||
#[must_use]
|
||||
pub fn dummy() -> Self {
|
||||
let lines = vec![
|
||||
"Oh come on, that's brilliant — and slightly chaotic, like always.".into(),
|
||||
"That is a proper bit of craftsmanship — then someone presses the red button.".into(),
|
||||
"Nice shot — looks good on the trailer, not so good on the gearbox.".into(),
|
||||
"Here you go. Judge for yourself.".into(),
|
||||
];
|
||||
Self {
|
||||
disclaimer: DISCLAIMER.into(),
|
||||
lines: lines.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load comments from a plaintext file asynchronously.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - Returns `Error::Io` if reading the file fails (propagated from
|
||||
/// `tokio::fs::read_to_string`).
|
||||
/// - Returns `Error::Other` if the file contains no usable lines after
|
||||
/// filtering (empty or all-comment file).
|
||||
pub async fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
let s = read_to_string(path).await?;
|
||||
|
||||
let lines = s
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|l| !l.is_empty() && !l.starts_with('#'))
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if lines.is_empty() {
|
||||
return Err(Error::other("comments file contains no usable lines"));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
disclaimer: DISCLAIMER.into(),
|
||||
lines: lines.into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Pick a random comment as &str (no allocation). Falls back to a small static
|
||||
/// string if the list is unexpectedly empty.
|
||||
#[must_use]
|
||||
pub fn pick(&self) -> &str {
|
||||
let mut rng = rng();
|
||||
self.lines
|
||||
.choose(&mut rng)
|
||||
.map_or("Here you go.", String::as_str)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn build_caption(&self) -> String {
|
||||
self.pick().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
static GLOBAL_COMMENTS: OnceLock<Comments> = OnceLock::new();
|
||||
|
||||
/// Initialize the global comments (call once at startup).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - Returns `Error::Other` when the global is already initialized (the
|
||||
/// underlying `OnceLock::set` fails).
|
||||
pub fn init_global_comments(comments: Comments) -> Result<()> {
|
||||
GLOBAL_COMMENTS
|
||||
.set(comments)
|
||||
.map_err(|_| Error::other("comments already initialized"))
|
||||
}
|
||||
|
||||
/// Get global comments (if initialized). Returns Option<&'static Comments>.
|
||||
pub fn global_comments() -> Option<&'static Comments> {
|
||||
GLOBAL_COMMENTS.get()
|
||||
}
|
||||
@ -3,7 +3,7 @@ use thiserror::Error;
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
Io(#[from] tokio::io::Error),
|
||||
|
||||
#[error("instaloader failed: {0}")]
|
||||
InstaloaderFaileled(String),
|
||||
@ -24,4 +24,11 @@ pub enum Error {
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
#[inline]
|
||||
pub fn other(text: impl Into<String>) -> Self {
|
||||
Self::Other(text.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
pub mod comments;
|
||||
pub mod error;
|
||||
pub mod handlers;
|
||||
pub mod telemetry;
|
||||
|
||||
13
src/main.rs
13
src/main.rs
@ -2,10 +2,11 @@ use dotenv::dotenv;
|
||||
use std::sync::Arc;
|
||||
use teloxide::{Bot, prelude::Requester, respond, types::Message};
|
||||
use tg_relay_rs::{
|
||||
comments::{Comments, init_global_comments},
|
||||
handlers::{InstagramHandler, SocialHandler},
|
||||
telemetry::setup_logger,
|
||||
};
|
||||
use tracing::{error, info};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> color_eyre::Result<()> {
|
||||
@ -13,6 +14,16 @@ async fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install().expect("color-eyre install");
|
||||
setup_logger();
|
||||
|
||||
let comments = Comments::load_from_file("comments.txt")
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("failed to laod comments.txt: {}; using dummy comments", e);
|
||||
e
|
||||
})
|
||||
.unwrap_or_else(|_| Comments::dummy());
|
||||
|
||||
init_global_comments(comments).expect("failed to initialize global comments");
|
||||
|
||||
let bot = Bot::from_env();
|
||||
info!("bot starting");
|
||||
|
||||
|
||||
35
src/utils.rs
35
src/utils.rs
@ -1,15 +1,23 @@
|
||||
use crate::error::{Error, Result};
|
||||
use crate::{
|
||||
comments::global_comments,
|
||||
error::{Error, Result},
|
||||
};
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use teloxide::{
|
||||
Bot,
|
||||
payloads::{SendPhotoSetters, SendVideoSetters},
|
||||
prelude::Requester,
|
||||
types::{ChatId, InputFile},
|
||||
};
|
||||
use tokio::{fs::File, io::AsyncReadExt};
|
||||
|
||||
const TELEGRAM_CAPTION_LIMIT: usize = 1024;
|
||||
static VIDEO_EXTS: &[&str] = &["mp4", "webm", "mov", "mkv", "avi"];
|
||||
static IMAGE_EXTS: &[&str] = &["jpg", "jpeg", "png", "webp"];
|
||||
|
||||
/// Simple media kind enum shared by handlers.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MediaKind {
|
||||
@ -18,9 +26,6 @@ pub enum MediaKind {
|
||||
Unknown,
|
||||
}
|
||||
|
||||
static VIDEO_EXTS: &[&str] = &["mp4", "webm", "mov", "mkv", "avi"];
|
||||
static IMAGE_EXTS: &[&str] = &["jpg", "jpeg", "png", "webp"];
|
||||
|
||||
/// Detect media kind first by extension, then by content/magic (sync).
|
||||
/// NOTE: `infer::get_from_path` is blocking — use `detect_media_kind_async` in
|
||||
/// async contexts to avoid blocking the Tokio runtime.
|
||||
@ -94,14 +99,32 @@ pub async fn send_media_from_path(
|
||||
kind: Option<MediaKind>,
|
||||
) -> Result<()> {
|
||||
let kind = kind.unwrap_or_else(|| detect_media_kind(&path));
|
||||
|
||||
let caption_opt = global_comments().map(|c| {
|
||||
let mut caption = c.build_caption();
|
||||
if caption.chars().count() > TELEGRAM_CAPTION_LIMIT {
|
||||
caption = caption.chars().take(TELEGRAM_CAPTION_LIMIT - 1).collect();
|
||||
caption.push_str("...");
|
||||
}
|
||||
caption
|
||||
});
|
||||
|
||||
match kind {
|
||||
MediaKind::Video => {
|
||||
let video = InputFile::file(path);
|
||||
bot.send_video(chat_id, video).await.map_err(Error::from)?;
|
||||
let mut req = bot.send_video(chat_id, video);
|
||||
if let Some(c) = caption_opt {
|
||||
req = req.caption(c);
|
||||
}
|
||||
req.await.map_err(Error::from)?;
|
||||
}
|
||||
MediaKind::Image => {
|
||||
let photo = InputFile::file(path);
|
||||
bot.send_photo(chat_id, photo).await.map_err(Error::from)?;
|
||||
let mut req = bot.send_photo(chat_id, photo);
|
||||
if let Some(c) = caption_opt {
|
||||
req = req.caption(c);
|
||||
}
|
||||
req.await.map_err(Error::from)?;
|
||||
}
|
||||
MediaKind::Unknown => {
|
||||
bot.send_message(chat_id, "No supported media found")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user