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