From 5e95d2b62c18f0acf5d3e69a6aa2a4a55a2036ca Mon Sep 17 00:00:00 2001 From: Neshura Date: Wed, 21 Jun 2023 21:29:14 +0200 Subject: [PATCH 1/3] Static Example TUI --- Cargo.lock | 96 ++++++++++++++++++++ Cargo.toml | 2 + src/main.rs | 255 +++++++++++++++++++++++++++++++++------------------- 3 files changed, 261 insertions(+), 92 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5a6490a..874d303 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,6 +93,7 @@ name = "ascendance-of-a-bookworm-bot" version = "1.1.0" dependencies = [ "chrono", + "crossterm 0.26.1", "lemmy_api_common", "lemmy_db_schema", "once_cell", @@ -101,6 +102,7 @@ dependencies = [ "serde_derive", "serde_json", "strum_macros 0.25.0", + "tui", "url", ] @@ -203,6 +205,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + [[package]] name = "cc" version = "1.0.79" @@ -266,6 +274,47 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "encoding_rs" version = "0.8.32" @@ -772,6 +821,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", + "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -1164,6 +1214,27 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "signal-hook" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -1478,6 +1549,19 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "tui" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" +dependencies = [ + "bitflags", + "cassowary", + "crossterm 0.25.0", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "typed-builder" version = "0.10.0" @@ -1510,6 +1594,18 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + [[package]] name = "url" version = "2.4.0" diff --git a/Cargo.toml b/Cargo.toml index c93197e..3ceaa82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] chrono = "0.4.26" +crossterm = "0.26.1" lemmy_api_common = "0.17.4" lemmy_db_schema = "0.17.4" once_cell = "1.18.0" @@ -15,4 +16,5 @@ serde = "1.0.164" serde_derive = "1.0.164" serde_json = "1.0.97" strum_macros = "0.25.0" +tui = "0.19.0" url = "2.4.0" diff --git a/src/main.rs b/src/main.rs index 617c333..5b628ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,9 @@ use lemmy_db_schema::{ }; use once_cell::sync::Lazy; use reqwest::{blocking::Client, StatusCode}; -use std::{thread::sleep, time}; +use tui::{backend::{CrosstermBackend, Backend}, Terminal, widgets::{Widget, Block, Borders, Cell, Row, Table}, layout::{Layout, Constraint, Direction}, Frame, style::{Style, Modifier, Color}}; +use crossterm::{event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},}; +use std::{thread::{sleep, self}, time::{self, Duration}, io}; use url::Url; use crate::config::FeedData; @@ -144,95 +146,164 @@ impl CommunitiesVector { } } -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(); - this.community_ids.load(&this.auth, &this.config.instance); - - // 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(); - this.community_ids.load(&this.auth, &this.config.instance); - } - - // Start the polling process - // Get all feed URLs (use cache) - let mut post_queue: Vec = 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 = 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: this.community_ids.find(&feed.communities.chapter), - 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(37)), // TODO get this id once every few hours per API request, the ordering of IDs suggests that the EN Id might change in the future - 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!("Posting: {}", post.name); - this.post(post.clone()); - }); - - while Utc::now().time() - start.time() < chrono::Duration::seconds(60) { - sleep(time::Duration::from_secs(10)); - } - } +fn run_bot() { + // 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(); + this.community_ids.load(&this.auth, &this.config.instance); + + // 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(); + this.community_ids.load(&this.auth, &this.config.instance); + } + + // Start the polling process + // Get all feed URLs (use cache) + let mut post_queue: Vec = 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 = 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: this.community_ids.find(&feed.communities.chapter), + 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(37)), // TODO get this id once every few hours per API request, the ordering of IDs suggests that the EN Id might change in the future + 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!("Posting: {}", post.name); + this.post(post.clone()); + }); + + while Utc::now().time() - start.time() < chrono::Duration::seconds(60) { + sleep(time::Duration::from_secs(10)); + } + } +} + +fn ui(f: &mut Frame) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints( + [ + Constraint::Percentage(10), + Constraint::Percentage(90), + ].as_ref() + ) + .split(f.size()); + + + let block = Block::default() + .title("Block") + .borders(Borders::ALL); + f.render_widget(block, chunks[0]); + + let selected_style = Style::default().add_modifier(Modifier::REVERSED); + let normal_style = Style::default().bg(Color::Blue); + let header_cells = ["Series", "Community", "Last Post"] + .iter() + .map(|h| Cell::from(*h).style(Style::default().fg(Color::Red))); + let header = Row::new(header_cells) + .style(normal_style) + .height(1) + .bottom_margin(1); + let rows = [ + Row::new([Cell::from("a1"), Cell::from("a2"), Cell::from("a3")]).height(1 as u16).bottom_margin(1), + Row::new([Cell::from("b1"), Cell::from("b2"), Cell::from("b3")]).height(1 as u16).bottom_margin(1), + Row::new([Cell::from("unicorn"), Cell::from("c2"), Cell::from("c3")]).height(2 as u16).bottom_margin(1) + ]; + + let t = Table::new(rows) + .header(header) + .block(Block::default().borders(Borders::ALL).title("Table")) + .highlight_style(selected_style) + .highlight_symbol(">> ") + .widths(&[ + Constraint::Percentage(50), + Constraint::Length(30), + Constraint::Min(10), + ]); + f.render_widget(t, chunks[1]); +} + +fn main() -> Result<(), io::Error> { + let stdout = io::stdout(); + let backend = CrosstermBackend::new(&stdout); + let mut terminal = Terminal::new(backend)?; + + terminal.draw(|f| { + ui(f); + })?; + + thread::sleep(Duration::from_secs(5)); + + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + Ok(()) + //run_bot(); } From 877c7ef32466ec5197444646dddce66c4a2588fa Mon Sep 17 00:00:00 2001 From: Neshura Date: Thu, 22 Jun 2023 22:08:10 +0200 Subject: [PATCH 2/3] Move Bot Logic to Class Methods --- src/config/mod.rs | 134 +++++++++++++++++++++++++++++++- src/main.rs | 194 +++++++++++----------------------------------- 2 files changed, 177 insertions(+), 151 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index c449ffe..6fb1519 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,7 +1,11 @@ -use std::{fs::{self, OpenOptions}, path::Path, io::Write}; +use std::{fs::{self, OpenOptions}, path::Path, io::Write, thread::sleep, time}; -use lemmy_api_common::sensitive::Sensitive; +use lemmy_api_common::{sensitive::Sensitive, post::CreatePost, community::{self, ListCommunities, ListCommunitiesResponse}}; +use lemmy_db_schema::newtypes::{LanguageId, CommunityId}; use serde_derive::{Deserialize, Serialize}; +use url::Url; + +use crate::{CLIENT}; macro_rules! pub_struct { ($name:ident {$($field:ident: $t:ty,)*}) => { @@ -19,7 +23,7 @@ pub_struct!(Secrets { }); impl Secrets { - pub(crate) fn load() -> Secrets { + pub(crate) fn init() -> 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), @@ -65,7 +69,7 @@ pub_struct!(Config { }); impl Config { - pub(crate) fn load() -> Config { + pub(crate) fn init() -> 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), @@ -77,6 +81,80 @@ impl Config { return config_parse; } + + pub(crate) fn load(&mut self) { + 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), + }; + + self.feeds = config_parse.feeds; + self.instance = config_parse.instance; + self.reddit_config = config_parse.reddit_config; + } + + pub(crate) fn check_feeds(&mut self, post_history: &mut Vec + , community_ids: &CommunitiesVector, auth: &Sensitive) -> Vec { + let mut post_queue: Vec = vec![]; + + self.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 = None; + let mut do_post = true; + 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: community_ids.find(&feed.communities.chapter), + 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(37)), // TODO get this id once every few hours per API request, the ordering of IDs suggests that the EN Id might change in the future + auth: auth.clone(), + }; + post_queue.push(new_post); + match prev_post_idx { + Some(idx) => { + post_history[idx].title = data.title; + post_history[idx].last_post_url = item.url.clone(); + } + None => 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(&post_history); + return post_queue; + } } pub_struct!(RedditConfig { @@ -177,3 +255,51 @@ pub_struct!(FeedEntry { date_published: String, }); +// Bot Helper Structs +pub_struct!(CommunitiesVector { + ids: Vec<(CommunityId, String)>, +}); + +impl CommunitiesVector { + pub(crate) fn new() -> CommunitiesVector { + CommunitiesVector{ids: vec![]} + } + + pub(crate) fn load(&mut self, auth: &Sensitive, base: &String) { + let params = ListCommunities { + auth: Some(auth.clone()), + ..Default::default() + }; + + let res = CLIENT + .get(base.clone() + "/api/v3/community/list") + .query(¶ms) + .send() + .unwrap() + .text() + .unwrap(); + + let site_data: ListCommunitiesResponse = serde_json::from_str(&res).unwrap(); + + let mut ids = [].to_vec(); + + site_data.communities.iter().for_each(|entry| { + let new_id = (entry.community.id, entry.community.name.clone()); + ids.push(new_id); + }); + + self.ids = ids; + } + + pub(crate) fn find(&self, name: &LemmyCommunities) -> CommunityId { + let mut ret_id = CommunityId(0); + + self.ids.iter().for_each(|id| { + let id_name = &id.1; + if &name.to_string() == id_name { + ret_id = id.0; + } + }); + return ret_id; + } +} diff --git a/src/main.rs b/src/main.rs index 5b628ef..0b243d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,19 @@ -use chrono::Utc; -use config::{Config, PrevPost, Secrets, LemmyCommunities}; +use chrono::{Utc, DateTime, NaiveTime}; +use config::{Config, PrevPost, Secrets, CommunitiesVector}; use lemmy_api_common::{ person::{Login, LoginResponse}, post::{CreatePost, GetPosts, GetPostsResponse}, - sensitive::Sensitive, community::{ListCommunities, ListCommunitiesResponse}, + sensitive::Sensitive, }; use lemmy_db_schema::{ - newtypes::{CommunityId, LanguageId}, ListingType, SortType, }; use once_cell::sync::Lazy; use reqwest::{blocking::Client, StatusCode}; -use tui::{backend::{CrosstermBackend, Backend}, Terminal, widgets::{Widget, Block, Borders, Cell, Row, Table}, layout::{Layout, Constraint, Direction}, Frame, style::{Style, Modifier, Color}}; +use tui::{backend::{CrosstermBackend, Backend}, Terminal, widgets::{Block, Borders, Cell, Row, Table}, layout::{Layout, Constraint, Direction}, Frame, style::{Style, Modifier, Color}}; use crossterm::{event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},}; use std::{thread::{sleep, self}, time::{self, Duration}, io}; -use url::Url; -use crate::config::FeedData; mod config; pub static CLIENT: Lazy = Lazy::new(|| { @@ -34,16 +31,18 @@ struct Bot { post_history: Vec, community_ids: CommunitiesVector, auth: Sensitive, + start_time: DateTime, } impl Bot { pub(crate) fn new() -> Bot { Bot { - secrets: Secrets::load(), - config: Config::load(), + secrets: Secrets::init(), + config: Config::init(), post_history: PrevPost::load(), community_ids: CommunitiesVector::new(), auth: Sensitive::new("".to_string()), + start_time: Utc::now(), } } @@ -77,6 +76,30 @@ impl Bot { .send() .unwrap(); } + + pub(crate) fn run_once(&mut self, mut prev_time: NaiveTime) { + self.start_time = Utc::now(); + + if self.start_time.time() - prev_time > chrono::Duration::seconds(6) { // Prod should use hours, add command line switch later and read duration from config + prev_time = self.start_time.time(); + self.config.load(); + self.community_ids.load(&self.auth, &self.config.instance); + } + + // Start the polling process + // Get all feed URLs (use cache) + let post_queue: Vec = self.config.check_feeds(&mut self.post_history, &self.community_ids, &self.auth); + + post_queue.iter().for_each(|post| { + println!("Posting: {}", post.name); + self.post(post.clone()); + }); + + while Utc::now().time() - self.start_time.time() < chrono::Duration::seconds(60) { + sleep(time::Duration::from_secs(10)); + } + + } } fn list_posts(auth: &Sensitive, base: String) -> GetPostsResponse { @@ -98,144 +121,20 @@ fn list_posts(auth: &Sensitive, base: String) -> GetPostsResponse { return serde_json::from_str(&res).unwrap(); } -struct CommunitiesVector { - ids: Vec<(CommunityId, String)>, -} - -impl CommunitiesVector { - fn new() -> CommunitiesVector { - CommunitiesVector{ids: vec![]} - } - - fn load(&mut self, auth: &Sensitive, base: &String) { - let params = ListCommunities { - auth: Some(auth.clone()), - ..Default::default() - }; - - let res = CLIENT - .get(base.clone() + "/api/v3/community/list") - .query(¶ms) - .send() - .unwrap() - .text() - .unwrap(); - - let site_data: ListCommunitiesResponse = serde_json::from_str(&res).unwrap(); - - let mut ids = [].to_vec(); - - site_data.communities.iter().for_each(|entry| { - let new_id = (entry.community.id, entry.community.name.clone()); - ids.push(new_id); - }); - - self.ids = ids; - } - - fn find(&self, name: &LemmyCommunities) -> CommunityId { - let mut ret_id = CommunityId(0); - - self.ids.iter().for_each(|id| { - let id_name = &id.1; - if &name.to_string() == id_name { - ret_id = id.0; - } - }); - return ret_id; - } -} - fn run_bot() { - // 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(); - this.community_ids.load(&this.auth, &this.config.instance); - - // 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(); - this.community_ids.load(&this.auth, &this.config.instance); - } - - // Start the polling process - // Get all feed URLs (use cache) - let mut post_queue: Vec = 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 = 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: this.community_ids.find(&feed.communities.chapter), - 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(37)), // TODO get this id once every few hours per API request, the ordering of IDs suggests that the EN Id might change in the future - 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!("Posting: {}", post.name); - this.post(post.clone()); - }); - - while Utc::now().time() - start.time() < chrono::Duration::seconds(60) { - sleep(time::Duration::from_secs(10)); - } + // 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(); + this.community_ids.load(&this.auth, &this.config.instance); + + // Create empty eTag list + println!("TODO: Etag list"); + + // Enter a loop (not for debugging) + loop { + this.run_once(old); } } @@ -304,6 +203,7 @@ fn main() -> Result<(), io::Error> { )?; terminal.show_cursor()?; + + run_bot(); Ok(()) - //run_bot(); } From 007570c2e3b8d662b7ab2b23206989806cd0533c Mon Sep 17 00:00:00 2001 From: Neshura Date: Tue, 27 Jun 2023 00:33:00 +0200 Subject: [PATCH 3/3] Expanded TUI: Lemmy Info + Post History --- src/config/mod.rs | 3 ++ src/main.rs | 96 ++++++++++++++++++++++++++++------------------- 2 files changed, 60 insertions(+), 39 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 6fb1519..841640e 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -144,6 +144,7 @@ impl Config { post_history[idx].last_post_url = item.url.clone(); } None => post_history.push(PrevPost { + id: feed.id, title: data.title, last_post_url: item.url.clone(), }), @@ -163,6 +164,7 @@ pub_struct!(RedditConfig { }); pub_struct!(FeedSetting { + id: usize, feed_url: String, communities: FeedCommunities, reddit: FeedRedditSettings, @@ -190,6 +192,7 @@ pub_struct!(FeedRedditSettings { // Posts structs pub_struct!(PrevPost { + id: usize, title: String, last_post_url: String, }); diff --git a/src/main.rs b/src/main.rs index 0b243d3..f8d0297 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,9 +10,9 @@ use lemmy_db_schema::{ }; use once_cell::sync::Lazy; use reqwest::{blocking::Client, StatusCode}; -use tui::{backend::{CrosstermBackend, Backend}, Terminal, widgets::{Block, Borders, Cell, Row, Table}, layout::{Layout, Constraint, Direction}, Frame, style::{Style, Modifier, Color}}; +use tui::{backend::{CrosstermBackend, Backend}, Terminal, widgets::{Block, Borders, Cell, Row, Table, Paragraph, Wrap}, layout::{Layout, Constraint, Direction, Alignment}, Frame, style::{Style, Modifier, Color}, text::{Span, Spans}}; use crossterm::{event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},}; -use std::{thread::{sleep, self}, time::{self, Duration}, io}; +use std::{thread::{sleep, self}, time::{self, Duration}, io, vec}; mod config; @@ -94,12 +94,13 @@ impl Bot { println!("Posting: {}", post.name); self.post(post.clone()); }); + } + pub(crate) fn idle(&self) { while Utc::now().time() - self.start_time.time() < chrono::Duration::seconds(60) { sleep(time::Duration::from_secs(10)); } - - } + } } fn list_posts(auth: &Sensitive, base: String) -> GetPostsResponse { @@ -121,55 +122,85 @@ fn list_posts(auth: &Sensitive, base: String) -> GetPostsResponse { return serde_json::from_str(&res).unwrap(); } -fn run_bot() { +fn run_bot(mut terminal: Terminal>) { // 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(); this.community_ids.load(&this.auth, &this.config.instance); - // Create empty eTag list - println!("TODO: Etag list"); - // Enter a loop (not for debugging) loop { this.run_once(old); + + // Update UI + terminal.draw(|f| { + ui(f, &this); + }).unwrap(); + + thread::sleep(Duration::from_secs(5)); + + disable_raw_mode().unwrap(); + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + ).unwrap(); + terminal.show_cursor().unwrap(); + + this.idle(); } } -fn ui(f: &mut Frame) { +fn ui(f: &mut Frame, state: &Bot) { let chunks = Layout::default() .direction(Direction::Vertical) .margin(1) .constraints( [ - Constraint::Percentage(10), + Constraint::Percentage(15), Constraint::Percentage(90), ].as_ref() ) .split(f.size()); + // Account Infos - let block = Block::default() - .title("Block") - .borders(Borders::ALL); + let account_info = vec![ + Row::new(vec![ + Cell::from(format!("Lemmy Username: {} |", state.secrets.lemmy.username)), + Cell::from(format!("Lemmy Instance: {} |", state.config.instance)) + ]) + ]; + + + let block = Table::new(account_info) + .block(Block::default().title("Account info").borders(Borders::ALL)) + .widths(&[ + Constraint::Percentage(15), + Constraint::Percentage(20), + Constraint::Percentage(20), + Constraint::Percentage(20), + Constraint::Percentage(20), + ]); f.render_widget(block, chunks[0]); + + // Post Table + let selected_style = Style::default().add_modifier(Modifier::REVERSED); let normal_style = Style::default().bg(Color::Blue); - let header_cells = ["Series", "Community", "Last Post"] + let header_cells = ["Series", "Community", "Reddit", "Discord", "Last Post"] .iter() .map(|h| Cell::from(*h).style(Style::default().fg(Color::Red))); let header = Row::new(header_cells) .style(normal_style) .height(1) .bottom_margin(1); - let rows = [ - Row::new([Cell::from("a1"), Cell::from("a2"), Cell::from("a3")]).height(1 as u16).bottom_margin(1), - Row::new([Cell::from("b1"), Cell::from("b2"), Cell::from("b3")]).height(1 as u16).bottom_margin(1), - Row::new([Cell::from("unicorn"), Cell::from("c2"), Cell::from("c3")]).height(2 as u16).bottom_margin(1) - ]; + let rows = state.post_history.iter().map(|post| { + let config = &state.config.feeds[post.id]; + Row::new([Cell::from(post.title.clone()), Cell::from(format!("{}", config.communities.chapter)), Cell::from(format!("{}", config.reddit.enabled as u8)), Cell::from("0"), Cell::from(post.last_post_url.clone())]).height(1 as u16).bottom_margin(1) + }); let t = Table::new(rows) .header(header) @@ -177,9 +208,11 @@ fn ui(f: &mut Frame) { .highlight_style(selected_style) .highlight_symbol(">> ") .widths(&[ + Constraint::Percentage(30), + Constraint::Percentage(10), + Constraint::Percentage(5), + Constraint::Percentage(5), Constraint::Percentage(50), - Constraint::Length(30), - Constraint::Min(10), ]); f.render_widget(t, chunks[1]); } @@ -187,23 +220,8 @@ fn ui(f: &mut Frame) { fn main() -> Result<(), io::Error> { let stdout = io::stdout(); let backend = CrosstermBackend::new(&stdout); - let mut terminal = Terminal::new(backend)?; + let terminal = Terminal::new(backend)?; - terminal.draw(|f| { - ui(f); - })?; - - thread::sleep(Duration::from_secs(5)); - - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; - - - run_bot(); + run_bot(terminal); Ok(()) }