use failure::{Error, Fail, ensure};
use select::document::Document;
use select::predicate::{Class, Name};
use std::str::FromStr;
use crate::model::{
attribute::{Attribute, Attributes},
clan::Clan,
class::{Classes, ClassInfo, ClassType},
gender::Gender,
race::Race,
server::Server,
datacenter::Datacenter,
util::load_url
};
#[derive(Fail, Debug)]
pub enum SearchError {
#[fail(display = "Node not found: {}", _0)]
NodeNotFound(String),
#[fail(display = "Invalid data found while parsing '{}'", _0)]
InvalidData(String),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
struct CharInfo {
race: Race,
clan: Clan,
gender: Gender,
}
struct HomeInfo {
server: Server,
datacenter: Datacenter,
}
macro_rules! ensure_node {
($doc:ident, $search:expr) => {{
ensure_node!($doc, $search, 0)
}};
($doc:ident, $search:expr, $nth:expr) => {{
let node = $doc.find($search).nth($nth);
ensure!(node.is_some(), SearchError::NodeNotFound(stringify!($search).to_string() + "(" + stringify!($nth) + ")"));
node.unwrap()
}};
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Profile {
pub user_id: u32,
pub free_company: Option<String>,
pub title: Option<String>,
pub name: String,
pub nameday: String,
pub guardian: String,
pub city_state: String,
pub server: Server,
pub datacenter: Datacenter,
pub race: Race,
pub clan: Clan,
pub gender: Gender,
pub hp: u32,
pub mp: u32,
pub attributes: Attributes,
classes: Classes,
}
impl Profile {
pub fn get(user_id: u32) -> Result<Self, Error> {
let main_doc = load_url(user_id, None)?;
let classes_doc = load_url(user_id, Some("class_job"))?;
let char_info = Self::parse_char_info(&main_doc)?;
let home_info = Self::parse_home_info(&main_doc)?;
let (hp, mp) = Self::parse_char_param(&main_doc)?;
Ok(Self {
user_id,
free_company: Self::parse_free_company(&main_doc),
title: Self::parse_title(&main_doc),
name: Self::parse_name(&main_doc)?,
nameday: Self::parse_nameday(&main_doc)?,
guardian: Self::parse_guardian(&main_doc)?,
city_state: Self::parse_city_state(&main_doc)?,
server: home_info.server,
datacenter: home_info.datacenter,
race: char_info.race,
clan: char_info.clan,
gender: char_info.gender,
hp,
mp,
attributes: Self::parse_attributes(&main_doc)?,
classes: Self::parse_classes(&classes_doc)?,
})
}
pub fn level(&self, class: ClassType) -> Option<u32> {
match self.class_info(class) {
Some(v) => Some(v.level),
None => None
}
}
pub fn class_info(&self, class: ClassType) -> Option<ClassInfo> {
self.classes.get(class)
}
pub fn all_class_info(&self) -> &Classes {
&self.classes
}
fn parse_free_company(doc: &Document) -> Option<String> {
match doc.find(Class("character__freecompany__name")).next() {
Some(node) => Some(
node.text().strip_prefix("Free Company").unwrap_or(&node.text()).to_string()
),
None => None,
}
}
fn parse_title(doc: &Document) -> Option<String> {
match doc.find(Class("frame__chara__title")).next() {
Some(node) => Some(node.text()),
None => None,
}
}
fn parse_name(doc: &Document) -> Result<String, Error> {
Ok(ensure_node!(doc, Class("frame__chara__name")).text())
}
fn parse_nameday(doc: &Document) -> Result<String, Error> {
Ok(ensure_node!(doc, Class("character-block__birth")).text())
}
fn parse_guardian(doc: &Document) -> Result<String, Error> {
Ok(ensure_node!(doc, Class("character-block__name"), 1).text())
}
fn parse_city_state(doc: &Document) -> Result<String, Error> {
Ok(ensure_node!(doc, Class("character-block__name"), 2).text())
}
fn parse_home_info(doc: &Document) -> Result<HomeInfo, Error> {
let text = ensure_node!(doc, Class("frame__chara__world")).text();
let mut server = text.split("\u{A0}").next();
ensure!(server.is_some(), SearchError::InvalidData("Could not find server/datacenter string.".into()));
let home_info = server
.unwrap()
.split_whitespace()
.map(|e| e.replace(&['[', ']'], ""))
.collect::<Vec<String>>();
Ok(HomeInfo {
server: Server::from_str(&home_info[0])?,
datacenter: Datacenter::from_str(&home_info[1])?,
})
}
fn parse_char_info(doc: &Document) -> Result<CharInfo, Error> {
let char_block = {
let mut block = ensure_node!(doc, Class("character-block__name")).inner_html();
block = block.replace(" ", "_");
block = block.replace("<br>", " ");
block.replace("_/_", " ")
};
let char_info = char_block
.split_whitespace()
.map(|e| e.replace("_", " "))
.map(|e| e.into())
.collect::<Vec<String>>();
ensure!(char_info.len() == 3 || char_info.len() == 4, SearchError::InvalidData("character block name".into()));
if char_info.len() == 4 {
Ok(CharInfo {
race: Race::Aura,
clan: Clan::from_str(&char_info[2])?,
gender: Gender::from_str(&char_info[3])?,
})
} else {
Ok(CharInfo {
race: Race::from_str(&char_info[0])?,
clan: Clan::from_str(&char_info[1])?,
gender: Gender::from_str(&char_info[2])?,
})
}
}
fn parse_char_param(doc: &Document) -> Result<(u32, u32), Error> {
let attr_block = ensure_node!(doc, Class("character__param"));
let mut hp = None;
let mut mp = None;
for item in attr_block.find(Name("li")) {
if item.find(Class("character__param__text__hp--en-us")).count() == 1 {
hp = Some(ensure_node!(item, Name("span")).text().parse::<u32>()?);
} else if item.find(Class("character__param__text__mp--en-us")).count() == 1 ||
item.find(Class("character__param__text__gp--en-us")).count() == 1 ||
item.find(Class("character__param__text__cp--en-us")).count() == 1 {
mp = Some(ensure_node!(item, Name("span")).text().parse::<u32>()?);
} else {
continue
}
}
ensure!(hp.is_some() && mp.is_some(), SearchError::InvalidData("character__param".into()));
Ok((hp.unwrap(), mp.unwrap()))
}
fn parse_attributes(doc: &Document) -> Result<Attributes, Error> {
let block = ensure_node!(doc, Class("character__profile__data"));
let mut attributes = Attributes::new();
for item in block.find(Name("tr")) {
let name = ensure_node!(item, Name("span")).text();
let value = Attribute{
level: ensure_node!(item, Name("td")).text().parse::<u16>()?
};
attributes.insert(name, value);
}
Ok(attributes)
}
fn parse_classes(doc: &Document) -> Result<Classes, Error> {
let mut classes = Classes::new();
for list in doc.find(Class("character__content")).take(4) {
for item in list.find(Name("li")) {
let name = ensure_node!(item, Class("character__job__name")).text();
let classinfo = match ensure_node!(item, Class("character__job__level")).text().as_str() {
"-" => None,
level => {
let text = ensure_node!(item, Class("character__job__exp")).text();
let mut parts = text.split(" / ");
let current_xp = parts.next();
ensure!(current_xp.is_some(), SearchError::InvalidData("character__job__exp".into()));
let max_xp = parts.next();
ensure!(max_xp.is_some(), SearchError::InvalidData("character__job__exp".into()));
Some(ClassInfo{
level: level.parse()?,
current_xp: match current_xp.unwrap() {
"--" => None,
value => Some(value.replace(",", "").parse()?)
},
max_xp: match max_xp.unwrap() {
"--" => None,
value => Some(value.replace(",", "").parse()?)
},
})
}
};
let name = name.split(" / ").next();
ensure!(name.is_some(), SearchError::InvalidData("character__job__name".into()));
let class = ClassType::from_str(&name.unwrap())?;
match class {
ClassType::Paladin => classes.insert(ClassType::Gladiator, classinfo),
ClassType::Warrior => classes.insert(ClassType::Marauder, classinfo),
ClassType::WhiteMage => classes.insert(ClassType::Conjurer, classinfo),
ClassType::Monk => classes.insert(ClassType::Pugilist, classinfo),
ClassType::Dragoon => classes.insert(ClassType::Lancer, classinfo),
ClassType::Ninja => classes.insert(ClassType::Rogue, classinfo),
ClassType::Bard => classes.insert(ClassType::Archer, classinfo),
ClassType::BlackMage => classes.insert(ClassType::Thaumaturge, classinfo),
ClassType::Summoner => classes.insert(ClassType::Arcanist, classinfo),
_ => (),
}
classes.insert(class, classinfo);
}
}
Ok(classes)
}
}