aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/categories.rs101
-rw-r--r--src/config.rs7
-rw-r--r--src/discord.rs14
-rw-r--r--src/logbag.rs278
-rw-r--r--src/main.rs80
-rw-r--r--src/matrix.rs11
6 files changed, 346 insertions, 145 deletions
diff --git a/src/categories.rs b/src/categories.rs
index de4f5e6..1713d87 100644
--- a/src/categories.rs
+++ b/src/categories.rs
@@ -1,48 +1,115 @@
use evtclib::{Encounter, GameMode, Log};
+// There is no canonical order of "categories", so we'll do
+// * WvW
+// * Raid wings ascending
+// * Strike
+// * Fractals descending
+// * Training area
+// * Unknown
+// Subject to change, it's only used to sort the message (if enabled).
+#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
+pub enum Category {
+ WvW,
+ Wing1,
+ Wing2,
+ Wing3,
+ Wing4,
+ Wing5,
+ Wing6,
+ Wing7,
+ Strike,
+ SunquaPeak,
+ ShatteredObservatory,
+ Nightmare,
+ SpecialForcesTrainingArea,
+ #[default]
+ Unknown,
+}
+
+macro_rules! category_strings {
+ ($(($item:path, $str:literal),)*) => {
+ impl std::fmt::Display for Category {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ let repr = match *self {
+ $($item => $str),*
+ };
+ write!(f, "{}", repr)
+ }
+ }
+
+ impl std::str::FromStr for Category {
+ type Err = ();
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ $($str => Ok($item),)*
+ _ => Err(()),
+ }
+ }
+ }
+ }
+}
+
+category_strings! {
+ (Category::WvW, "World versus World"),
+ (Category::Wing1, "Wing 1 (Spirit Vale)"),
+ (Category::Wing2, "Wing 2 (Salvation Pass)"),
+ (Category::Wing3, "Wing 3 (Stronghold of the Faithful)"),
+ (Category::Wing4, "Wing 4 (Bastion of the Penitent)"),
+ (Category::Wing5, "Wing 5 (Hall of Chains)"),
+ (Category::Wing6, "Wing 6 (Mythwright Gambit)"),
+ (Category::Wing7, "Wing 7 (Key of Ahdashim)"),
+ (Category::SunquaPeak, "100 CM (Sunqua Peak)"),
+ (Category::ShatteredObservatory, "99 CM (Shattered Observatory)"),
+ (Category::Nightmare, "98 CM (Nightmare)"),
+ (Category::Strike, "Strike Mission"),
+ (Category::SpecialForcesTrainingArea, "Special Forces Training Area"),
+ (Category::Unknown, "Unknown"),
+}
+
pub trait Categorizable {
- fn category(&self) -> &'static str;
+ fn category(&self) -> Category;
}
impl Categorizable for Log {
- fn category(&self) -> &'static str {
+ fn category(&self) -> Category {
if self.game_mode() == Some(GameMode::WvW) {
- return "World versus World";
+ return Category::WvW;
}
if let Some(encounter) = self.encounter() {
match encounter {
Encounter::ValeGuardian | Encounter::Gorseval | Encounter::Sabetha => {
- "Wing 1 (Spirit Vale)"
+ Category::Wing1
}
Encounter::Slothasor | Encounter::BanditTrio | Encounter::Matthias => {
- "Wing 2 (Salvation Pass)"
+ Category::Wing2
}
Encounter::KeepConstruct | Encounter::TwistedCastle | Encounter::Xera => {
- "Wing 3 (Stronghold of the Faithful)"
+ Category::Wing3
}
Encounter::Cairn
| Encounter::MursaatOverseer
| Encounter::Samarog
- | Encounter::Deimos => "Wing 4 (Bastion of the Penitent)",
+ | Encounter::Deimos => Category::Wing4,
Encounter::SoullessHorror
| Encounter::RiverOfSouls
| Encounter::BrokenKing
| Encounter::EaterOfSouls
| Encounter::StatueOfDarkness
- | Encounter::VoiceInTheVoid => "Wing 5 (Hall of Chains)",
+ | Encounter::VoiceInTheVoid => Category::Wing5,
Encounter::ConjuredAmalgamate | Encounter::TwinLargos | Encounter::Qadim => {
- "Wing 6 (Mythwright Gambit)"
+ Category::Wing6
}
Encounter::CardinalAdina
| Encounter::CardinalSabir
- | Encounter::QadimThePeerless => "Wing 7 (Key of Ahdashim)",
+ | Encounter::QadimThePeerless => Category::Wing7,
- Encounter::Ai => "100 CM (Sunqua Peak)",
+ Encounter::Ai => Category::SunquaPeak,
Encounter::Skorvald | Encounter::Artsariiv | Encounter::Arkk => {
- "99 CM (Shattered Observatory)"
+ Category::ShatteredObservatory
}
- Encounter::MAMA | Encounter::Siax | Encounter::Ensolyss => "98 CM (Nightmare)",
+ Encounter::MAMA | Encounter::Siax | Encounter::Ensolyss => Category::Nightmare,
Encounter::IcebroodConstruct
| Encounter::SuperKodanBrothers
@@ -52,16 +119,16 @@ impl Categorizable for Log {
| Encounter::CaptainMaiTrin
| Encounter::Ankka
| Encounter::MinisterLi
- | Encounter::Dragonvoid => "Strike Mission",
+ | Encounter::Dragonvoid => Category::Strike,
Encounter::StandardKittyGolem
| Encounter::MediumKittyGolem
- | Encounter::LargeKittyGolem => "Special Forces Training Area",
+ | Encounter::LargeKittyGolem => Category::SpecialForcesTrainingArea,
- _ => "Unknown",
+ _ => Category::Unknown,
}
} else {
- "Unknown"
+ Category::Unknown
}
}
}
diff --git a/src/config.rs b/src/config.rs
index ad2ee22..6c2a035 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -9,6 +9,9 @@ use serde_with::{serde_as, OneOrMany};
pub struct Config {
/// Flag indicating whether logs should be uploaded or not.
pub upload: bool,
+ /// Whether to sort the logs when posting them.
+ #[serde(default)]
+ pub sort_logs: bool,
/// Where to upload the logs.
#[serde(default = "default_dps_report_upload_url")]
pub dps_report_upload_url: String,
@@ -23,6 +26,8 @@ pub struct Config {
pub retries: u32,
/// Whether ezau should zip non-zipped logs.
#[serde(default = "default_zip")]
+ // We don't use this anymore, but we keep it so old configs can be parsed and we can properly
+ // inform the user.
pub zip: bool,
/// Option Discord information for bot postings.
pub discord: Option<Discord>,
@@ -63,7 +68,7 @@ pub fn load<P: AsRef<Path>>(path: P) -> Result<Config> {
}
fn default_zip() -> bool {
- true
+ false
}
fn default_dps_report_upload_url() -> String {
diff --git a/src/discord.rs b/src/discord.rs
index 3ab849c..eeb4210 100644
--- a/src/discord.rs
+++ b/src/discord.rs
@@ -10,6 +10,7 @@ use tokio::runtime::Runtime;
use log::info;
+use super::config::Config;
use super::categories::Categorizable;
use super::logbag::{state_emoji, LogBag};
@@ -33,6 +34,7 @@ struct Handler {
channel_id: u64,
log: Log,
link: String,
+ sort_logs: bool,
}
impl Handler {
@@ -59,14 +61,14 @@ impl Handler {
}
if let Some(mut m) = messages.pop() {
- let new_text = insert_link(&m.content, &self.log, &self.link);
+ let new_text = insert_link(&m.content, &self.log, &self.link, self.sort_logs);
if new_text.len() <= MAX_MESSAGE_LENGTH {
m.edit(ctx, |m| m.content(new_text)).await?;
return Ok(());
}
}
- let new_text = insert_link("", &self.log, &self.link);
+ let new_text = insert_link("", &self.log, &self.link, false);
ChannelId(self.channel_id).say(ctx, new_text).await?;
Ok(())
@@ -88,7 +90,7 @@ impl EventHandler for Handler {
}
}
-pub fn post_link(discord_token: &str, channel_id: u64, log: &Log, link: &str) -> Result<()> {
+pub fn post_link(config: &Config, discord_token: &str, channel_id: u64, log: &Log, link: &str) -> Result<()> {
let link = link.to_owned();
let log = log.clone();
let rt = Runtime::new()?;
@@ -99,6 +101,7 @@ pub fn post_link(discord_token: &str, channel_id: u64, log: &Log, link: &str) ->
channel_id,
log,
link,
+ sort_logs: config.sort_logs,
})
.await?;
{
@@ -112,9 +115,12 @@ pub fn post_link(discord_token: &str, channel_id: u64, log: &Log, link: &str) ->
})
}
-fn insert_link(text: &str, log: &Log, link: &str) -> String {
+fn insert_link(text: &str, log: &Log, link: &str, sort_logs: bool) -> String {
let mut logbag = LogBag::parse_markdown(text).unwrap();
let line = format!("{} {}", state_emoji(log), link);
logbag.insert(log.category(), line);
+ if sort_logs {
+ logbag.sort();
+ }
logbag.render_markdown()
}
diff --git a/src/logbag.rs b/src/logbag.rs
index 362bee5..f173d6e 100644
--- a/src/logbag.rs
+++ b/src/logbag.rs
@@ -1,8 +1,10 @@
-use std::iter;
-use std::str::FromStr;
+use super::categories::Category;
-use evtclib::{Log, Outcome};
+use std::{cmp::Ordering, sync::OnceLock, str::FromStr};
+
+use evtclib::{Encounter, Log, Outcome};
use itertools::Itertools;
+use regex::Regex;
/// A [`LogBag`] is a struct that holds multiple logs in their categories.
///
@@ -11,7 +13,7 @@ use itertools::Itertools;
/// them and we can just handle arbitrary data.
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
pub struct LogBag {
- data: Vec<(String, Vec<String>)>,
+ data: Vec<(Category, Vec<String>)>,
}
// Conditional compilation makes it hard to really use all the code, so we just allow dead code
@@ -24,30 +26,54 @@ impl LogBag {
}
/// Return an iterator over all available categories.
- pub fn categories(&self) -> impl Iterator<Item = &str> {
- self.data.iter().map(|x| &x.0 as &str)
+ pub fn categories(&self) -> impl Iterator<Item = Category> + '_ {
+ self.data.iter().map(|x| x.0)
}
/// Return an iterator over (category, items).
- pub fn iter(&self) -> impl Iterator<Item = (&str, impl Iterator<Item = &str>)> {
+ pub fn iter(&self) -> impl Iterator<Item = (Category, impl Iterator<Item = &str>)> {
self.data
.iter()
- .map(|(cat, lines)| (cat as &str, lines.iter().map(|l| l as &str)))
+ .map(|(cat, lines)| (*cat, lines.iter().map(|l| l as &str)))
}
/// Insert an item into the given category.
///
/// If the category does not exist yet, it will be appended at the bottom.
- pub fn insert(&mut self, category: &str, line: String) {
+ pub fn insert(&mut self, category: Category, line: String) {
for (cat, lines) in self.data.iter_mut() {
- if cat == category {
+ if *cat == category {
lines.push(line);
return;
}
}
// When we reach here, we don't have the category yet, so we gotta insert it.
- self.data.push((String::from(category), vec![line]));
+ self.data.push((category, vec![line]));
+ }
+
+ /// Sort the categories and logs.
+ pub fn sort(&mut self) {
+ // First sort the categories, the Ord impl of `Category` takes care here
+ self.data.sort();
+ // Then sort the lines within the categories
+ for (_, ref mut lines) in self.data.iter_mut() {
+ lines.sort_by(|a, b| {
+ let (encounter_a, date_a, url_a) = info_from_line(&a);
+ let (encounter_b, date_b, url_b) = info_from_line(&b);
+ match (encounter_a, encounter_b) {
+ (None, None) => date_a.cmp(&date_b),
+ (None, Some(_)) => Ordering::Greater,
+ (Some(_), None) => Ordering::Less,
+ (Some(encounter_a), Some(encounter_b)) => encounter_a.partial_cmp(&encounter_b)
+ .or_else(|| order_strikes(encounter_a, encounter_b))
+ // at this point, just give us a stable order
+ .unwrap_or((encounter_a as u16).cmp(&(encounter_b as u16)))
+ .then(date_a.cmp(&date_b))
+ .then(url_a.cmp(&url_b)),
+ }
+ });
+ }
}
/// Tries to parse the given text as a plain [`LogBag`].
@@ -68,7 +94,7 @@ impl LogBag {
/// The output of this can be fed back into [`LogBag::parse_plain`] to round-trip.
pub fn render_plain(&self) -> String {
self.iter()
- .map(|(category, lines)| iter::once(category).chain(lines).join("\n"))
+ .map(|(category, mut lines)| category.to_string() + "\n" + &lines.join("\n"))
.join("\n\n")
}
@@ -104,18 +130,18 @@ impl FromStr for LogBag {
let mut lines = chunk.split('\n');
let category = lines.next().unwrap();
(
- category.to_string(),
+ category.parse::<Category>().unwrap_or_default(),
lines.map(ToString::to_string).collect::<Vec<_>>(),
)
})
- .filter(|(cat, lines)| !cat.is_empty() && !lines.is_empty())
+ .filter(|(_, lines)| !lines.is_empty())
.collect();
Ok(LogBag { data })
}
}
-impl From<Vec<(String, Vec<String>)>> for LogBag {
- fn from(data: Vec<(String, Vec<String>)>) -> Self {
+impl From<Vec<(Category, Vec<String>)>> for LogBag {
+ fn from(data: Vec<(Category, Vec<String>)>) -> Self {
LogBag { data }
}
}
@@ -130,6 +156,100 @@ pub fn state_emoji(log: &Log) -> &'static str {
}
}
+// I don't want to pull in something like `chrono` just to represent a local datetime. Therefore,
+// we simply use two integers: The date in the format YYYYMMDD (which automatically sorts
+// correctly), and the time in HHMMSS (which does as well).
+fn info_from_line(line: &str) -> (Option<Encounter>, Option<(u32, u32)>, &str) {
+ static RE: OnceLock<Regex> = OnceLock::new();
+ let url_re = RE.get_or_init(|| {
+ Regex::new("http(s?)://[^ ]+(?P<date>\\d{8})-(?P<time>\\d{6})_(?P<slug>[a-z]+)").unwrap()
+ });
+ let Some(caps) = url_re.captures(line) else { return (None, None, line); };
+ let date = caps
+ .name("date")
+ .expect("date group must be present")
+ .as_str()
+ .parse()
+ .expect("matched only digits, parsing should succeeed");
+ let time = caps
+ .name("time")
+ .expect("time group must be present")
+ .as_str()
+ .parse()
+ .expect("matched only digits, parsing should succeeed");
+
+ let encounter = match caps.name("slug").expect("slug group must be present").as_str() {
+ "vg" => Some(Encounter::ValeGuardian),
+ "gors" => Some(Encounter::Gorseval),
+ "sab" => Some(Encounter::Sabetha),
+ "sloth" => Some(Encounter::Slothasor),
+ "trio" => Some(Encounter::BanditTrio),
+ "matt" => Some(Encounter::Matthias),
+ "kc" => Some(Encounter::KeepConstruct),
+ "tc" => Some(Encounter::TwistedCastle),
+ "xera" => Some(Encounter::Xera),
+ "cairn" => Some(Encounter::Cairn),
+ "mo" => Some(Encounter::MursaatOverseer),
+ "sam" => Some(Encounter::Samarog),
+ "dei" => Some(Encounter::Deimos),
+ "sh" => Some(Encounter::SoullessHorror),
+ "rr" => Some(Encounter::RiverOfSouls),
+ "bk" => Some(Encounter::BrokenKing),
+ "se" => Some(Encounter::EaterOfSouls),
+ "eyes" => Some(Encounter::StatueOfDarkness),
+ "dhuum" => Some(Encounter::VoiceInTheVoid),
+ "ca" => Some(Encounter::ConjuredAmalgamate),
+ "twins" => Some(Encounter::TwinLargos),
+ "qadim" => Some(Encounter::Qadim),
+ "adina" => Some(Encounter::CardinalAdina),
+ "sabir" => Some(Encounter::CardinalSabir),
+ "qpeer" => Some(Encounter::QadimThePeerless),
+ // ambiguous, but it doesn't matter because we map them all to
+ // Category::SpecialForcesTrainingArea
+ "golem" => Some(Encounter::LargeKittyGolem),
+ "ai" => Some(Encounter::Ai),
+ "skor" => Some(Encounter::Skorvald),
+ "arriv" => Some(Encounter::Artsariiv),
+ "arkk" => Some(Encounter::Arkk),
+ "mama" => Some(Encounter::MAMA),
+ "siax" => Some(Encounter::Siax),
+ "enso" => Some(Encounter::Ensolyss),
+ "ice" => Some(Encounter::IcebroodConstruct),
+ "frae" => Some(Encounter::FraenirOfJormag),
+ "falln" => Some(Encounter::SuperKodanBrothers),
+ "whisp" => Some(Encounter::WhisperOfJormag),
+ "bone" => Some(Encounter::Boneskinner),
+ "trin" => Some(Encounter::CaptainMaiTrin),
+ "ankka" => Some(Encounter::Ankka),
+ "li" => Some(Encounter::MinisterLi),
+ "void" => Some(Encounter::Dragonvoid),
+ _ => None,
+ };
+
+ (encounter, Some((date, time)), caps.get(0).unwrap().as_str())
+}
+
+fn order_strikes(left: Encounter, right: Encounter) -> Option<Ordering> {
+ // Order according to the wiki at https://wiki.guildwars2.com/wiki/Strike_Mission
+ let strikes = &[
+ Encounter::IcebroodConstruct,
+ Encounter::SuperKodanBrothers,
+ Encounter::FraenirOfJormag,
+ Encounter::Boneskinner,
+ Encounter::WhisperOfJormag,
+ Encounter::CaptainMaiTrin,
+ Encounter::Ankka,
+ Encounter::MinisterLi,
+ Encounter::Dragonvoid,
+ ];
+ if let Some(pos_a) = strikes.iter().position(|x| *x == left) {
+ if let Some(pos_b) = strikes.iter().position(|x| *x == right) {
+ return Some(pos_a.cmp(&pos_b));
+ }
+ }
+ None
+}
+
#[cfg(test)]
mod test {
use super::*;
@@ -137,16 +257,16 @@ mod test {
#[test]
fn insert() {
let mut logbag = LogBag::new();
- logbag.insert("cat 1", "line 1".to_string());
+ logbag.insert(Category::WvW, "line 1".to_string());
assert_eq!(logbag.categories().count(), 1);
- logbag.insert("cat 1", "line 2".to_string());
+ logbag.insert(Category::WvW, "line 2".to_string());
assert_eq!(logbag.categories().count(), 1);
- logbag.insert("cat 2", "line 1".to_string());
+ logbag.insert(Category::Strike, "line 1".to_string());
assert_eq!(logbag.categories().count(), 2);
assert_eq!(
logbag.categories().collect::<Vec<_>>(),
- vec!["cat 1", "cat 2"]
+ vec![Category::WvW, Category::Strike]
);
}
@@ -158,13 +278,13 @@ mod test {
#[test]
fn parse_single() {
let mut logbag = LogBag::new();
- logbag.insert("cat 1", "line 1".to_string());
- logbag.insert("cat 1", "line 2".to_string());
+ logbag.insert(Category::WvW, "line 1".to_string());
+ logbag.insert(Category::WvW, "line 2".to_string());
assert_eq!(
LogBag::parse_plain(
"\
-cat 1
+World versus World
line 1
line 2"
),
@@ -175,19 +295,19 @@ line 2"
#[test]
fn parse_multi() {
let mut logbag = LogBag::new();
- logbag.insert("cat 1", "line 1".to_string());
- logbag.insert("cat 1", "line 2".to_string());
- logbag.insert("cat 2", "line 1".to_string());
- logbag.insert("cat 2", "line 2".to_string());
+ logbag.insert(Category::WvW, "line 1".to_string());
+ logbag.insert(Category::WvW, "line 2".to_string());
+ logbag.insert(Category::Strike, "line 1".to_string());
+ logbag.insert(Category::Strike, "line 2".to_string());
assert_eq!(
LogBag::parse_plain(
"\
-cat 1
+World versus World
line 1
line 2
-cat 2
+Strike Mission
line 1
line 2"
),
@@ -198,19 +318,19 @@ line 2"
#[test]
fn parse_markdown() {
let mut logbag = LogBag::new();
- logbag.insert("cat 1", "line 1".to_string());
- logbag.insert("cat 1", "line 2".to_string());
- logbag.insert("cat 2", "line 1".to_string());
- logbag.insert("cat 2", "line 2".to_string());
+ logbag.insert(Category::WvW, "line 1".to_string());
+ logbag.insert(Category::WvW, "line 2".to_string());
+ logbag.insert(Category::Strike, "line 1".to_string());
+ logbag.insert(Category::Strike, "line 2".to_string());
assert_eq!(
LogBag::parse_markdown(
"\
-**cat 1**
+**World versus World**
line 1
line 2
-**cat 2**
+**Strike Mission**
line 1
line 2"
),
@@ -221,13 +341,13 @@ line 2"
#[test]
fn render_plain_single() {
let mut logbag = LogBag::new();
- logbag.insert("category", "line 1".to_string());
- logbag.insert("category", "line 2".to_string());
+ logbag.insert(Category::Unknown, "line 1".to_string());
+ logbag.insert(Category::Unknown, "line 2".to_string());
assert_eq!(
logbag.render_plain(),
"\
-category
+Unknown
line 1
line 2"
);
@@ -236,19 +356,19 @@ line 2"
#[test]
fn render_plain_multi() {
let mut logbag = LogBag::new();
- logbag.insert("category 1", "line 1".to_string());
- logbag.insert("category 1", "line 2".to_string());
- logbag.insert("category 2", "enil 1".to_string());
- logbag.insert("category 2", "enil 2".to_string());
+ logbag.insert(Category::WvW, "line 1".to_string());
+ logbag.insert(Category::WvW, "line 2".to_string());
+ logbag.insert(Category::SpecialForcesTrainingArea, "enil 1".to_string());
+ logbag.insert(Category::SpecialForcesTrainingArea, "enil 2".to_string());
assert_eq!(
logbag.render_plain(),
"\
-category 1
+World versus World
line 1
line 2
-category 2
+Special Forces Training Area
enil 1
enil 2"
);
@@ -257,13 +377,13 @@ enil 2"
#[test]
fn render_html_single() {
let mut logbag = LogBag::new();
- logbag.insert("category", "line 1".to_string());
- logbag.insert("category", "line 2".to_string());
+ logbag.insert(Category::WvW, "line 1".to_string());
+ logbag.insert(Category::WvW, "line 2".to_string());
assert_eq!(
logbag.render_html(),
"\
-<b>category</b><br>
+<b>World versus World</b><br>
line 1<br>
line 2"
);
@@ -272,19 +392,19 @@ line 2"
#[test]
fn render_html_multi() {
let mut logbag = LogBag::new();
- logbag.insert("category 1", "line 1".to_string());
- logbag.insert("category 1", "line 2".to_string());
- logbag.insert("category 2", "enil 1".to_string());
- logbag.insert("category 2", "enil 2".to_string());
+ logbag.insert(Category::WvW, "line 1".to_string());
+ logbag.insert(Category::WvW, "line 2".to_string());
+ logbag.insert(Category::Unknown, "enil 1".to_string());
+ logbag.insert(Category::Unknown, "enil 2".to_string());
assert_eq!(
logbag.render_html(),
"\
-<b>category 1</b><br>
+<b>World versus World</b><br>
line 1<br>
line 2<br>
<br>
-<b>category 2</b><br>
+<b>Unknown</b><br>
enil 1<br>
enil 2"
);
@@ -293,13 +413,13 @@ enil 2"
#[test]
fn render_markdown_single() {
let mut logbag = LogBag::new();
- logbag.insert("category", "line 1".to_string());
- logbag.insert("category", "line 2".to_string());
+ logbag.insert(Category::WvW, "line 1".to_string());
+ logbag.insert(Category::WvW, "line 2".to_string());
assert_eq!(
logbag.render_markdown(),
"\
-**category**
+**World versus World**
line 1
line 2"
);
@@ -308,21 +428,55 @@ line 2"
#[test]
fn render_markdown_multi() {
let mut logbag = LogBag::new();
- logbag.insert("category 1", "line 1".to_string());
- logbag.insert("category 1", "line 2".to_string());
- logbag.insert("category 2", "enil 1".to_string());
- logbag.insert("category 2", "enil 2".to_string());
+ logbag.insert(Category::Strike, "line 1".to_string());
+ logbag.insert(Category::Strike, "line 2".to_string());
+ logbag.insert(Category::SpecialForcesTrainingArea, "enil 1".to_string());
+ logbag.insert(Category::SpecialForcesTrainingArea, "enil 2".to_string());
assert_eq!(
logbag.render_markdown(),
"\
-**category 1**
+**Strike Mission**
line 1
line 2
-**category 2**
+**Special Forces Training Area**
enil 1
enil 2"
);
}
+
+ #[test]
+ fn test_info_from_line() {
+ assert_eq!(
+ info_from_line("✅ https://dps.report/O514-20240827-214630_dhuum"),
+ (Some(Encounter::VoiceInTheVoid), Some((20240827, 214630)), "https://dps.report/O514-20240827-214630_dhuum")
+ );
+ }
+
+ #[test]
+ fn test_sort_logbag() {
+ let mut logbag = LogBag::new();
+ logbag.insert(Category::Wing2, String::from("https://dps.report/abcd-20240101-120000_matt"));
+ logbag.insert(Category::Wing2, String::from("https://dps.report/abcd-20240101-115500_matt"));
+ logbag.insert(Category::Wing2, String::from("https://dps.report/abcd-20240101-130000_sloth"));
+ logbag.insert(Category::Wing1, String::from("https://dps.report/abcd-20240101-120000_gors"));
+ logbag.insert(Category::Wing1, String::from("https://dps.report/abcd-20240101-120000_vg"));
+ logbag.insert(Category::Wing1, String::from("https://dps.report/abcd-20240101-120000_sab"));
+
+ logbag.sort();
+
+ assert_eq!(logbag.data, vec![
+ (Category::Wing1, vec![
+ String::from("https://dps.report/abcd-20240101-120000_vg"),
+ String::from("https://dps.report/abcd-20240101-120000_gors"),
+ String::from("https://dps.report/abcd-20240101-120000_sab"),
+ ]),
+ (Category::Wing2, vec![
+ String::from("https://dps.report/abcd-20240101-130000_sloth"),
+ String::from("https://dps.report/abcd-20240101-115500_matt"),
+ String::from("https://dps.report/abcd-20240101-120000_matt"),
+ ]),
+ ]);
+ }
}
diff --git a/src/main.rs b/src/main.rs
index 3e84e2d..482ec53 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,20 +1,17 @@
use std::{
- fs::{self, File},
- io::{BufReader, BufWriter, Read, Write},
path::{Path, PathBuf},
sync::mpsc::channel,
thread,
time::Duration,
};
-use anyhow::{anyhow, bail, Context, Result};
+use anyhow::{bail, Context, Result};
use clap::Parser;
use evtclib::{Compression, Encounter, Log};
use log::{debug, error, info, warn};
use notify::{self, DebouncedEvent, RecursiveMode, Watcher};
use regex::Regex;
use serde::Deserialize;
-use zip::{CompressionMethod, ZipArchive, ZipWriter};
mod categories;
use categories::Categorizable;
@@ -48,7 +45,7 @@ enum SubCommand {
/// Use the watch mode to automatically handle new logs.
///
-/// This watches the given directory for new files and then zips and uploads them.
+/// This watches the given directory for new files and then uploads them.
#[derive(Parser, Debug, Clone, PartialEq, Eq, Hash)]
struct Watch {
/// The directory to watch.
@@ -87,13 +84,13 @@ fn inner_main(opts: &Opts) -> Result<()> {
let log = load_log(&u.path)?;
if let Some(d) = &config.discord {
- discord::post_link(&d.auth_token, d.channel_id, &log, &permalink)
+ discord::post_link(&config, &d.auth_token, d.channel_id, &log, &permalink)
.context("Could not post link to Discord")?;
}
if let Some(m) = &config.matrix {
for room_id in &m.room_id {
- matrix::post_link(m.clone().into(), room_id, &log, &permalink).context(
+ matrix::post_link(&config, m.clone().into(), room_id, &log, &permalink).context(
format!("Could not post link to Matrix (room_id: {})", &room_id),
)?;
}
@@ -104,8 +101,7 @@ fn inner_main(opts: &Opts) -> Result<()> {
}
fn watch(watch: &Watch, config: &Config) -> Result<()> {
- let raw_evtc_re = Regex::new(r"\d{8}-\d{6}(\.evtc)?$").unwrap();
- let zip_evtc_re = Regex::new(r"(\.zip|\.zevtc)$").unwrap();
+ let zip_evtc_re = Regex::new(r"(\.evtc\.zip|\.zevtc)$").unwrap();
let (tx, rx) = channel();
let mut watcher = notify::watcher(tx, Duration::from_secs(WATCH_DELAY_SECONDS))?;
@@ -118,54 +114,13 @@ fn watch(watch: &Watch, config: &Config) -> Result<()> {
debug!("Event: {:?}", event);
if let DebouncedEvent::Create(path) = event {
let path_str = path.to_str().unwrap();
- // Check if we need to zip it first.
- if config.zip && raw_evtc_re.is_match(path_str) {
- info!("Zipping up {}", path_str);
- zip_file(&path)?;
- } else if zip_evtc_re.is_match(path_str) {
+ if zip_evtc_re.is_match(path_str) {
handle_file(config, &path)?;
}
}
}
}
-fn zip_file(filepath: &Path) -> Result<()> {
- let evtc_content = fs::read(filepath)?;
-
- let filename = filepath
- .file_name()
- .ok_or_else(|| anyhow!("Path does not have a file name"))?
- .to_str()
- .ok_or_else(|| anyhow!("Filename is invalid utf-8"))?;
- let outname = filepath.with_extension("zevtc");
- let outfile = BufWriter::new(File::create(&outname)?);
- let mut zip = ZipWriter::new(outfile);
- let options =
- zip::write::FileOptions::default().compression_method(CompressionMethod::Deflated);
-
- zip.start_file(filename, options)?;
- zip.write_all(&evtc_content)?;
- zip.finish()?.flush()?;
-
- if !verify_zip(filepath, &outname)? {
- warn!("ZIP content mismatch, keeping original file");
- return Ok(());
- }
-
- fs::remove_file(filepath)?;
- Ok(())
-}
-
-fn verify_zip(original: &Path, zip_path: &Path) -> Result<bool> {
- let expected_content = fs::read(original)?;
- let mut archive = ZipArchive::new(BufReader::new(File::open(zip_path)?))?;
- let mut inner = archive.by_index(0)?;
- let mut actual_content = Vec::new();
- inner.read_to_end(&mut actual_content)?;
-
- Ok(expected_content == actual_content)
-}
-
fn handle_file(config: &Config, filename: &Path) -> Result<()> {
if !config.upload {
return Ok(());
@@ -199,14 +154,14 @@ fn handle_file(config: &Config, filename: &Path) -> Result<()> {
info!("Uploaded log, available at {}", permalink);
if let Some(d) = &config.discord {
- discord::post_link(&d.auth_token, d.channel_id, &log, &permalink)
+ discord::post_link(config, &d.auth_token, d.channel_id, &log, &permalink)
.context("Could not post link to Discord")?;
info!("Posted link to Discord");
}
if let Some(m) = &config.matrix {
for room_id in &m.room_id {
- matrix::post_link(m.clone().into(), room_id, &log, &permalink).context(format!(
+ matrix::post_link(config, m.clone().into(), room_id, &log, &permalink).context(format!(
"Could not post link to Matrix (room_id: {})",
&room_id
))?;
@@ -259,6 +214,17 @@ fn upload_log(file: &Path, url: &str) -> Result<String> {
}
fn sanity_check(config: &Config, opts: &Opts) -> Result<()> {
+ if config.zip {
+ warn!(
+ "You have zipping enabled, but zipping is no longer part of ezau. \
+ Arcdps automatically zips logs now."
+ );
+ }
+ if matches!(opts.subcmd, SubCommand::Watch(_))
+ && !config.upload
+ {
+ bail!("Watching but not uploading. What am I even doing here?");
+ }
if matches!(opts.subcmd, SubCommand::Watch(_))
&& config.discord.is_none()
&& config.matrix.is_none()
@@ -289,18 +255,18 @@ fn sanity_check(config: &Config, opts: &Opts) -> Result<()> {
// Dummy modules for when the features are disabled
#[cfg(not(feature = "im-discord"))]
mod discord {
- use super::{Log, Result};
+ use super::{config::Config, Log, Result};
use anyhow::bail;
/// Stub, enable the `im-discord` feature to use this function.
- pub fn post_link(_: &str, _: u64, _: &Log, _: &str) -> Result<()> {
+ pub fn post_link(_: &Config, _: &str, _: u64, _: &Log, _: &str) -> Result<()> {
bail!("Discord feature is disabled in this build!")
}
}
#[cfg(not(feature = "im-matrix"))]
mod matrix {
- use super::{Log, Result};
+ use super::{config::Config, Log, Result};
use anyhow::bail;
pub struct MatrixUser;
impl From<super::config::Matrix> for MatrixUser {
@@ -309,7 +275,7 @@ mod matrix {
}
}
/// Stub, enable the `im-matrix` feature to use this function.
- pub fn post_link(_: MatrixUser, _: &str, _: &Log, _: &str) -> Result<()> {
+ pub fn post_link(_: &Config, _: MatrixUser, _: &str, _: &Log, _: &str) -> Result<()> {
bail!("Matrix feature is disabled in this build!")
}
}
diff --git a/src/matrix.rs b/src/matrix.rs
index 8be0e38..d75381d 100644
--- a/src/matrix.rs
+++ b/src/matrix.rs
@@ -95,7 +95,7 @@ const MESSAGE_CHUNK_COUNT: u16 = 3;
///
/// This function blocks until all API calls have been made, that is until the message has reached
/// the homeserver.
-pub fn post_link(user: MatrixUser, room_id: &str, log: &Log, link: &str) -> Result<()> {
+pub fn post_link(config: &config::Config, user: MatrixUser, room_id: &str, log: &Log, link: &str) -> Result<()> {
let rt = Runtime::new()?;
let room_id = RoomId::parse(room_id)?;
@@ -112,7 +112,7 @@ pub fn post_link(user: MatrixUser, room_id: &str, log: &Log, link: &str) -> Resu
};
sync(&client, sync_token, &session_file).await?;
-
+
info!("Matrix connected as {:?}", client.user_id());
let old_msg = find_message(&client, &client.user_id().unwrap(), &room_id).await?;
@@ -125,7 +125,10 @@ pub fn post_link(user: MatrixUser, room_id: &str, log: &Log, link: &str) -> Resu
Some((old_id, old_text)) => {
debug!("Updating message {:?}", old_id);
debug!("Updating message body {:?}", old_text);
- let logbag = insert_log(&old_text, log, link);
+ let mut logbag = insert_log(&old_text, log, link);
+ if config.sort_logs {
+ logbag.sort();
+ }
let new_text = logbag.render_plain();
debug!("New message body {:?}", new_text);
let new_html = logbag.render_html();
@@ -377,7 +380,7 @@ async fn find_message(
/// Post a new message to the given Matrix channel.
async fn post_new(client: &Client, room_id: OwnedRoomId, log: &Log, link: &str) -> Result<()> {
let line = format!("{} {}", state_emoji(log), link);
- let logbag: LogBag = vec![(log.category().to_string(), vec![line])].into();
+ let logbag: LogBag = vec![(log.category(), vec![line])].into();
let body = logbag.render_plain();
let html = logbag.render_html();