412d17cddc6941cb77a79cc75b7169e2fb885baa
[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
49 loop {
50 let url = format!("https://www.steg-electronics.ch/fr/search?suche={}", to_find);
51 println!("Request: {}", url);
52 let resp = reqwest::blocking::get(&url)?;
53
54 if resp.status().is_success() {
55 let html = resp.text()?;
56 let document = Html::parse_document(&html);
57
58 // A vector of (<title>, <url>), empty if nothing matches.
59 let match_items: Vec<(String, String)> =
60 document.select(&selector).filter_map(
61 |element| {
62 if let (Some(title), Some(url_to_item)) = (element.value().attr("title"), element.value().attr("href")) {
63 if title.find(to_match).is_some() {
64 return Some((String::from(title), String::from(url_to_item)))
65 }
66 }
67 None
68 }
69 ).collect();
70
71 if match_items.is_empty() {
72 println!("No matches...");
73 } else if send_email(&match_items[..], &config.smtp_login, &config.smtp_password) {
74 println!("Some matches has been found! An e-mail has been sent!");
75 return Ok(())
76 } else {
77 println!("Unable to send e-mail");
78 }
79 } else {
80 println!("Request unsuccessful:\n{:#?}", resp);
81 }
82
83 thread::sleep(time::Duration::from_secs(60)); // 1 min.
84 }
85 }
86
87 /// Return 'true' if the email has been successfully sent.
88 fn send_email(items: &[(String, String)], login: &str, password: &str) -> bool {
89
90 let items_list_html =
91 items.iter().fold(
92 String::new(),
93 |acc, (title, url)| {
94 format!("{}\n<li><a href=\"{}\">{}</a></li>", acc, url, title)
95 }
96 );
97
98 let email = Email::builder()
99 // Addresses can be specified by the tuple (email, alias)
100 .to(("greg.burri@gmail.com", "Greg Burri"))
101 .from("redmine@d-lan.net")
102 .subject("RTX 3080 AVAILABLE!")
103 .html(format!("<ul>{}</ul>", items_list_html))
104 .build()
105 .unwrap()
106 .into();
107
108 let mut tls_builder = TlsConnector::builder();
109 tls_builder.min_protocol_version(Some(Protocol::Tlsv10));
110 let tls_parameters =
111 ClientTlsParameters::new(
112 "mail.gandi.net".to_string(),
113 tls_builder.build().unwrap()
114 );
115
116 let mut mailer =
117 SmtpClient::new(("mail.gandi.net", 465), ClientSecurity::Wrapper(tls_parameters)).unwrap()
118 .authentication_mechanism(Mechanism::Login)
119 .smtp_utf8(true)
120 .credentials(Credentials::new(String::from(login), String::from(password)))
121 .connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
122 .transport();
123
124 let result = mailer.send(email);
125
126 if result.is_err() {
127 println!("Error when sending E-mail:\n{:?}", &result);
128 }
129
130 mailer.close();
131
132 result.is_ok()
133 }