pub mod actions;
pub mod cli;
pub mod config;
pub mod context;
pub mod error;
pub mod escape;
pub mod model;
pub mod output;
pub mod redact;
pub mod telegram;
pub mod template;
pub mod truncate;
use std::time::Duration;
use crate::actions::Level;
use crate::config::Config;
use crate::context::Context;
use crate::error::Result;
use crate::output::Report;
use crate::telegram::client::{Client, HttpTransport};
use crate::telegram::request::Request;
use crate::telegram::retry::{RetryPolicy, SystemClock};
use crate::template::{TemplateSource, render};
use crate::truncate::truncate;
#[derive(Debug, Clone, Copy, Default)]
pub struct RunOptions {
pub dry_run: bool,
}
#[derive(Debug, Clone)]
pub struct RenderResult {
pub text: String,
pub source: TemplateSource,
pub unknown_placeholders: Vec<String>,
pub truncated: bool,
}
pub fn render_message(config: &Config, ctx: &Context) -> RenderResult {
let selected = config.templates.select(config.status);
let (text, unknown) = match selected.source {
TemplateSource::VerbatimMessage => (selected.body.clone(), Vec::new()),
_ => {
let rendered = render(&selected.body, ctx, config.parse_mode);
(rendered.text, rendered.unknown)
}
};
for name in &unknown {
actions::log(Level::Warning, &format!("UNKNOWN_PLACEHOLDER:{name}"));
}
let truncated_text = truncate(&text, config.parse_mode);
let truncated = truncated_text != text;
if truncated {
actions::log(
Level::Warning,
"message exceeded Telegram's 4096-unit limit and was truncated",
);
}
RenderResult {
text: truncated_text,
source: selected.source,
unknown_placeholders: unknown,
truncated,
}
}
pub fn build_request(config: &Config, text: String) -> Request {
Request {
chat_id: config.chat_id.clone(),
text,
parse_mode: config.parse_mode,
disable_web_page_preview: config.disable_web_page_preview,
disable_notification: config.disable_notification,
message_thread_id: config.message_thread_id,
edit_message_id: config.edit_message_id,
}
}
pub fn run(config: &Config, options: RunOptions) -> Result<Report> {
if !config.should_notify() {
actions::log(
Level::Notice,
&format!("skipping (status={} not in notify_on={})", config.status, config.notify_on),
);
return Ok(Report::skipped(config.chat_id.to_string()));
}
let ctx = Context::discover(config.status, !actions::is_github_actions());
let rendered = render_message(config, &ctx);
let request = build_request(config, rendered.text);
if options.dry_run {
return Ok(Report {
ok: false,
skipped: false,
message_id: None,
http_status: 0,
error: None,
chat_id: config.chat_id.to_string(),
attempts: 0,
dry_run: true,
request: Some(request.to_json()),
});
}
let transport = HttpTransport::new(
Duration::from_secs(config.connect_timeout),
Duration::from_secs(config.timeout),
);
let client = Client::new(&config.api_base, &config.bot_token, transport);
let policy = RetryPolicy {
max_attempts: config.retries + 1,
max_retry_after: config.max_retry_after,
jitter: true,
};
let outcome = telegram::deliver(&client, &request, policy, &SystemClock, &|level, msg| {
actions::log(level, msg)
});
Ok(Report {
ok: outcome.ok,
skipped: false,
message_id: outcome.message_id,
http_status: outcome.http_status,
error: outcome.error,
chat_id: config.chat_id.to_string(),
attempts: outcome.attempts,
dry_run: false,
request: None,
})
}
pub const VERSION: &str = env!("CARGO_PKG_VERSION");