diff options
Diffstat (limited to 'src')
| -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 | 
6 files changed, 181 insertions, 32 deletions
| @@ -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)      }  } | 
