aeafb571d572e78a17c82dca940eba7f8553a2db
[rtx3080.git] / src / main.rs
1 use std::{ thread, time, fs::File };
2 use scraper::{ Html, Selector };
3 use lettre::{
4 ClientSecurity,
5 ClientTlsParameters,
6 Transport,
7 SmtpClient,
8 smtp::{
9 ConnectionReuseParameters,
10 authentication::{
11 Credentials,
12 Mechanism
13 }
14 }
15 };
16 use lettre_email::Email;
17 use native_tls::{Protocol, TlsConnector};
18 use ron::{ de::from_reader, ser::to_writer };
19 use serde::{ Deserialize, Serialize };
20
21 #[derive(Debug, Deserialize, Serialize)]
22 struct Config {
23 smtp_login: String,
24 smtp_password: String
25 }
26
27 fn get_default_config() -> Config { Config { smtp_login: "login".to_string(), smtp_password: "password".to_string() } }
28 const FILE_CONF: &str = "config.ron";
29
30 fn main() -> Result<(), Box<dyn std::error::Error>> {
31 println!("I need a RTX 3080 right now :)");
32
33 let to_find = "RTX 3080";
34 let to_match = "3080";
35
36 let config: Config =
37 match File::open(FILE_CONF) {
38 Ok(file) => match from_reader(file) { Ok(c) => c, Err(e) => panic!("Failed to load config: {}", e)},
39 Err(_) => {
40 let file = File::create(FILE_CONF)?;
41 let default_config = get_default_config();
42 to_writer(file, &default_config)?;
43 default_config
44 }
45 };
46
47 let selector = Selector::parse("div.productGridElement > h2 > a:nth-child(1)").unwrap();
48 let url = format!("https://www.steg-electronics.ch/fr/search?suche={}", to_find);
49
50 let client = reqwest::blocking::Client::new();
51
52 loop {
53 println!("Request: {}", url);
54
55 match client.get(&url).send() {
56 Ok(resp) =>
57 if resp.status().is_success() {
58 let html = resp.text()?;
59 let document = Html::parse_document(&html);
60
61 // A vector of (<title>, <url>), empty if nothing matches.
62 let match_items: Vec<(String, String)> =
63 document.select(&selector).filter_map(
64 |element| {
65 if let (Some(title), Some(url_to_item)) = (element.value().attr("title"), element.value().attr("href")) {
66 if title.find(to_match).is_some() {
67 return Some((String::from(title), String::from(url_to_item)))
68 }
69 }
70 None
71 }
72 ).collect();
73
74 if match_items.is_empty() {
75 println!("No matches...");
76 } else if send_email(&match_items[..], &config.smtp_login, &config.smtp_password) {
77 println!("Some matches has been found! An e-mail has been sent!");
78 return Ok(())
79 } else {
80 println!("Unable to send e-mail");
81 }
82 } else {
83 println!("Request unsuccessful:\n{:#?}", resp);
84 },
85 Err(error) =>
86 println!("Error during request: {:?}", error)
87 }
88
89 thread::sleep(time::Duration::from_secs(60)); // 1 min.
90 }
91 }
92
93 /// Return 'true' if the email has been successfully sent.
94 fn send_email(items: &[(String, String)], login: &str, password: &str) -> bool {
95
96 let items_list_html =
97 items.iter().fold(
98 String::new(),
99 |acc, (title, url)| {
100 format!("{}\n<li><a href=\"{}\">{}</a></li>", acc, url, title)
101 }
102 );
103
104 let email = Email::builder()
105 // Addresses can be specified by the tuple (email, alias)
106 .to(("greg.burri@gmail.com", "Greg Burri"))
107 .from("redmine@d-lan.net")
108 .subject("RTX 3080 AVAILABLE!")
109 .html(format!("<ul>{}</ul>", items_list_html))
110 .build()
111 .unwrap()
112 .into();
113
114 let mut tls_builder = TlsConnector::builder();
115 tls_builder.min_protocol_version(Some(Protocol::Tlsv10));
116 let tls_parameters =
117 ClientTlsParameters::new(
118 "mail.gandi.net".to_string(),
119 tls_builder.build().unwrap()
120 );
121
122 let mut mailer =
123 SmtpClient::new(("mail.gandi.net", 465), ClientSecurity::Wrapper(tls_parameters)).unwrap()
124 .authentication_mechanism(Mechanism::Login)
125 .smtp_utf8(true)
126 .credentials(Credentials::new(String::from(login), String::from(password)))
127 .connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
128 .transport();
129
130 let result = mailer.send(email);
131
132 if result.is_err() {
133 println!("Error when sending E-mail:\n{:?}", &result);
134 }
135
136 mailer.close();
137
138 result.is_ok()
139 }