Automatic Lemmy Post creation using jnovel rss feeds only on new chapter
This commit is contained in:
parent
c1eba780d1
commit
17283a9d9b
5 changed files with 2573 additions and 0 deletions
178
src/config/mod.rs
Normal file
178
src/config/mod.rs
Normal file
|
@ -0,0 +1,178 @@
|
|||
use std::{fs::{self, OpenOptions}, path::Path, io::Write};
|
||||
|
||||
use lemmy_api_common::sensitive::Sensitive;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
macro_rules! pub_struct {
|
||||
($name:ident {$($field:ident: $t:ty,)*}) => {
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub(crate) struct $name {
|
||||
$(pub(crate) $field: $t), *
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Secrets structs
|
||||
pub_struct!(Secrets {
|
||||
lemmy: LemmyLogin,
|
||||
reddit: RedditLogin,
|
||||
});
|
||||
|
||||
impl Secrets {
|
||||
pub(crate) fn load() -> Secrets {
|
||||
let file_contents = match fs::read_to_string("secrets.json") {
|
||||
Ok(data) => data,
|
||||
Err(e) => panic!("ERROR: secrets.json could not be read:\n\n{:#?}", e),
|
||||
};
|
||||
let config_parse: Secrets = match serde_json::from_str(&file_contents) {
|
||||
Ok(data) => data,
|
||||
Err(e) => panic!("ERROR: secrets.json could not be parsed:\n\n{:#?}", e),
|
||||
};
|
||||
|
||||
return config_parse;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub(crate) struct LemmyLogin {
|
||||
pub(crate) username: String,
|
||||
password: String
|
||||
}
|
||||
|
||||
impl LemmyLogin {
|
||||
pub(crate) fn get_username(&self) -> Sensitive<String> {
|
||||
return Sensitive::new(self.username.clone());
|
||||
}
|
||||
|
||||
pub(crate) fn get_password(&self) -> Sensitive<String> {
|
||||
return Sensitive::new(self.password.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub(crate) struct RedditLogin {
|
||||
pub(crate) app_id: String,
|
||||
app_secret: String,
|
||||
refresh_token: String,
|
||||
redirect_uri: String,
|
||||
}
|
||||
|
||||
// Config structs
|
||||
pub_struct!(Config {
|
||||
reddit_config: RedditConfig,
|
||||
feeds: Vec<FeedSetting>,
|
||||
});
|
||||
|
||||
impl Config {
|
||||
pub(crate) fn load() -> Config {
|
||||
let file_contents = match fs::read_to_string("config.json") {
|
||||
Ok(data) => data,
|
||||
Err(e) => panic!("ERROR: config.json could not be read:\n\n{:#?}", e),
|
||||
};
|
||||
let config_parse: Config = match serde_json::from_str(&file_contents) {
|
||||
Ok(data) => data,
|
||||
Err(e) => panic!("ERROR: config.json could not be parsed:\n\n{:#?}", e),
|
||||
};
|
||||
|
||||
return config_parse;
|
||||
}
|
||||
}
|
||||
|
||||
pub_struct!(RedditConfig {
|
||||
user_agent: String,
|
||||
subreddit: String,
|
||||
});
|
||||
|
||||
pub_struct!(FeedSetting {
|
||||
feed_url: String,
|
||||
communities: FeedCommunities,
|
||||
reddit: FeedRedditSettings,
|
||||
});
|
||||
|
||||
pub_struct!(FeedCommunities {
|
||||
chapter: LemmyCommunities,
|
||||
volume: LemmyCommunities,
|
||||
});
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub(crate) enum LemmyCommunities {
|
||||
aobwebnovel,
|
||||
aobprepub,
|
||||
aoblightnovel,
|
||||
aobmanga,
|
||||
aobanime
|
||||
}
|
||||
|
||||
pub_struct!(FeedRedditSettings {
|
||||
enabled: bool,
|
||||
flair: String,
|
||||
});
|
||||
|
||||
// Posts structs
|
||||
pub_struct!(PrevPost {
|
||||
title: String,
|
||||
last_post_url: String,
|
||||
});
|
||||
|
||||
impl PrevPost {
|
||||
pub(crate) fn load() -> Vec<PrevPost> {
|
||||
let history;
|
||||
|
||||
if Path::new("posts.json").exists() {
|
||||
let file_contents = match fs::read_to_string("posts.json") {
|
||||
Ok(data) => data,
|
||||
Err(e) => panic!("ERROR: secrets.json could not be read:\n\n{:#?}", e),
|
||||
};
|
||||
|
||||
if file_contents.len() > 0 {
|
||||
let history_parse: Vec<PrevPost> = match serde_json::from_str(&file_contents) {
|
||||
Ok(data) => data,
|
||||
Err(e) => panic!("ERROR: secrets.json could not be parsed:\n\n{:#?}", e),
|
||||
};
|
||||
history = history_parse;
|
||||
}
|
||||
else {
|
||||
history = [].to_vec()
|
||||
}
|
||||
}
|
||||
else {
|
||||
let _ = fs::File::create("posts.json");
|
||||
history = [].to_vec()
|
||||
}
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
pub(crate) fn save(data: &Vec<PrevPost>) {
|
||||
let mut file = OpenOptions::new().read(true).write(true).create(true).open("posts.json").unwrap();
|
||||
|
||||
let json_data = serde_json::to_string_pretty(&data).unwrap();
|
||||
|
||||
write!(&mut file, "{}", json_data).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// RSS Feed Structs
|
||||
pub_struct!(FeedData {
|
||||
version: String,
|
||||
title: String,
|
||||
home_page_url: String,
|
||||
description: String,
|
||||
author: FeedAuthor,
|
||||
items: Vec<FeedEntry>,
|
||||
});
|
||||
|
||||
pub_struct!(FeedAuthor {
|
||||
name: String,
|
||||
});
|
||||
|
||||
pub_struct!(FeedEntry {
|
||||
id: String,
|
||||
url: String,
|
||||
title: String,
|
||||
summary: String,
|
||||
image: Option<String>,
|
||||
date_published: String,
|
||||
});
|
||||
|
182
src/main.rs
Normal file
182
src/main.rs
Normal file
|
@ -0,0 +1,182 @@
|
|||
use chrono::Utc;
|
||||
use config::{Config, PrevPost, Secrets};
|
||||
use lemmy_api_common::{
|
||||
person::{Login, LoginResponse},
|
||||
post::{CreatePost, GetPosts, GetPostsResponse},
|
||||
sensitive::Sensitive,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
newtypes::{CommunityId, LanguageId},
|
||||
ListingType, SortType,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::{blocking::Client, StatusCode};
|
||||
use std::{thread::sleep, time};
|
||||
use url::Url;
|
||||
|
||||
use crate::config::FeedData;
|
||||
mod config;
|
||||
|
||||
pub static CLIENT: Lazy<Client> = Lazy::new(|| {
|
||||
let client = Client::builder()
|
||||
.timeout(time::Duration::from_secs(30))
|
||||
.connect_timeout(time::Duration::from_secs(30))
|
||||
.build()
|
||||
.expect("build client");
|
||||
client
|
||||
});
|
||||
|
||||
struct Bot {
|
||||
secrets: Secrets,
|
||||
config: Config,
|
||||
post_history: Vec<PrevPost>,
|
||||
auth: Sensitive<String>,
|
||||
}
|
||||
|
||||
impl Bot {
|
||||
pub(crate) fn new() -> Bot {
|
||||
Bot {
|
||||
secrets: Secrets::load(),
|
||||
config: Config::load(),
|
||||
post_history: PrevPost::load(),
|
||||
auth: Sensitive::new("".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn login(&mut self) {
|
||||
let login_params = Login {
|
||||
username_or_email: self.secrets.lemmy.get_username(),
|
||||
password: self.secrets.lemmy.get_password(),
|
||||
};
|
||||
|
||||
let res = CLIENT
|
||||
.post("https://lemmy.neshweb.net/api/v3/user/login")
|
||||
.json(&login_params)
|
||||
.send()
|
||||
.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
let data: &LoginResponse = &res.json().unwrap();
|
||||
|
||||
let jwt = data.jwt.clone().expect("JWT Token could not be acquired");
|
||||
self.auth = jwt;
|
||||
} else {
|
||||
println!("Error Code: {:?}", res.status());
|
||||
panic!("JWT Token could not be acquired");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn post(&mut self, post_data: CreatePost) {
|
||||
let res = CLIENT
|
||||
.post("https://lemmy.neshweb.net/api/v3/post")
|
||||
.json(&post_data)
|
||||
.send()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn list_posts(auth: &Sensitive<String>) -> GetPostsResponse {
|
||||
let params = GetPosts {
|
||||
type_: Some(ListingType::Local),
|
||||
sort: Some(SortType::New),
|
||||
auth: Some(auth.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let res = CLIENT
|
||||
.get("https://lemmy.neshweb.net/api/v3/post/list")
|
||||
.query(¶ms)
|
||||
.send()
|
||||
.unwrap()
|
||||
.text()
|
||||
.unwrap();
|
||||
|
||||
return serde_json::from_str(&res).unwrap();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Get all needed auth tokens at the start
|
||||
let mut old = Utc::now().time();
|
||||
let mut this = Bot::new();
|
||||
println!("{}", this.secrets.lemmy.username);
|
||||
this.login();
|
||||
|
||||
// Create empty eTag list
|
||||
println!("TODO: Etag list");
|
||||
|
||||
// Enter a loop (not for debugging)
|
||||
loop {
|
||||
let start = Utc::now();
|
||||
print!("\x1B[2J\x1B[1;1H");
|
||||
println!("Started loop at {} {}", start.format("%H:%M:%S"), start.timezone());
|
||||
|
||||
if start.time() - old > chrono::Duration::seconds(6) {
|
||||
old = start.time();
|
||||
this.config = Config::load();
|
||||
}
|
||||
|
||||
// Start the polling process
|
||||
// Get all feed URLs (use cache)
|
||||
let mut post_queue: Vec<CreatePost> = vec![];
|
||||
this.config.feeds.iter().for_each(|feed| {
|
||||
let res = CLIENT
|
||||
.get(feed.feed_url.clone())
|
||||
.send()
|
||||
.unwrap()
|
||||
.text()
|
||||
.unwrap();
|
||||
let data: FeedData = serde_json::from_str(&res).unwrap();
|
||||
|
||||
let mut prev_post_idx: Option<usize> = None;
|
||||
let mut do_post = true;
|
||||
this.post_history
|
||||
.iter()
|
||||
.enumerate()
|
||||
.for_each(|(idx, post)| {
|
||||
if &post.last_post_url == &data.items[0].url {
|
||||
do_post = false;
|
||||
} else if &post.title == &data.title {
|
||||
prev_post_idx = Some(idx);
|
||||
}
|
||||
});
|
||||
|
||||
if do_post {
|
||||
let item = &data.items[0];
|
||||
let new_post = CreatePost {
|
||||
name: item.title.clone(),
|
||||
community_id: CommunityId(3), // TODO get community id by using community name at the start, save it in a list, planned refresh once a day
|
||||
url: Some(Url::parse(&item.url).unwrap()),
|
||||
body: Some(
|
||||
"[Reddit](https://reddit.com)\n\n[Discord](https://discord.com)".into(),
|
||||
),
|
||||
honeypot: None,
|
||||
nsfw: Some(false),
|
||||
language_id: Some(LanguageId(0)), // TODO get English language id by api (at the start)
|
||||
auth: this.auth.clone(),
|
||||
};
|
||||
post_queue.push(new_post);
|
||||
match prev_post_idx {
|
||||
Some(idx) => {
|
||||
this.post_history[idx].title = data.title;
|
||||
this.post_history[idx].last_post_url = item.url.clone();
|
||||
}
|
||||
None => this.post_history.push(PrevPost {
|
||||
title: data.title,
|
||||
last_post_url: item.url.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
sleep(time::Duration::from_millis(100)); // Should prevent dos-ing J-Novel servers
|
||||
});
|
||||
|
||||
PrevPost::save(&this.post_history);
|
||||
post_queue.iter().for_each(|post| {
|
||||
println!("Posted: {}", post.name);
|
||||
this.post(post.clone());
|
||||
});
|
||||
|
||||
while Utc::now().time() - start.time() < chrono::Duration::seconds(60) {
|
||||
sleep(time::Duration::from_secs(10));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue