feat: add commentary

This commit is contained in:
Kristofers Solo 2025-09-19 21:18:26 +03:00
parent 36270c7a3f
commit 7a9ef2c48c
Signed by: kristoferssolo
GPG Key ID: 8687F2D3EEE6F0ED
8 changed files with 327 additions and 10 deletions

41
Cargo.lock generated
View File

@ -1149,6 +1149,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 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]] [[package]]
name = "proc-macro-error-attr2" name = "proc-macro-error-attr2"
version = "2.0.0" version = "2.0.0"
@ -1203,6 +1212,35 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 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]] [[package]]
name = "rc-box" name = "rc-box"
version = "1.3.0" version = "1.3.0"
@ -1705,8 +1743,9 @@ dependencies = [
"dotenv", "dotenv",
"futures", "futures",
"infer", "infer",
"once_cell", "rand",
"regex", "regex",
"serde",
"teloxide", "teloxide",
"tempfile", "tempfile",
"thiserror 2.0.16", "thiserror 2.0.16",

View File

@ -11,8 +11,9 @@ color-eyre = "0.6"
dotenv = "0.15" dotenv = "0.15"
futures = "0.3.31" futures = "0.3.31"
infer = "0.19" infer = "0.19"
once_cell = "1.21.3" rand = "0.9"
regex = "1.11" regex = "1.11"
serde = { version = "1.0", features = ["derive"] }
teloxide = { version = "0.17", features = ["macros"] } teloxide = { version = "0.17", features = ["macros"] }
tempfile = "3" tempfile = "3"
thiserror = "2.0" thiserror = "2.0"

141
comments.txt Normal file
View 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?
Hes 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
View 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()
}

View File

@ -3,7 +3,7 @@ use thiserror::Error;
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
#[error("io error: {0}")] #[error("io error: {0}")]
Io(#[from] std::io::Error), Io(#[from] tokio::io::Error),
#[error("instaloader failed: {0}")] #[error("instaloader failed: {0}")]
InstaloaderFaileled(String), InstaloaderFaileled(String),
@ -24,4 +24,11 @@ pub enum Error {
Other(String), 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>; pub type Result<T> = std::result::Result<T, Error>;

View File

@ -1,3 +1,4 @@
pub mod comments;
pub mod error; pub mod error;
pub mod handlers; pub mod handlers;
pub mod telemetry; pub mod telemetry;

View File

@ -2,10 +2,11 @@ use dotenv::dotenv;
use std::sync::Arc; use std::sync::Arc;
use teloxide::{Bot, prelude::Requester, respond, types::Message}; use teloxide::{Bot, prelude::Requester, respond, types::Message};
use tg_relay_rs::{ use tg_relay_rs::{
comments::{Comments, init_global_comments},
handlers::{InstagramHandler, SocialHandler}, handlers::{InstagramHandler, SocialHandler},
telemetry::setup_logger, telemetry::setup_logger,
}; };
use tracing::{error, info}; use tracing::{error, info, warn};
#[tokio::main] #[tokio::main]
async fn main() -> color_eyre::Result<()> { async fn main() -> color_eyre::Result<()> {
@ -13,6 +14,16 @@ async fn main() -> color_eyre::Result<()> {
color_eyre::install().expect("color-eyre install"); color_eyre::install().expect("color-eyre install");
setup_logger(); 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(); let bot = Bot::from_env();
info!("bot starting"); info!("bot starting");

View File

@ -1,15 +1,23 @@
use crate::error::{Error, Result}; use crate::{
comments::global_comments,
error::{Error, Result},
};
use std::{ use std::{
ffi::OsStr, ffi::OsStr,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use teloxide::{ use teloxide::{
Bot, Bot,
payloads::{SendPhotoSetters, SendVideoSetters},
prelude::Requester, prelude::Requester,
types::{ChatId, InputFile}, types::{ChatId, InputFile},
}; };
use tokio::{fs::File, io::AsyncReadExt}; 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. /// Simple media kind enum shared by handlers.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MediaKind { pub enum MediaKind {
@ -18,9 +26,6 @@ pub enum MediaKind {
Unknown, 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). /// Detect media kind first by extension, then by content/magic (sync).
/// NOTE: `infer::get_from_path` is blocking — use `detect_media_kind_async` in /// NOTE: `infer::get_from_path` is blocking — use `detect_media_kind_async` in
/// async contexts to avoid blocking the Tokio runtime. /// async contexts to avoid blocking the Tokio runtime.
@ -94,14 +99,32 @@ pub async fn send_media_from_path(
kind: Option<MediaKind>, kind: Option<MediaKind>,
) -> Result<()> { ) -> Result<()> {
let kind = kind.unwrap_or_else(|| detect_media_kind(&path)); 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 { match kind {
MediaKind::Video => { MediaKind::Video => {
let video = InputFile::file(path); 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 => { MediaKind::Image => {
let photo = InputFile::file(path); 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 => { MediaKind::Unknown => {
bot.send_message(chat_id, "No supported media found") bot.send_message(chat_id, "No supported media found")