2023-06-22 20:08:10 +00:00
|
|
|
use chrono::{Utc, DateTime, NaiveTime};
|
|
|
|
use config::{Config, PrevPost, Secrets, CommunitiesVector};
|
2023-06-18 22:26:50 +00:00
|
|
|
use lemmy_api_common::{
|
|
|
|
person::{Login, LoginResponse},
|
|
|
|
post::{CreatePost, GetPosts, GetPostsResponse},
|
2023-06-22 20:08:10 +00:00
|
|
|
sensitive::Sensitive,
|
2023-06-18 22:26:50 +00:00
|
|
|
};
|
|
|
|
use lemmy_db_schema::{
|
|
|
|
ListingType, SortType,
|
|
|
|
};
|
|
|
|
use once_cell::sync::Lazy;
|
|
|
|
use reqwest::{blocking::Client, StatusCode};
|
2023-06-26 22:33:00 +00:00
|
|
|
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}};
|
2023-06-21 19:29:14 +00:00
|
|
|
use crossterm::{event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},};
|
2023-06-26 22:33:00 +00:00
|
|
|
use std::{thread::{sleep, self}, time::{self, Duration}, io, vec};
|
2023-06-18 22:26:50 +00:00
|
|
|
|
|
|
|
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>,
|
2023-06-19 17:21:28 +00:00
|
|
|
community_ids: CommunitiesVector,
|
2023-06-18 22:26:50 +00:00
|
|
|
auth: Sensitive<String>,
|
2023-06-22 20:08:10 +00:00
|
|
|
start_time: DateTime<Utc>,
|
2023-06-18 22:26:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Bot {
|
|
|
|
pub(crate) fn new() -> Bot {
|
|
|
|
Bot {
|
2023-06-22 20:08:10 +00:00
|
|
|
secrets: Secrets::init(),
|
|
|
|
config: Config::init(),
|
2023-06-18 22:26:50 +00:00
|
|
|
post_history: PrevPost::load(),
|
2023-06-19 17:21:28 +00:00
|
|
|
community_ids: CommunitiesVector::new(),
|
2023-06-18 22:26:50 +00:00
|
|
|
auth: Sensitive::new("".to_string()),
|
2023-06-22 20:08:10 +00:00
|
|
|
start_time: Utc::now(),
|
2023-06-18 22:26:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2023-06-19 20:10:28 +00:00
|
|
|
.post(self.config.instance.clone() + "/api/v3/user/login")
|
2023-06-18 22:26:50 +00:00
|
|
|
.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
|
2023-06-19 20:10:28 +00:00
|
|
|
.post(self.config.instance.clone() + "/api/v3/post")
|
2023-06-18 22:26:50 +00:00
|
|
|
.json(&post_data)
|
|
|
|
.send()
|
|
|
|
.unwrap();
|
|
|
|
}
|
2023-06-22 20:08:10 +00:00
|
|
|
|
|
|
|
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<CreatePost> = 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());
|
|
|
|
});
|
2023-06-26 22:33:00 +00:00
|
|
|
}
|
2023-06-22 20:08:10 +00:00
|
|
|
|
2023-06-26 22:33:00 +00:00
|
|
|
pub(crate) fn idle(&self) {
|
2023-06-22 20:08:10 +00:00
|
|
|
while Utc::now().time() - self.start_time.time() < chrono::Duration::seconds(60) {
|
|
|
|
sleep(time::Duration::from_secs(10));
|
|
|
|
}
|
2023-06-26 22:33:00 +00:00
|
|
|
}
|
2023-06-18 22:26:50 +00:00
|
|
|
}
|
|
|
|
|
2023-06-19 20:10:28 +00:00
|
|
|
fn list_posts(auth: &Sensitive<String>, base: String) -> GetPostsResponse {
|
2023-06-18 22:26:50 +00:00
|
|
|
let params = GetPosts {
|
|
|
|
type_: Some(ListingType::Local),
|
|
|
|
sort: Some(SortType::New),
|
|
|
|
auth: Some(auth.clone()),
|
|
|
|
..Default::default()
|
|
|
|
};
|
|
|
|
|
|
|
|
let res = CLIENT
|
2023-06-19 20:10:28 +00:00
|
|
|
.get(base + "/api/v3/post/list")
|
2023-06-18 22:26:50 +00:00
|
|
|
.query(¶ms)
|
|
|
|
.send()
|
|
|
|
.unwrap()
|
|
|
|
.text()
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
return serde_json::from_str(&res).unwrap();
|
|
|
|
}
|
|
|
|
|
2023-06-26 22:33:00 +00:00
|
|
|
fn run_bot(mut terminal: Terminal<CrosstermBackend<&io::Stdout>>) {
|
2023-06-22 20:08:10 +00:00
|
|
|
// Get all needed auth tokens at the start
|
|
|
|
let mut old = Utc::now().time();
|
|
|
|
let mut this = Bot::new();
|
|
|
|
this.login();
|
|
|
|
this.community_ids.load(&this.auth, &this.config.instance);
|
|
|
|
|
|
|
|
// Enter a loop (not for debugging)
|
|
|
|
loop {
|
2023-07-03 17:22:02 +00:00
|
|
|
let _ = enable_raw_mode();
|
|
|
|
let _ = execute!(terminal.backend_mut(), EnterAlternateScreen, EnableMouseCapture);
|
2023-06-22 20:08:10 +00:00
|
|
|
this.run_once(old);
|
2023-06-26 22:33:00 +00:00
|
|
|
|
|
|
|
// 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();
|
2023-06-21 19:29:14 +00:00
|
|
|
}
|
2023-07-03 17:22:02 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
2023-06-21 19:29:14 +00:00
|
|
|
}
|
2023-06-18 22:26:50 +00:00
|
|
|
|
2023-06-26 22:33:00 +00:00
|
|
|
fn ui<B: Backend>(f: &mut Frame<B>, state: &Bot) {
|
2023-06-21 19:29:14 +00:00
|
|
|
let chunks = Layout::default()
|
|
|
|
.direction(Direction::Vertical)
|
|
|
|
.margin(1)
|
|
|
|
.constraints(
|
|
|
|
[
|
2023-06-26 22:33:00 +00:00
|
|
|
Constraint::Percentage(15),
|
2023-06-21 19:29:14 +00:00
|
|
|
Constraint::Percentage(90),
|
|
|
|
].as_ref()
|
|
|
|
)
|
|
|
|
.split(f.size());
|
|
|
|
|
2023-06-26 22:33:00 +00:00
|
|
|
// Account Infos
|
2023-06-18 22:26:50 +00:00
|
|
|
|
2023-06-26 22:33:00 +00:00
|
|
|
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),
|
|
|
|
]);
|
2023-06-21 19:29:14 +00:00
|
|
|
f.render_widget(block, chunks[0]);
|
2023-06-18 22:26:50 +00:00
|
|
|
|
2023-06-26 22:33:00 +00:00
|
|
|
|
|
|
|
// Post Table
|
|
|
|
|
2023-06-21 19:29:14 +00:00
|
|
|
let selected_style = Style::default().add_modifier(Modifier::REVERSED);
|
|
|
|
let normal_style = Style::default().bg(Color::Blue);
|
2023-06-26 22:33:00 +00:00
|
|
|
let header_cells = ["Series", "Community", "Reddit", "Discord", "Last Post"]
|
2023-06-21 19:29:14 +00:00
|
|
|
.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);
|
2023-06-26 22:33:00 +00:00
|
|
|
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)
|
|
|
|
});
|
2023-06-21 19:29:14 +00:00
|
|
|
|
|
|
|
let t = Table::new(rows)
|
|
|
|
.header(header)
|
|
|
|
.block(Block::default().borders(Borders::ALL).title("Table"))
|
|
|
|
.highlight_style(selected_style)
|
|
|
|
.highlight_symbol(">> ")
|
|
|
|
.widths(&[
|
2023-06-26 22:33:00 +00:00
|
|
|
Constraint::Percentage(30),
|
|
|
|
Constraint::Percentage(10),
|
|
|
|
Constraint::Percentage(5),
|
|
|
|
Constraint::Percentage(5),
|
2023-06-21 19:29:14 +00:00
|
|
|
Constraint::Percentage(50),
|
|
|
|
]);
|
|
|
|
f.render_widget(t, chunks[1]);
|
|
|
|
}
|
|
|
|
|
|
|
|
fn main() -> Result<(), io::Error> {
|
|
|
|
let stdout = io::stdout();
|
|
|
|
let backend = CrosstermBackend::new(&stdout);
|
2023-06-26 22:33:00 +00:00
|
|
|
let terminal = Terminal::new(backend)?;
|
2023-06-21 19:29:14 +00:00
|
|
|
|
2023-06-26 22:33:00 +00:00
|
|
|
run_bot(terminal);
|
2023-06-21 19:29:14 +00:00
|
|
|
Ok(())
|
2023-06-18 22:26:50 +00:00
|
|
|
}
|