mirror of
https://github.com/davegallant/rfd-notify.git
synced 2025-08-06 08:43:39 +00:00
Add initial (#1)
This commit is contained in:
40
src/config.rs
Normal file
40
src/config.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
extern crate toml;
|
||||
use serde_derive::Deserialize;
|
||||
use std::fs;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_config() {
|
||||
let file = "./examples/config.toml";
|
||||
parse(&file);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Config {
|
||||
pub expressions: Vec<String>,
|
||||
pub sendgrid: Sendgrid,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Sendgrid {
|
||||
pub mail_from: String,
|
||||
pub mail_to: String,
|
||||
pub api_key: String,
|
||||
}
|
||||
|
||||
pub fn parse(filename: &str) -> Config {
|
||||
let contents = fs::read_to_string(filename)
|
||||
.unwrap_or_else(|e| panic!("Unable to read configuration file '{}'. {}", filename, e));
|
||||
let config: Config = toml::from_str(&contents).unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"Unable to parse configuration with contents: {}. {}",
|
||||
contents, e
|
||||
)
|
||||
});
|
||||
|
||||
config
|
||||
}
|
23
src/db.rs
Normal file
23
src/db.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
pub fn hash_exists(hash: &str) -> bool {
|
||||
let tree = sled::open("./deals_db").expect("open");
|
||||
let result = tree.get(hash);
|
||||
if result.is_err() {
|
||||
error!("{:?}", &result);
|
||||
}
|
||||
if result == Ok(None) {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn insert(hash: &str) {
|
||||
let tree = sled::open("./deals_db").expect("open");
|
||||
let result = tree.insert(&hash, "");
|
||||
if result.is_err() {
|
||||
error!("{:?}", &result);
|
||||
}
|
||||
let result = tree.flush();
|
||||
if result.is_err() {
|
||||
error!("{:?}", &result);
|
||||
}
|
||||
}
|
46
src/mail.rs
Normal file
46
src/mail.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use crate::config::Config;
|
||||
use crate::rfd::Posts;
|
||||
use crate::rfd::Topic;
|
||||
use sendgrid::SGClient;
|
||||
use sendgrid::{Destination, Mail};
|
||||
|
||||
const RFD_FORUMS_BASE_URL: &str = "https://forums.redflagdeals.com";
|
||||
|
||||
pub fn send(topic: &Topic, posts: &Posts, config: &Config) {
|
||||
let api_key = &config.sendgrid.api_key;
|
||||
let sg = SGClient::new(api_key.to_string());
|
||||
|
||||
let html_message = format!(
|
||||
"\
|
||||
<b>First Posted:</b> {}
|
||||
<br>
|
||||
<b>DEALER:</b> {:?}
|
||||
<br>
|
||||
<b>DEAL:</b> {:?}
|
||||
<br>
|
||||
<b>POST:</b> {}\
|
||||
<br>
|
||||
<br>
|
||||
<b>Body:</b> {}
|
||||
",
|
||||
topic.post_time,
|
||||
topic.offer.dealer_name,
|
||||
topic.offer.url,
|
||||
format!("{}/{}", RFD_FORUMS_BASE_URL, topic.web_path),
|
||||
posts.posts[0].body,
|
||||
);
|
||||
|
||||
let mail_info = Mail::new()
|
||||
.add_to(Destination {
|
||||
address: &config.sendgrid.mail_to,
|
||||
name: "you",
|
||||
})
|
||||
.add_from(&config.sendgrid.mail_from)
|
||||
.add_subject(&topic.title)
|
||||
.add_html(&html_message);
|
||||
|
||||
match sg.send(mail_info) {
|
||||
Err(err) => println!("Error: {}", err),
|
||||
Ok(body) => println!("Response: {}", body),
|
||||
};
|
||||
}
|
38
src/main.rs
Normal file
38
src/main.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
extern crate pretty_env_logger;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate crypto;
|
||||
mod config;
|
||||
mod db;
|
||||
mod mail;
|
||||
mod rfd;
|
||||
|
||||
use std::env;
|
||||
|
||||
fn help() {
|
||||
println!(
|
||||
"usage:\n
|
||||
rfd-notify <config-toml>
|
||||
Specify the filepath of the config."
|
||||
);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
pretty_env_logger::init();
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
match args.len() {
|
||||
2 => {
|
||||
let config_path = &args[1];
|
||||
let config = config::parse(config_path);
|
||||
debug!("{:?}\n", config);
|
||||
let hot_deals = rfd::get_hot_deals().map_err(|err| error!("{:?}", err)).ok();
|
||||
let parsed_deals = rfd::parse_hot_deals(&hot_deals.unwrap());
|
||||
rfd::match_deals(parsed_deals, config)
|
||||
}
|
||||
_ => {
|
||||
help();
|
||||
}
|
||||
}
|
||||
info!("Complete")
|
||||
}
|
118
src/rfd.rs
Normal file
118
src/rfd.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use crate::config::Config;
|
||||
use crate::db;
|
||||
use crate::mail;
|
||||
use crypto::digest::Digest;
|
||||
use crypto::sha2::Sha256;
|
||||
use regex::RegexBuilder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Deals {
|
||||
topics: Vec<Topic>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Posts {
|
||||
pub posts: Vec<Post>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Post {
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Topic {
|
||||
#[serde(rename = "topic_id")]
|
||||
pub id: u32,
|
||||
pub title: String,
|
||||
pub post_time: String,
|
||||
pub web_path: String,
|
||||
pub offer: Offer,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Offer {
|
||||
pub dealer_name: Option<String>,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn get_hot_deals() -> Result<String, Box<dyn std::error::Error>> {
|
||||
let resp = reqwest::get("https://forums.redflagdeals.com/api/topics?forum_id=9&per_page=40")
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn get_topic(topic_id: u32) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let resp = reqwest::get(&format!(
|
||||
"https://forums.redflagdeals.com/api/topics/{}/posts?per_page=1&page=1",
|
||||
topic_id
|
||||
))
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
pub fn parse_hot_deals(response: &str) -> Deals {
|
||||
serde_json::from_str(&response).unwrap()
|
||||
}
|
||||
|
||||
pub fn parse_posts(response: String) -> Posts {
|
||||
serde_json::from_str(&response).unwrap()
|
||||
}
|
||||
|
||||
fn hash_deal(topic: &Topic) -> String {
|
||||
let digest = format!("{}{}{}", topic.web_path, topic.title, topic.post_time);
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.input_str(&digest);
|
||||
hasher.result_str()
|
||||
}
|
||||
|
||||
pub fn match_deals(deals: Deals, config: Config) {
|
||||
for topic in deals.topics.iter() {
|
||||
for expression in config.expressions.iter() {
|
||||
let mut found_match = false;
|
||||
let re = RegexBuilder::new(expression)
|
||||
.case_insensitive(true)
|
||||
.build()
|
||||
.unwrap_or_else(|e| panic!("Invalid regex: {}. {}", expression, e));
|
||||
if re.is_match(&topic.title) {
|
||||
found_match = true;
|
||||
debug!(
|
||||
"Expression '{}' matched title: {}",
|
||||
expression, &topic.title
|
||||
)
|
||||
} else if topic.offer.dealer_name.is_some() {
|
||||
let dealer_name = topic.offer.dealer_name.as_ref().unwrap();
|
||||
if re.is_match(&dealer_name) {
|
||||
found_match = true;
|
||||
debug!(
|
||||
"Expression '{}' matched dealer: {}",
|
||||
expression, &topic.title
|
||||
)
|
||||
}
|
||||
}
|
||||
if !found_match {
|
||||
continue;
|
||||
}
|
||||
let deal_hash = hash_deal(topic);
|
||||
if db::hash_exists(&deal_hash) {
|
||||
debug!("deal hash '{}' already exists", &deal_hash);
|
||||
} else {
|
||||
let posts = parse_posts(
|
||||
get_topic(topic.id)
|
||||
.map_err(|err| error!("{:?}", err))
|
||||
.ok()
|
||||
.unwrap(),
|
||||
);
|
||||
db::insert(&deal_hash);
|
||||
mail::send(topic, &posts, &config);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user