start
This commit is contained in:
parent
8afdd208c6
commit
8e82ec9523
23 changed files with 3345 additions and 0 deletions
5
froxy-scraper/src/lib.rs
Normal file
5
froxy-scraper/src/lib.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
pub mod types;
|
||||
pub mod state;
|
||||
pub mod parsing;
|
||||
121
froxy-scraper/src/parsing.rs
Normal file
121
froxy-scraper/src/parsing.rs
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
use scraper::Selector;
|
||||
use url::Url;
|
||||
|
||||
use crate::types::{ClusterLocation, ClusterLocationData, Relation};
|
||||
|
||||
macro_rules! sel {
|
||||
($name:ident, $value:literal) => {
|
||||
static $name: std::sync::LazyLock<Selector> =
|
||||
std::sync::LazyLock::new(|| Selector::parse($value).unwrap());
|
||||
};
|
||||
}
|
||||
|
||||
sel!(
|
||||
LOCATION_SEL,
|
||||
"div.container-fluid.mt-2.scroll.p-0 > table.grid.text-center > tbody > tr > td"
|
||||
);
|
||||
sel!(IMAGE_SEL, "img");
|
||||
|
||||
pub fn get_cluster_location(
|
||||
cluster_name: impl AsRef<str>,
|
||||
page: impl AsRef<str>,
|
||||
) -> crate::types::CluserInformation {
|
||||
let page = page.as_ref();
|
||||
|
||||
let doc = scraper::Html::parse_document(page);
|
||||
let tds = doc.select(&LOCATION_SEL);
|
||||
|
||||
let mut out = crate::types::CluserInformation {
|
||||
cluster_name: cluster_name.as_ref().into(),
|
||||
locations: Vec::with_capacity(128),
|
||||
};
|
||||
|
||||
for td in tds {
|
||||
let pos_login = td.attr("data-pos").and_then(|pos| {
|
||||
td.attr("data-login")
|
||||
.map(|login| (pos.trim(), login.trim()))
|
||||
});
|
||||
let is_dead = td
|
||||
.value()
|
||||
.has_class("dead", scraper::CaseSensitivity::AsciiCaseInsensitive);
|
||||
let is_warn = td
|
||||
.value()
|
||||
.has_class("attention", scraper::CaseSensitivity::AsciiCaseInsensitive);
|
||||
|
||||
match (pos_login, is_dead, is_warn) {
|
||||
(Some((pos, login)), d, w) if login.is_empty() && (d || w) => {
|
||||
out.locations.push(match (d, w) {
|
||||
(true, _) => ClusterLocation {
|
||||
location: pos.into(),
|
||||
data: ClusterLocationData::Normal {
|
||||
status: crate::types::ClusterLocationStatus::Dead,
|
||||
},
|
||||
},
|
||||
(false, true) => ClusterLocation {
|
||||
location: pos.into(),
|
||||
data: ClusterLocationData::Normal {
|
||||
status: crate::types::ClusterLocationStatus::Damaged,
|
||||
},
|
||||
},
|
||||
// this should never happen
|
||||
_ => ClusterLocation {
|
||||
location: pos.into(),
|
||||
data: ClusterLocationData::Empty,
|
||||
},
|
||||
})
|
||||
}
|
||||
(Some((pos, login)), _, _) if !login.trim().is_empty() => {
|
||||
let image = td
|
||||
.select(&IMAGE_SEL)
|
||||
.next()
|
||||
.and_then(|i| i.attr("src"))
|
||||
.and_then(|s| Url::parse(s).ok());
|
||||
let is_me = td
|
||||
.value()
|
||||
.has_class("me", scraper::CaseSensitivity::AsciiCaseInsensitive);
|
||||
let is_focus = td
|
||||
.value()
|
||||
.has_class("focus", scraper::CaseSensitivity::AsciiCaseInsensitive);
|
||||
let is_friend = td
|
||||
.value()
|
||||
.has_class("friend", scraper::CaseSensitivity::AsciiCaseInsensitive);
|
||||
let is_close_friend = td.value().has_class(
|
||||
"close_friend",
|
||||
scraper::CaseSensitivity::AsciiCaseInsensitive,
|
||||
);
|
||||
let is_pooled = td
|
||||
.value()
|
||||
.has_class("pooled", scraper::CaseSensitivity::AsciiCaseInsensitive);
|
||||
|
||||
let mut relation = Relation::None;
|
||||
if is_me {
|
||||
relation = Relation::Me;
|
||||
}
|
||||
if is_friend {
|
||||
relation = Relation::Friend;
|
||||
}
|
||||
if is_close_friend {
|
||||
relation = Relation::CloseFriend;
|
||||
}
|
||||
if is_pooled {
|
||||
relation = Relation::Pooled;
|
||||
}
|
||||
if is_focus {
|
||||
relation = Relation::Focus;
|
||||
}
|
||||
|
||||
out.locations.push(ClusterLocation {
|
||||
location: pos.into(),
|
||||
data: ClusterLocationData::User {
|
||||
login: login.into(),
|
||||
relation,
|
||||
image,
|
||||
},
|
||||
})
|
||||
}
|
||||
(_, _, _) => {}
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
138
froxy-scraper/src/state.rs
Normal file
138
froxy-scraper/src/state.rs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use reqwest::{Method, Response};
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
use url::UrlQuery;
|
||||
|
||||
macro_rules! const_from_env {
|
||||
($const:ident, $env:literal) => {
|
||||
const $const: Option<&'static str> = option_env!($env);
|
||||
};
|
||||
($const:ident, $env:literal, $default:expr) => {
|
||||
const $const: &'static str = match option_env!($env) {
|
||||
Some(s) => s,
|
||||
None => $default,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const_from_env!(USER_AGENT_APP, "FROXY_UA_APP_NAME", "Froxy");
|
||||
const_from_env!(USER_AGENT_VERSION, "FROXY_UA_APP_VERSION", {
|
||||
const_from_env!(CARGO_PKG_VERSION, "CARGO_PKG_VERSION", "0.1-alpha");
|
||||
CARGO_PKG_VERSION
|
||||
});
|
||||
const_from_env!(
|
||||
USER_AGENT_DESC,
|
||||
"FROXY_UA_DESC",
|
||||
"(by maiboyer; for maiboyer)"
|
||||
);
|
||||
|
||||
pub const DEFAULT_USER_AGENT: &str =
|
||||
constcat::concat!(USER_AGENT_APP, "/", USER_AGENT_VERSION, USER_AGENT_DESC);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State {
|
||||
client: reqwest::Client,
|
||||
base_url: url::Url,
|
||||
secret_cookie: secrecy::SecretString,
|
||||
user_agent: std::borrow::Cow<'static, str>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new(secret_cookie: impl AsRef<str>) -> Self {
|
||||
let user_agent = DEFAULT_USER_AGENT;
|
||||
let base_url = "https://friends.42paris.fr";
|
||||
|
||||
Self::with_info(
|
||||
secret_cookie,
|
||||
user_agent.into(),
|
||||
url::Url::parse(base_url).expect("default base url not valid"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn with_info(
|
||||
secret_cookie: impl AsRef<str>,
|
||||
user_agent: std::borrow::Cow<'static, str>,
|
||||
base_url: url::Url,
|
||||
) -> Self {
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent(&*user_agent)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
Self {
|
||||
client,
|
||||
base_url,
|
||||
secret_cookie: secret_cookie.as_ref().into(),
|
||||
user_agent,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client(&self) -> &reqwest::Client {
|
||||
&self.client
|
||||
}
|
||||
|
||||
pub fn base_url(&self) -> &url::Url {
|
||||
&self.base_url
|
||||
}
|
||||
|
||||
pub fn cookie(&self) -> &SecretString {
|
||||
&self.secret_cookie
|
||||
}
|
||||
|
||||
pub fn user_agent(&self) -> &str {
|
||||
&self.user_agent
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub async fn request(
|
||||
&self,
|
||||
method: Method,
|
||||
url: impl AsRef<str>,
|
||||
qs: Option<&str>,
|
||||
body: Option<Vec<u8>>,
|
||||
) -> Result<Response, reqwest::Error> {
|
||||
let mut u = self.base_url.clone();
|
||||
u.set_path(url.as_ref());
|
||||
u.set_query(qs);
|
||||
let builder = self
|
||||
.client
|
||||
.request(method, u)
|
||||
.header("Cookie", self.secret_cookie.expose_secret());
|
||||
|
||||
let builder = if let Some(body) = body {
|
||||
builder.body(body)
|
||||
} else {
|
||||
builder
|
||||
};
|
||||
|
||||
builder.send().await
|
||||
}
|
||||
|
||||
pub async fn get(
|
||||
&self,
|
||||
url: impl AsRef<str>,
|
||||
qs: Option<&str>,
|
||||
) -> Result<Response, reqwest::Error> {
|
||||
self.request(Method::GET, url.as_ref(), qs, None).await
|
||||
}
|
||||
|
||||
pub async fn post(
|
||||
&self,
|
||||
url: impl AsRef<str>,
|
||||
qs: Option<&str>,
|
||||
) -> Result<Response, reqwest::Error> {
|
||||
self.request(Method::POST, url.as_ref(), qs, None).await
|
||||
}
|
||||
|
||||
pub async fn post_with_body(
|
||||
&self,
|
||||
url: impl AsRef<str>,
|
||||
qs: Option<&str>,
|
||||
body: Vec<u8>,
|
||||
) -> Result<Response, reqwest::Error> {
|
||||
self.request(Method::POST, url.as_ref(), qs, Some(body))
|
||||
.await
|
||||
}
|
||||
}
|
||||
48
froxy-scraper/src/types.rs
Normal file
48
froxy-scraper/src/types.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Default, Eq, PartialEq)]
|
||||
pub enum ClusterLocationStatus {
|
||||
#[doc(alias = "red")]
|
||||
Dead,
|
||||
#[doc(alias = "orange")]
|
||||
Damaged,
|
||||
#[doc(alias = "good")]
|
||||
#[default]
|
||||
Ok,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub enum Relation {
|
||||
CloseFriend,
|
||||
Friend,
|
||||
Pooled,
|
||||
Me,
|
||||
Focus,
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub enum ClusterLocationData {
|
||||
User {
|
||||
login: smol_str::SmolStr,
|
||||
image: Option<url::Url>,
|
||||
relation: Relation,
|
||||
},
|
||||
Normal {
|
||||
status: ClusterLocationStatus,
|
||||
},
|
||||
#[default]
|
||||
Empty,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClusterLocation {
|
||||
pub location: smol_str::SmolStr,
|
||||
pub data: ClusterLocationData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CluserInformation {
|
||||
pub cluster_name: smol_str::SmolStr,
|
||||
pub locations: Vec<ClusterLocation>,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue