2023-06-18 22:26:50 +00:00
use chrono ::Utc ;
2023-06-19 17:21:28 +00:00
use config ::{ Config , PrevPost , Secrets , LemmyCommunities } ;
2023-06-18 22:26:50 +00:00
use lemmy_api_common ::{
person ::{ Login , LoginResponse } ,
post ::{ CreatePost , GetPosts , GetPostsResponse } ,
2023-06-19 17:21:28 +00:00
sensitive ::Sensitive , community ::{ ListCommunities , ListCommunitiesResponse } ,
2023-06-18 22:26:50 +00:00
} ;
use lemmy_db_schema ::{
newtypes ::{ CommunityId , LanguageId } ,
ListingType , SortType ,
} ;
use once_cell ::sync ::Lazy ;
use reqwest ::{ blocking ::Client , StatusCode } ;
2023-06-21 19:29:14 +00:00
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 } ;
2023-06-18 22:26:50 +00:00
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 > ,
2023-06-19 17:21:28 +00:00
community_ids : CommunitiesVector ,
2023-06-18 22:26:50 +00:00
auth : Sensitive < String > ,
}
impl Bot {
pub ( crate ) fn new ( ) -> Bot {
Bot {
secrets : Secrets ::load ( ) ,
config : Config ::load ( ) ,
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 ( ) ) ,
}
}
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-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 ( & params )
. send ( )
. unwrap ( )
. text ( )
. unwrap ( ) ;
return serde_json ::from_str ( & res ) . unwrap ( ) ;
}
2023-06-19 17:21:28 +00:00
struct CommunitiesVector {
ids : Vec < ( CommunityId , String ) > ,
}
impl CommunitiesVector {
fn new ( ) -> CommunitiesVector {
CommunitiesVector { ids : vec ! [ ] }
}
2023-06-19 20:10:28 +00:00
fn load ( & mut self , auth : & Sensitive < String > , base : & String ) {
2023-06-19 17:21:28 +00:00
let params = ListCommunities {
auth : Some ( auth . clone ( ) ) ,
.. Default ::default ( )
} ;
let res = CLIENT
2023-06-19 20:10:28 +00:00
. get ( base . clone ( ) + " /api/v3/community/list " )
2023-06-19 17:21:28 +00:00
. query ( & params )
. 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 ;
}
}
2023-06-21 19:29:14 +00:00
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 < 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 : 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 ) ) ;
}
}
}
2023-06-18 22:26:50 +00:00
2023-06-21 19:29:14 +00:00
fn ui < B : Backend > ( f : & mut Frame < B > ) {
let chunks = Layout ::default ( )
. direction ( Direction ::Vertical )
. margin ( 1 )
. constraints (
[
Constraint ::Percentage ( 10 ) ,
Constraint ::Percentage ( 90 ) ,
] . as_ref ( )
)
. split ( f . size ( ) ) ;
2023-06-18 22:26:50 +00:00
2023-06-21 19:29:14 +00:00
let block = Block ::default ( )
. title ( " Block " )
. borders ( Borders ::ALL ) ;
f . render_widget ( block , chunks [ 0 ] ) ;
2023-06-18 22:26:50 +00:00
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 ) ;
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();
2023-06-18 22:26:50 +00:00
}