diff options
-rw-r--r-- | Cargo.toml | 5 | ||||
-rw-r--r-- | src/csl.rs | 2 | ||||
-rw-r--r-- | src/filters.rs | 23 | ||||
-rw-r--r-- | src/guilds.rs | 79 | ||||
-rw-r--r-- | src/main.rs | 81 | ||||
-rw-r--r-- | src/output/formats.rs | 23 | ||||
-rw-r--r-- | src/output/mod.rs | 5 |
7 files changed, 186 insertions, 32 deletions
@@ -17,3 +17,8 @@ num-traits = "0.2" humantime = "2.0" zip = "0.5" anyhow = "1.0" +once_cell = "1.3" +ureq = { version = "0.12", features = ["json"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +dirs = "2.0" @@ -36,7 +36,7 @@ macro_rules! variants { }; } -variants! { SearchField => Account, Character } +variants! { SearchField => Account, Character, Guild } variants! { FightOutcome => Success, Wipe } variants! { Weekday => Mon, Tue, Wed, Thu, Fri, Sat, Sun } variants! { Boss => diff --git a/src/filters.rs b/src/filters.rs index 02334bc..715e4e4 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -4,7 +4,7 @@ use evtclib::statistics::gamedata::Boss; use num_traits::FromPrimitive; -use super::{SearchField, LogResult, Opt}; +use super::{SearchField, LogResult, Opt, guilds}; use chrono::Datelike; @@ -30,7 +30,8 @@ pub fn filter_name(evtc: &PartialEvtc, opt: &Opt) -> bool { _ => (), } } - false + // Don't throw away the log yet if we are searching for guilds + opt.field.contains(&SearchField::Guild) } /// Do filtering based on the boss ID. @@ -64,3 +65,21 @@ pub fn filter_time(result: &LogResult, opt: &Opt) -> bool { after_ok && before_ok } + +/// Do filtering based on the guilds. +pub fn filter_guilds(result: &LogResult, opt: &Opt) -> bool { + if !opt.guilds { + return true; + } + if !opt.field.contains(&SearchField::Guild) { + return true; + } + result.players.iter().any(|player| { + let guild = player.guild_id.as_ref().and_then(|id| guilds::lookup(id)); + if let Some(guild) = guild { + opt.expression.is_match(guild.tag()) || opt.expression.is_match(guild.name()) + } else { + false + } + }) +} diff --git a/src/guilds.rs b/src/guilds.rs new file mode 100644 index 0000000..0eff6ad --- /dev/null +++ b/src/guilds.rs @@ -0,0 +1,79 @@ +//! Guild name retrieval and caching functions. +use std::collections::HashMap; +use std::fs::File; +use std::path::PathBuf; +use std::sync::RwLock; + +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; + +static CACHE: Lazy<RwLock<HashMap<String, Guild>>> = Lazy::new(|| RwLock::new(HashMap::new())); + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Guild { + tag: String, + name: String, +} + +impl Guild { + pub fn tag(&self) -> &str { + &self.tag + } + + pub fn name(&self) -> &str { + &self.name + } +} + +/// Looks up the given guild. +/// +/// This checks the cache first, and if nothing was found, it will hit the API. +pub fn lookup(api_id: &str) -> Option<Guild> { + { + let cache = CACHE.read().unwrap(); + if let Some(guild) = cache.get(api_id) { + return Some(guild.clone()); + } + } + + let mut cache = CACHE.write().unwrap(); + let url = format!("https://api.guildwars2.com/v2/guild/{}", api_id); + let result = ureq::get(&url) + .call() + .into_json() + .expect("Invalid JSON in API response"); + let name = result["name"].as_str()?; + let tag = result["tag"].as_str()?; + let guild = Guild { + tag: tag.into(), + name: name.into(), + }; + cache.insert(api_id.into(), guild.clone()); + Some(guild) +} + +fn cache_path() -> PathBuf { + let mut cache_path = dirs::cache_dir().unwrap(); + cache_path.push("raidgrep"); + cache_path +} + +/// Loads the cache from the file system. +pub fn prepare_cache() { + let path = cache_path(); + if !path.is_file() { + return; + } + + let file = File::open(path).expect("Unable to read cache file"); + let cache = serde_json::from_reader(file).expect("Cache file has invalid format"); + let mut target = CACHE.write().unwrap(); + *target = cache; +} + +/// Saves the cache to the file system +pub fn save_cache() { + let path = cache_path(); + let file = File::create(path).expect("Cannot open cache for writing"); + serde_json::to_writer(file, &*CACHE.read().unwrap()).unwrap(); +} diff --git a/src/main.rs b/src/main.rs index 838a518..f82e522 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,20 @@ +use std::collections::HashMap; use std::fs::File; use std::io::{BufReader, Read, Seek}; use std::path::PathBuf; use std::str::FromStr; +use anyhow::{anyhow, Result}; use chrono::{Duration, NaiveDateTime, Weekday}; use num_traits::cast::FromPrimitive; use regex::Regex; use structopt::StructOpt; use walkdir::{DirEntry, WalkDir}; -use anyhow::{anyhow, Result}; use evtclib::{AgentKind, AgentName, EventKind, Log}; +mod guilds; mod output; - mod filters; mod csl; @@ -55,16 +56,11 @@ fn debug_enabled() -> bool { #[structopt(name = "raidgrep")] pub struct Opt { /// Path to the folder with logs. - #[structopt( - short = "d", - long = "dir", - default_value = ".", - parse(from_os_str) - )] + #[structopt(short = "d", long = "dir", default_value = ".", parse(from_os_str))] path: PathBuf, /// The fields which should be searched. - #[structopt(short = "f", long = "fields", default_value = "*")] + #[structopt(short = "f", long = "fields", default_value = "account,character")] field: CommaSeparatedList<SearchField>, /// Only display fights with the given outcome. @@ -109,17 +105,17 @@ pub struct Opt { weekdays: CommaSeparatedList<Weekday>, /// Only show logs from the given encounters. - #[structopt( - short = "e", - long = "bosses", - default_value = "*", - )] + #[structopt(short = "e", long = "bosses", default_value = "*")] bosses: CommaSeparatedList<evtclib::statistics::gamedata::Boss>, /// Print more debugging information to stderr. #[structopt(long = "debug")] debug: bool, + /// Load guild information from the API. + #[structopt(long = "guilds")] + guilds: bool, + /// The regular expression to search for. #[structopt(name = "EXPR")] expression: Regex, @@ -132,6 +128,8 @@ enum SearchField { Account, /// Only search the character name. Character, + /// Only search the guild name or tag. + Guild, } impl FromStr for SearchField { @@ -141,6 +139,7 @@ impl FromStr for SearchField { match s { "account" => Ok(SearchField::Account), "character" => Ok(SearchField::Character), + "guild" => Ok(SearchField::Guild), _ => Err("Must be account or character"), } } @@ -172,6 +171,8 @@ pub struct Player { profession: String, /// Subsquad that the player was in. subgroup: u8, + /// Guild ID, ready for API consumption. + guild_id: Option<String>, } /// Outcome of the fight. @@ -209,12 +210,10 @@ fn parse_time_arg(input: &str) -> Result<NaiveDateTime> { Err(anyhow!("unknown time format")) } -fn try_from_str_simple_error<T: FromStr>(input: &str) -> Result<T, String> -{ +fn try_from_str_simple_error<T: FromStr>(input: &str) -> Result<T, String> { T::from_str(input).map_err(|_| format!("'{}' is an invalid value", input)) } - enum ZipWrapper<R: Read + Seek> { Raw(Option<R>), Zipped(zip::ZipArchive<R>), @@ -237,7 +236,6 @@ impl<R: Read + Seek> ZipWrapper<R> { } } - fn main() { let opt = Opt::from_args(); @@ -250,6 +248,10 @@ fn main() { unsafe { DEBUG_ENABLED = true }; } + if opt.guilds { + guilds::prepare_cache(); + } + let result = grep(&opt); match result { Ok(_) => {} @@ -257,6 +259,10 @@ fn main() { eprintln!("Error: {}", e); } } + + if opt.guilds { + guilds::save_cache(); + } } /// Check if the given entry represents a log file, based on the file name. @@ -311,11 +317,11 @@ fn search_log(entry: &DirEntry, opt: &Opt) -> Result<Option<LogResult>> { let mut stream = wrapper.get_stream(); let partial = evtclib::raw::parser::parse_partial_file(&mut stream)?; - let early_ok = filters::filter_name(&partial, opt) != opt.invert - && filters::filter_boss(&partial, opt); + let early_ok = + filters::filter_name(&partial, opt) != opt.invert && filters::filter_boss(&partial, opt); if !early_ok { - return Ok(None) + return Ok(None); } let raw = evtclib::raw::parser::finish_parsing(partial, &mut stream)?; @@ -331,7 +337,8 @@ fn search_log(entry: &DirEntry, opt: &Opt) -> Result<Option<LogResult>> { let take_log = filters::filter_outcome(&info, opt) && filters::filter_weekday(&info, opt) - && filters::filter_time(&info, opt); + && filters::filter_time(&info, opt) + && filters::filter_guilds(&info, opt); if take_log { Ok(Some(info)) @@ -350,7 +357,10 @@ fn extract_info(entry: &DirEntry, log: &Log) -> LogResult { log.boss_id() ); "unknown" - }).into(); + }) + .into(); + + let guild_ids = get_guild_mapping(log); let mut players = log .players() @@ -367,9 +377,11 @@ fn extract_info(entry: &DirEntry, log: &Log) -> LogResult { character_name: character_name.clone(), profession: get_profession_name(*profession, *elite).into(), subgroup: *subgroup, + guild_id: guild_ids.get(p.addr()).cloned(), } }}}} - }).collect::<Vec<Player>>(); + }) + .collect::<Vec<Player>>(); players.sort_by_key(|p| p.subgroup); LogResult { @@ -381,6 +393,27 @@ fn extract_info(entry: &DirEntry, log: &Log) -> LogResult { } } +/// Get a mapping of agent IDs to guild API strings. +fn get_guild_mapping(log: &Log) -> HashMap<u64, String> { + log.events() + .iter() + .filter_map(|event| { + if let EventKind::Guild { + source_agent_addr, + ref api_guild_id, + .. + } = event.kind + { + api_guild_id + .as_ref() + .map(|api_id| (source_agent_addr, api_id.clone())) + } else { + None + } + }) + .collect() +} + /// Get the timestamp of the log start time. fn get_start_timestamp(log: &Log) -> u32 { for event in log.events() { diff --git a/src/output/formats.rs b/src/output/formats.rs index 5915069..b697401 100644 --- a/src/output/formats.rs +++ b/src/output/formats.rs @@ -2,6 +2,7 @@ use std::fmt::Write; use super::{LogResult, FightOutcome}; +use super::super::guilds; /// An output format pub trait Format: Sync + Send { @@ -12,7 +13,9 @@ pub trait Format: Sync + Send { /// The human readable, colored format. #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] -pub struct HumanReadable; +pub struct HumanReadable { + pub show_guilds: bool, +} impl Format for HumanReadable { @@ -35,14 +38,26 @@ impl Format for HumanReadable { outcome, ).unwrap(); for player in &item.players { - writeln!( + write!( result, - " {:2} {:20} {:19} {}", + " {:2} {:20} {:19} {:12}", player.subgroup, player.account_name.yellow(), player.character_name.cyan(), - player.profession + player.profession, ).unwrap(); + if self.show_guilds { + let guild = player.guild_id.as_ref().and_then(|id| guilds::lookup(id)); + if let Some(guild) = guild { + write!( + result, + " [{}] {}", + guild.tag().magenta(), + guild.name().magenta(), + ).unwrap(); + } + } + writeln!(result).unwrap(); } result } diff --git a/src/output/mod.rs b/src/output/mod.rs index 73af5ab..84ed0a4 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -17,6 +17,9 @@ pub fn build_pipeline(opt: &Opt) -> Pipeline { if opt.file_name_only { Pipeline::new(stream, formats::FileOnly, aggregator) } else { - Pipeline::new(stream, formats::HumanReadable, aggregator) + let format = formats::HumanReadable { + show_guilds: opt.guilds, + }; + Pipeline::new(stream, format, aggregator) } } |