use chrono::{DateTime, Local};
use comfy_table::Table;
use serde::Deserialize;
use std::time::Duration;
use table_to_html::HtmlTable;
use super::VersionedSource;
use crate::ops::check::{Options, Result};
pub mod display;
pub mod element;
pub mod mail;
pub mod mattermost;
pub(crate) const DEFAULT_MESSAGE_TEMPLATE: &str = "\
There was {{ count }} new packages versions {%if since %}since {{ date }}{% else %}in the last {{ duration }}{% endif %}
{{ versions_table }}
";
pub(crate) fn should_skip_empty(send_empty: Option<bool>, sources: &[VersionedSource]) -> bool {
!send_empty.unwrap_or(false) && sources.is_empty()
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub enum OutputKind {
Mattermost(mattermost::OutputConfig),
Element(element::OutputConfig),
Mail(mail::OutputConfig),
}
#[derive(Clone, Copy)]
pub(crate) enum TableFormat {
Html,
ComfyTable,
}
pub(crate) fn render_message(
opts: &Options,
sources: &[VersionedSource],
template: &str,
table_format: TableFormat,
) -> Result<String> {
use tera::{Context, Tera};
let count: usize = sources.iter().map(|e| e.versions.len()).sum();
let (duration, date) = compute_duration_and_date(opts.last, opts.since)?;
let table = if sources.is_empty() {
String::new()
} else {
match table_format {
TableFormat::Html => setup_table_html(sources).to_string(),
TableFormat::ComfyTable => {
let mut table = Table::new();
table
.load_style(comfy_table::presets::ASCII_MARKDOWN)
.set_content_arrangement(comfy_table::ContentArrangement::DynamicFullWidth)
.set_width(120);
setup_table(table, sources).to_string()
}
}
};
let mut tera = Tera::default();
tera.add_raw_template("mattermost_template", template)?;
let mut context = Context::new();
context.insert("count", &count);
context.insert("since", &opts.since);
context.insert("last", &opts.last);
context.insert("duration", &duration);
context.insert("date", &date);
context.insert("versions_table", &table);
Ok(tera.render("mattermost_template", &context)?)
}
pub fn create_table_with_defaults(sources: &[VersionedSource]) -> Table {
use comfy_table::{ContentArrangement, presets::UTF8_FULL};
let mut table = Table::new();
table
.load_style(UTF8_FULL)
.set_content_arrangement(ContentArrangement::DynamicFullWidth)
.set_width(100);
setup_table(table, sources)
}
pub fn setup_table(mut table: Table, sources: &[VersionedSource]) -> Table {
table.set_header(vec!["Label", "Version", "Source"]);
for source in sources {
for version in &source.versions {
table.add_row([
source.source.label_or_source(),
version.name(),
source.source.source.as_str(),
]);
}
}
table
}
fn escape_html(value: &str) -> String {
let mut buf = Vec::with_capacity(value.len());
let _ = tera::escape_html(value, &mut buf);
String::from_utf8(buf).unwrap_or_else(|_| value.to_owned())
}
pub fn setup_table_html(sources: &[VersionedSource]) -> HtmlTable {
let mut table = vec![[
"Label".to_owned(),
"Version".to_owned(),
"Source".to_owned(),
]];
for source in sources {
for version in &source.versions {
table.push([
escape_html(source.source.label_or_source()),
escape_html(version.name()),
escape_html(source.source.source.as_str()),
]);
}
}
HtmlTable::new(table)
}
pub(crate) fn compute_duration_and_date(
last: Option<Duration>,
since: Option<DateTime<Local>>,
) -> Result<(String, String)> {
use crate::util::date::{
date_from_duration, date_to_formatted_string, duration_from_date,
duration_to_formatted_string,
};
use chrono::Duration as chronoDuration;
Ok(match (last, since) {
(None, None) => (
duration_to_formatted_string(Duration::new(86400, 0)),
date_to_formatted_string(Local::now() - chronoDuration::days(1)),
),
(None, Some(since)) => (
duration_to_formatted_string(duration_from_date(since)?),
date_to_formatted_string(since),
),
(Some(last), _) => (
duration_to_formatted_string(last),
date_to_formatted_string(date_from_duration(last)?),
),
})
}
#[cfg(test)]
mod tests {
use super::setup_table_html;
use crate::ops::check::{Source, VersionedSource};
use crate::sources::list::Tag;
#[test]
fn html_table_escapes_remote_tag_names() {
let source: Source = "https://example.com/x".parse().unwrap();
let versioned = VersionedSource {
source,
versions: vec![Tag::new_without_date(
r#"v1.0<a href="https://evil.example/">Update now</a>"#,
)],
};
let html = setup_table_html(&[versioned]).to_string();
assert!(
!html.contains("<a href="),
"unescaped markup reached the notification: {html}"
);
assert!(html.contains("<a href="), "not escaped at all: {html}");
}
}