use std::{ fs::{self, File}, io::{BufWriter, Write}, path::{Path, PathBuf}, sync::mpsc::channel, time::Duration, }; use anyhow::{anyhow, Result}; use clap::Clap; use evtclib::{Boss, Compression, Log}; use log::{debug, error, info}; use notify::{self, DebouncedEvent, RecursiveMode, Watcher}; use regex::Regex; use serde::Deserialize; use zip::{CompressionMethod, ZipWriter}; mod categories; use categories::Categorizable; mod discord; const DPS_REPORT_API: &str = "https://dps.report/uploadContent"; const WATCH_DELAY_SECONDS: u64 = 2; #[derive(Clap, Debug, Clone, PartialEq, Eq, Hash)] #[clap(version = "0.1")] struct Opts { /// The directory name to watch. dirname: PathBuf, /// The Discord auth token for the bot account. #[clap(long)] discord_token: String, /// The channel ID where the log message should be posted. #[clap(long)] channel_id: u64, } fn main() { pretty_env_logger::init(); let opts = Opts::parse(); if let Err(e) = inner_main(&opts) { error!("{}", e); } } fn inner_main(opts: &Opts) -> 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 (tx, rx) = channel(); let mut watcher = notify::watcher(tx, Duration::from_secs(WATCH_DELAY_SECONDS))?; watcher.watch(&opts.dirname, RecursiveMode::Recursive)?; loop { let event = rx.recv()?; 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 raw_evtc_re.is_match(path_str) { info!("Zipping up {}", path_str); zip_file(&path)?; } else if zip_evtc_re.is_match(path_str) { handle_file(opts, &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()?; fs::remove_file(filepath)?; Ok(()) } fn handle_file(opts: &Opts, filename: &Path) -> Result<()> { let log = load_log(filename)?; info!("Loaded log from category {}", log.category()); if !should_upload(&log) { info!("Skipping log, not uploading"); return Ok(()); } let permalink = upload_log(filename)?; info!("Uploaded log, available at {}", permalink); discord::post_link(&opts.discord_token, opts.channel_id, log, permalink)?; Ok(()) } fn should_upload(log: &Log) -> bool { // Only upload known logs if log.encounter().is_none() { return false; } // Only upload Skorvald if it actually was in 100 CM (and not in in lower-tier or non-CM). if log.encounter() == Some(Boss::Skorvald) && !log.is_cm() { return false; } true } fn load_log(path: &Path) -> Result { evtclib::process_file(path, Compression::Zip).map_err(Into::into) } fn upload_log(file: &Path) -> Result { #[derive(Debug, Deserialize)] struct ApiResponse { permalink: String, } let client = reqwest::blocking::Client::new(); let form = reqwest::blocking::multipart::Form::new().file("file", file)?; let resp: ApiResponse = client .post(DPS_REPORT_API) .query(&[("json", 1)]) .multipart(form) .send()? .json()?; Ok(resp.permalink) }