Verified Commit b11235e1 authored by Ole Martin Ruud's avatar Ole Martin Ruud
Browse files

Implement query and order commands

parent 4c2b2f1f
......@@ -287,6 +287,61 @@ dependencies = [
"syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "darling"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"darling_core 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"darling_macro 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "darling_core"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"ident_case 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
"strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "darling_macro"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"darling_core 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "derive_builder"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"darling 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"derive_builder_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "derive_builder_core"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"darling 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "derive_more"
version = "0.15.0"
......@@ -489,6 +544,7 @@ name = "grupperom"
version = "0.1.0"
dependencies = [
"chrono 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"derive_builder 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
"directories 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"fern 0.5.8 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
......@@ -604,6 +660,11 @@ dependencies = [
"tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "idna"
version = "0.1.5"
......@@ -1450,6 +1511,11 @@ name = "string_cache_shared"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "strsim"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "strsim"
version = "0.8.0"
......@@ -1954,6 +2020,11 @@ dependencies = [
"checksum crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6"
"checksum cssparser 0.25.9 (registry+https://github.com/rust-lang/crates.io-index)" = "fbe18ca4efb9ba3716c6da66cc3d7e673bf59fa576353011f48c4cfddbdd740e"
"checksum cssparser-macros 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "5bb1c84e87c717666564ec056105052331431803d606bd45529b28547b611eef"
"checksum darling 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fcfbcb0c5961907597a7d1148e3af036268f2b773886b8bb3eeb1e1281d3d3d6"
"checksum darling_core 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6afc018370c3bff3eb51f89256a6bdb18b4fdcda72d577982a14954a7a0b402c"
"checksum darling_macro 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c6d8dac1c6f1d29a41c4712b4400f878cb4fcc4c7628f298dd75038e024998d1"
"checksum derive_builder 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3ac53fa6a3cda160df823a9346442525dcaf1e171999a1cf23e67067e4fd64d4"
"checksum derive_builder_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0288a23da9333c246bb18c143426074a6ae96747995c5819d2947b64cd942b37"
"checksum derive_more 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a141330240c921ec6d074a3e188a7c7ef95668bb95e7d44fa0e5778ec2a7afe"
"checksum directories 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "551a778172a450d7fc12e629ca3b0428d00f6afa9a43da1b630d54604e97371c"
"checksum dirs-sys 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "afa0b23de8fd801745c471deffa6e12d248f962c9fd4b4c33787b055599bde7b"
......@@ -1987,6 +2058,7 @@ dependencies = [
"checksum httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9"
"checksum hyper 0.12.33 (registry+https://github.com/rust-lang/crates.io-index)" = "7cb44cbce9d8ee4fb36e4c0ad7b794ac44ebaad924b9c8291a63215bb44c2c8f"
"checksum hyper-tls 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3a800d6aa50af4b5850b2b0f659625ce9504df908e9733b635720483be26174f"
"checksum ident_case 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
"checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e"
"checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9"
"checksum indexmap 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a4d6d89e0948bf10c08b9ecc8ac5b83f07f857ebe2c0cbe38de15b4e4f510356"
......@@ -2084,6 +2156,7 @@ dependencies = [
"checksum string_cache 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "25d70109977172b127fe834e5449e5ab1740b9ba49fa18a2020f509174f25423"
"checksum string_cache_codegen 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1eea1eee654ef80933142157fdad9dd8bc43cf7c74e999e369263496f04ff4da"
"checksum string_cache_shared 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b1884d1bc09741d466d9b14e6d37ac89d6909cbcac41dd9ae982d4d063bbedfc"
"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550"
"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
"checksum structopt 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "48399718b3ad695558b979b08a9056a5272ec573cd3070f5ca34165bd4a5bf35"
"checksum structopt-derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2558075232402034384db060831349fb2d1303479593177cc84c25febbebbc6d"
......
......@@ -17,3 +17,4 @@ serde = { version = "1.0.99", features = ["derive"] }
directories = "2.0.2"
serde_json = "1.0.40"
chrono = "0.4.8"
derive_builder = "0.7.2"
......@@ -2,7 +2,7 @@ use directories::ProjectDirs;
use log::*;
use reqwest::{Client, Request, Response};
use std::fs::File;
use std::io::Write;
use std::io::{Read, Write};
use std::string::ToString;
use unhtml::scraper::{Html, Selector};
use unhtml::FromHtml;
......@@ -11,10 +11,6 @@ use crate::auth::*;
use crate::query::*;
use crate::{Config, Result, QUERY_OPTIONS_FILENAME};
fn fmt_dbg<D: std::fmt::Debug>(d: D) -> String {
format!("{:?}", d)
}
pub struct App {
pub config: Config,
pub client: Client,
......@@ -23,12 +19,71 @@ pub struct App {
impl App {
fn request(&self, req: Request) -> Result<Response> {
trace!("Sending request:\n{:#?}", req);
trace!("Sending request:\n{:#?}\n{:#?}", req, req.body());
let res = self.client.execute(req)?;
trace!("Recived response:\n{:#?}", res);
Ok(res)
}
pub fn run(&mut self) -> Result<()> {
use crate::Command::*;
match self.config.command {
Authenticate => self.authenticate().map(|_| ()),
Fetch { fresh } => self.fetch(fresh),
QueryOptions => self.query_options(),
Query {
ref day,
ref hour,
ref duration,
} => {
let _ = self.authenticate()?;
let order_time_info = OrderTimeInfoBuilder::default()
.day(day)
.hour(hour)
.duration(duration)
.build()?;
let rooms = self.query_avaliable_rooms(&order_time_info)?;
eprintln!("AVALIABLE ROOMS:");
rooms
.0
.iter()
.for_each(|room| eprintln!(" - {} ({}) [{}]", room.name, room.size, room.id));
eprintln!("#############");
Ok(())
}
Order {
ref name,
ref day,
ref hour,
ref duration,
} => {
let _ = self.authenticate()?;
let order_time_info = OrderTimeInfoBuilder::default()
.day(day)
.hour(hour)
.duration(duration)
.build()?;
// TODO implement a way to select room from CLI
let rooms = self.query_avaliable_rooms(&order_time_info)?;
let first = rooms.0.get(0).ok_or("No avaliable rooms")?;
self.order_room(name, first, &order_time_info)?;
eprintln!(
"Successfully ordered room {} at {} {} for {}",
first.name, order_time_info.hour, order_time_info.day, order_time_info.duration
);
Ok(())
}
}
}
/// Run authentication through FEIDE to login to tp.uio.no and load the page for ordering rooms
///
/// On success returns a string with the content of the HTML page.
......@@ -97,140 +152,6 @@ impl App {
Ok(content)
}
/// Query for avaliable rooms
// TODO return a vector with rooms
fn query_rooms(&mut self) -> Result<()> {
// TODO turn into a proper struct
let form_params = [
("start", "08:00"), // HH:MM
("duration", "01:00"), // HH:MM
("preset_date", "02.09.2019"), // DD.MM.YYYY
("area", "50000"), // ID
("building", "501"), // ID
("roomtype", ""), // ID
("size", ""), // Number of people
("new_equipment", ""), // ID
("single_place", ""), // none/'on'
("preformsubmit", "1"), // 1
];
let req = self
.client
.post(&self.config.base_url)
.form(&form_params)
.build()?;
info!("Sending room query");
let mut res = self.request(req)?;
// TODO actually parse html and extract avaliable rooms
Ok(())
}
/// Order a room
// TODO return info about ordered room
fn order_room(&mut self) -> Result<()> {
// TODO turn into a proper struct
let form_params = [
("start", "08:00"), // HH:MM
("duration", "01:00"), // HH:MM
("preset_date", "31.08.2019"), // DD.MM.YYYY
("area", "50000"), // ID
("building", "501"), // ID
("roomtype", ""), // ID
("size", ""), // Number of people
("single_place", ""), // none/'on'
("exam", ""), //
("room[]", "501A158"), // ID
("submitall", "Bestill ⇨"),
];
let req = self
.client
.post(&self.config.base_url)
.form(&form_params)
.build()?;
info!("Sending room order");
let mut res = self.request(req)?;
info!("Parsing room order confirmation page");
let html = Html::parse_document(&res.text()?);
let form = html
.select(&Selector::parse("form[name=origform]").map_err(fmt_dbg)?)
.next()
.ok_or("Did not find origform")?;
let token = form
.select(&Selector::parse("[name=tokenrb]").map_err(fmt_dbg)?)
.next()
.ok_or("Did not find tokenrb input")
.and_then(|e| {
e.value()
.attr("value")
.ok_or("Did not find 'value' attr in tokenrb input")
})?;
// TODO turn into a proper struct
let form_params = [
("name", "Woot woot"), // REQUIRED
("notes", ""),
("confirmed", "true"),
("confirm", ""),
("start", "08:00"), // HH:MM
("size", ""), // Number of people
("roomtype", ""), // ID
("duration", "01:00"), // TT:MM
("area", "50000"), // ID
("room[]", "501A158"), // ID
("building", "501"), // ID
("preset_day", "SAT"), // Day of week (three letters)
("preset_date", "2019-08-31"), // YYYY-MM-DD
("exam", ""),
("single_place", ""),
("dates[]", "2019-08-31"), // YYYY-MM-DD
("tokenrb", token), // TOKEN
];
let req = self
.client
.post(&self.config.base_url)
.form(&form_params)
.build()?;
info!("Sending room order confirmation");
let mut res = self.request(req)?;
info!("Parsing room order receipt");
let html = Html::parse_document(&res.text()?);
let form = html
.select(&Selector::parse("form[name=origform]").map_err(fmt_dbg)?)
.next()
.ok_or("Did not find origform")?;
let first_h3 = form
.select(&Selector::parse("h3").map_err(fmt_dbg)?)
.next()
.ok_or("Did not find section in origform")?;
// TODO improve this check to ensure room was ordered
if first_h3.text().any(|t| t.contains("Bekreftelse")) {
info!("Successfully ordered a room");
Ok(())
} else {
Err("Did not get confirmation of room order".into())
}
}
pub fn run(&mut self) -> Result<()> {
use crate::Command::*;
match self.config.command {
Authenticate => {
self.authenticate()?;
Ok(())
}
Fetch { fresh } => self.fetch(fresh),
Query => self.query(),
}
}
fn fetch(&self, fresh: bool) -> Result<()> {
if let Some(ref project_dirs) = self.project_dirs {
let already_exists = File::open(
......@@ -276,7 +197,7 @@ impl App {
}
/// Query cached results and display all options
fn query(&self) -> Result<()> {
fn query_options(&self) -> Result<()> {
if let Some(ref project_dirs) = self.project_dirs {
let file_name = project_dirs
.data_dir()
......@@ -309,4 +230,82 @@ impl App {
Err("Project directories are required to query options".into())
}
}
/// Query for avaliable rooms
fn query_avaliable_rooms(&self, order_time_info: &OrderTimeInfo) -> Result<Rooms> {
let query = QueryRoomFormBuilder::default()
.start(order_time_info.hour)
.duration(order_time_info.duration)
.preset_date(order_time_info.day)
// TODO remove hardcoded area
.area("50000")
.build()?;
info!("Sending room query");
let req = self
.client
.post(&self.config.base_url)
.form(&query)
.build()?;
let mut res = self.request(req)?;
info!("Parsing result of room query");
let content = res.text()?;
Rooms::from_html(&content).map_err(|e| e.to_string().into())
}
/// Return info about ordered room
fn order_room(&self, name: &str, room: &Room, order_time_info: &OrderTimeInfo) -> Result<()> {
let order = OrderRoomFormBuilder::default()
.start(order_time_info.hour)
.duration(order_time_info.duration)
.preset_date(order_time_info.day)
.room(room.id.clone())
.build()?;
info!("Sending room order:\n{:#?}", order);
let req = self
.client
.post(&self.config.base_url)
.form(&order)
.build()?;
let mut res = self.request(req)?;
info!("Parsing room order confirmation page");
let content = res.text()?;
let mut order_confirm =
OrderRoomConfirmForm::from_html(&content).map_err(|e| e.to_string())?;
order_confirm.name = name.into();
info!("Sending room order confirmation:\n{:#?}", order_confirm);
let req = self
.client
.post(&self.config.base_url)
.form(&order_confirm)
.build()?;
let mut res = self.request(req)?;
// TODO improve this receipt to ensure room was ordered
#[derive(FromHtml, Debug)]
#[html(selector = "form[name=origform]")]
struct Receipt {
#[html(selector = "h3", attr = "inner")]
title: String,
};
info!("Parsing room order receipt");
let content = res.text()?;
let receipt = Receipt::from_html(&content).map_err(|e| e.to_string())?;
info!("Recived receipt:\n{:#?}", receipt);
if receipt.title.contains("Bekreftelse") {
info!(
"Successfully ordered room at {:?}:\n{:#?}",
room, order_time_info
);
Ok(())
} else {
Err("Did not get confirmation of room order".into())
}
}
}
......@@ -9,10 +9,10 @@ use std::io::{Read, Write};
use structopt::StructOpt;
use unhtml::FromHtml;
mod app;
mod auth;
mod logger;
mod query;
pub mod app;
pub mod auth;
pub mod logger;
pub mod query;
use app::App;
use query::OrderFormOpts;
......@@ -50,14 +50,16 @@ pub struct Config {
short,
long,
help = "Base URL for the room ordering service",
default_value = "https://tp.uio.no/ntnu/rombestilling"
default_value = "https://tp.uio.no/ntnu/rombestilling/"
)]
base_url: String,
#[structopt(subcommand)]
command: Command,
}
#[derive(StructOpt)]
// TODO make all command date/hour fields default to today and next half hour
#[derive(StructOpt, Clone, Debug)]
#[structopt(rename_all = "kebab-case")]
enum Command {
#[structopt(about = "Authenticate to service to ensure it works")]
......@@ -67,8 +69,28 @@ enum Command {
#[structopt(short, long, help = "Fetch query options even if they already exist")]
fresh: bool,
},
#[structopt(about = "Query the service for buildings, rooms, etc.")]
Query,
#[structopt(about = "Query the service for options regarding buildings, rooms, etc.")]
QueryOptions,
#[structopt(about = "Query the service for avaliable rooms")]
Query {
#[structopt(long, help = "Day to order room (DD.MM.YYYY)")]
day: String,
#[structopt(long, help = "Hour to order room from (HH:MM)")]
hour: String,
#[structopt(long, help = "Duration of order (HH:MM)")]
duration: String,
},
#[structopt(about = "Order the first avaliable room the given day")]
Order {
#[structopt(short, long, help = "Name to add to order")]
name: String,
#[structopt(long, help = "Day to order room (DD.MM.YYYY)")]
day: String,
#[structopt(short, long, help = "Hour to order room from (HH:MM)")]
hour: String,
#[structopt(short, long, help = "Duration of order (HH:MM)")]
duration: String,
},
}
fn main() {
......
use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use unhtml::FromHtml;
#[derive(FromHtml, Serialize, Debug)]
#[derive(Debug, Builder)]
pub struct OrderTimeInfo<'a> {
pub day: &'a str,
pub hour: &'a str,
pub duration: &'a str,
}
#[derive(FromHtml, Serialize, Debug, Builder)]
#[html(selector = "[name=origform]")]
pub struct OrderForm {
pub struct QueryRoomForm {
/// HH:MM
#[html(selector = "select[name=start]>option[selected]", attr = "value")]
pub start: Option<String>,
#[builder(setter(into))]
pub start: String,
/// HH:MM
#[html(selector = "select[name=duration]>option[selected]", attr = "value")]
pub duration: Option<String>,
#[builder(setter(into))]
pub duration: String,
/// DD.MM.YYYY
#[html(selector = "[name=preset_date]", attr = "value")]
pub preset_date: Option<String>,
#[builder(setter(into))]
pub preset_date: String,
/// ID
#[html(selector = "select[name=area]>option[selected]", attr = "value")]
#[builder(setter(into, strip_option), default)]
pub area: Option<String>,
/// ID
#[html(selector = "select[name=building]>option[selected]", attr = "value")]
#[builder(setter(into, strip_option), default)]
pub building: Option<String>,
/// ID
#[html(selector = "select[name=roomtype]>option[selected]", attr = "value")]
#[builder(setter(into, strip_option), default)]
pub roomtype: Option<String>,
/// Number of people
#[html(selector = "[name=size]", attr = "value")]
#[builder(setter(into, strip_option), default)]
pub size: Option<usize>,
/// ID
#[html(
selector = "select[name=new_equipment]>option[selected]",
attr = "value"
)]
#[builder(setter(into, strip_option), default)]
pub new_equipment: Option<String>,
/// none/'on'
#[html(selector = "[name=single_place]", attr = "checked")]
#[builder(setter(into, strip_option), default)]
pub single_place: Option<String>,
/// 1
#[html(selector = "[name=preformsubmit]", attr = "value")]
pub preformsubmit: usize,
#[builder(setter(skip), default = "1")]
preformsubmit: usize,
}
#[derive(FromHtml, Deserialize, Serialize, Debug)]
......@@ -63,3 +81,136 @@ pub struct OrderFormOpts {
#[html(selector = "select[name=new_equipment]>option")]
pub new_equipment: Vec<FormOption>,
}
#[derive(FromHtml, Serialize, Debug)]
#[html(selector = "#roomChoice")]
pub struct Rooms(#[html(selector = ".possible-rooms-table>tbody>tr")] pub Vec<Room>);
#[derive(FromHtml, Serialize, Debug, Clone)]
pub struct Room {
#[html(selector = r#"td:nth-child(3)>[name="room[]"]"#, attr = "value")]
pub id: String,
#[html(selector = "td:nth-child(1)>a", attr = "inner")]
pub name: String,
#[html(selector = "td:nth-child(2)", attr = "inner")]
pub size: usize,
}
#[derive(FromHtml, Serialize, Debug, Builder)]
#[html(selector = "#roomChoice")]
pub struct OrderRoomForm {
/// ID
#[html(selector = r#"[name="room[]"]"#, attr = "value", default)]
#[serde(rename = "room[]")]
#[builder(setter(into))]
pub room: String,
/// HH:MM
#[html(selector = "[name=start]", attr = "value", default)]
#[builder(setter(into))]
pub start: String,
/// HH:MM
#[html(selector = "[name=duration]", attr = "value", default)]
#[builder(setter(into))]
pub duration: String,
/// DD.MM.YYYY
#[html(selector = "[name=preset_date]", attr = "value", default)]
#[builder(setter(into))]
pub preset_date: String,
/// ID
#[html(selector = "[name=area]", attr = "value")]
#[builder(setter(into, strip_option), default)]
pub area: Option<String>,
/// ID
#[html(selector = "[name=building]", attr = "value")]
#[builder(setter(into, strip_option), default)]
pub building: Option<String>,
/// ID
#[html(selector = "[name=roomtype]", attr = "value")]
#[builder(setter(into, strip_option), default)]
pub roomtype: Option<String>,
/// Number of people
#[html(selector = "[name=size]", attr = "value")]
#[builder(setter(strip_option), default)]
pub size: Option<usize>,