chore(renamed): removed froxy prefix on folders

This commit is contained in:
Maieul BOYER 2026-02-08 18:13:20 +01:00
parent e57bb34a35
commit 6960959794
Signed by: maix
SSH key fingerprint: SHA256:iqCzqFFF5KjRixmDExqbAltCIj9ndlBWIGJf3t9Ln9g
29 changed files with 32 additions and 12 deletions

5
scraper/src/lib.rs Normal file
View file

@ -0,0 +1,5 @@
#![allow(dead_code)]
pub mod parsing;
pub mod state;
pub mod types;

188
scraper/src/parsing.rs Normal file
View file

@ -0,0 +1,188 @@
use scraper::Selector;
use smol_str::SmolStr;
use url::Url;
use crate::types::{ClusterLocation, ClusterLocationData, Friend, Friends, 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_pooled {
relation = Relation::Pooled;
}
if is_close_friend {
relation = Relation::CloseFriend;
}
if is_friend {
relation = Relation::Friend;
}
if is_me {
relation = Relation::Me;
}
if is_focus {
relation = Relation::Focus;
}
out.locations.push(ClusterLocation {
location: pos.into(),
data: ClusterLocationData::User {
login: login.into(),
relation,
image,
},
})
}
(_, _, _) => {}
}
}
out
}
sel!(FRIEND_CONT_SEL, "div.container > div > div.card.pl-fix");
sel!(FRIEND_CARD_TITLE, "h5.card-title");
sel!(FRIEND_LAST_ACTIVE, ".card-text");
pub fn friend_page(page: impl AsRef<str>) -> Friends {
let page = page.as_ref();
let doc = scraper::Html::parse_document(page);
let mut out = Friends {
friends: Vec::new(),
};
for f in doc.select(&FRIEND_CONT_SEL) {
let Some(login) = f
.select(&FRIEND_CARD_TITLE)
.next()
.map(|e| {
e.text()
.fold(String::new(), |mut acc: String, s: &str| -> String {
acc += s;
acc
})
})
.map(|s| SmolStr::new(s.trim()))
else {
continue;
};
let img = f
.select(&IMAGE_SEL)
.next()
.and_then(|e| e.attr("src"))
.map(ToString::to_string);
let pos_la = f
.select(&FRIEND_LAST_ACTIVE)
.next()
.map(|e| {
e.text()
.fold(String::new(), |mut acc: String, s: &str| -> String {
acc += s;
acc
})
})
.map(|s| SmolStr::new(s.trim()))
.and_then(|s| {
s.split_once(' ')
.map(|(pos, last_active)| {
(Some(SmolStr::new(pos)), Some(SmolStr::new(last_active)))
})
.or(Some((Some(s), None)))
});
out.friends.push(Friend {
login,
image: img,
position: pos_la
.as_ref()
.and_then(|(pos, _)| pos.clone())
.filter(|s| s != "Absent"),
last_active: pos_la.as_ref().and_then(|(_, la)| la.clone()),
})
}
out
}

181
scraper/src/state.rs Normal file
View file

@ -0,0 +1,181 @@
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 get_user(&self, name: &str) -> Result<Option<crate::types::Profile>, GetUserError> {
let req = self.get(format!("/getuser/{name}"), None).await?;
let text = req.text().await?;
let json = serde_json::from_str::<crate::types::ProfileRaw>(&text)?;
Ok(Some(crate::types::Profile {
admin: json.admin,
campus: json.campus,
id: json.id,
userid: json.userid,
pool: json.pool.filter(|s| !s.contains("None")),
image: json.image,
image_medium: json.image_medium,
is_friend: match json.is_friend {
crate::types::IsFriend::U32(u) => u != 0,
crate::types::IsFriend::Bool(b) => b,
},
lang: json.lang,
name: json.name,
position: json.position,
active: json.active,
website: json.website.filter(|s| !s.is_empty()),
recit: json.recit.filter(|s| !s.is_empty()),
discord: json.discord.filter(|s| !s.is_empty()),
github: json.github.filter(|s| !s.is_empty()),
}))
}
pub async fn add_friend(&self, names: &[impl AsRef<str>]) -> Result<bool, reqwest::Error> {
let mut names_comma =
names
.iter()
.map(AsRef::as_ref)
.fold(String::from("/friends/add/"), |mut s, name| {
s += name;
s += ",";
s
});
names_comma.pop();
self.get(names_comma, None)
.await
.map(|r| r.status().is_success())
}
}
#[derive(Debug, thiserror::Error)]
pub enum GetUserError {
#[error("RequestError: {0}")]
RequestError(#[from] reqwest::Error),
#[error("JsonError: {0}")]
JsonError(#[from] serde_json::Error),
}

117
scraper/src/types.rs Normal file
View file

@ -0,0 +1,117 @@
use smol_str::SmolStr;
#[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>,
}
#[derive(Debug, Clone)]
pub struct Friend {
pub login: SmolStr,
pub image: Option<String>,
pub position: Option<SmolStr>,
pub last_active: Option<SmolStr>,
}
#[derive(Debug, Clone)]
pub struct Friends {
pub friends: Vec<Friend>,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(untagged)]
pub(crate) enum IsFriend {
U32(u32),
Bool(bool),
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub(crate) struct ProfileRaw {
pub(crate) admin: bool,
pub(crate) active: Option<SmolStr>,
pub(crate) campus: u64,
pub(crate) id: u64,
pub(crate) userid: Option<u64>,
pub(crate) image: String,
pub(crate) image_medium: String,
pub(crate) is_friend: IsFriend,
pub(crate) lang: SmolStr,
pub(crate) name: SmolStr,
pub(crate) pool: Option<SmolStr>,
pub(crate) position: Option<SmolStr>,
pub(crate) recit: Option<String>,
pub(crate) website: Option<String>,
pub(crate) discord: Option<String>,
pub(crate) github: Option<String>,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct Profile {
pub admin: bool,
pub campus: u64,
pub id: u64,
pub userid: Option<u64>,
pub pool: Option<SmolStr>,
pub image: String,
pub image_medium: String,
pub is_friend: bool,
pub lang: SmolStr,
pub name: SmolStr,
pub position: Option<SmolStr>,
pub active: Option<SmolStr>,
pub website: Option<String>,
pub recit: Option<String>,
pub discord: Option<String>,
pub github: Option<String>,
}