initial commit

This commit is contained in:
puffaboo 2022-06-26 18:51:08 +01:00
commit 6564c7b546
8 changed files with 2622 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
frc.toml
index.html

2197
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

27
Cargo.toml Normal file
View File

@ -0,0 +1,27 @@
[package]
name = "frc"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-std = { version = "1.9.0", features = ["unstable"] }
futures = "0.3.15"
rand = "0.8.4"
serde = "1.0.126"
serde_json = "1.0.64"
serde_derive = "1.0.130"
# tokio = { version = "0.2", features = ["full"] }
tokio = { version = "1.19.2", features = ["full"] }
telexide = "0.1.6"
uuid = { version = "0.8.2", features = ["v4"] }
anyhow = "1.0.41"
thiserror = "1.0.26"
url = "2.2.2"
thirtyfour = "0.29.3"
rouille = "3.5.0"
confy = "0.4.0"
typemap = "0.3.3"
sysinfo = "0.24.5"
signal-hook = "0.3.14"

125
src/botdriver.rs Normal file
View File

@ -0,0 +1,125 @@
use std::pin::Pin;
use futures::Future;
use telexide::{
api::types::SendMessage,
client::{self, ClientBuilder},
model::{self, MessageContent, MessageEntity, UpdateContent},
};
use typemap::Key;
use crate::browser::BrowserHandle;
pub struct BotDriver {
client: client::Client,
listen_user: String,
}
struct BotConfig {
handle: BrowserHandle,
listen_user: String,
}
struct BotConfigKey {}
impl Key for BotConfigKey {
type Value = BotConfig;
}
fn telegram_handler(
ctx: client::Context,
upd: model::Update,
) -> Pin<Box<dyn Future<Output = ()> + Send>> {
let config = if let Some(cfg) = ctx.data.read().get::<BotConfigKey>() {
BotConfig {
handle: cfg.handle.clone(),
listen_user: cfg.listen_user.clone(),
}
} else {
panic!("no config");
};
if let UpdateContent::Message(msg) = upd.content {
if let Some(user) = msg.from {
if let Some(name) = user.username {
if name.to_lowercase() == config.listen_user.to_lowercase() {
if let MessageContent::Text { content, entities } = msg.content {
if let Some(url_text) = get_url(content, entities) {
return Box::pin(async move {
match config.handle.go_to(url_text.as_str()).await {
Err(err) => {
println!("failed opening url [{}]: {}", url_text, err)
}
_ => {}
};
});
}
}
} else {
return Box::pin(async move {
match ctx
.api
.send_message(SendMessage::new(
msg.chat.get_id(),
"we don't listen to you",
))
.await
{
Err(err) => println!("failed writing not listening: {}", err),
_ => {}
};
});
}
}
}
}
Box::pin(async {})
}
impl BotDriver {
pub fn new(token: String, listen_user: String) -> Self {
Self {
client: ClientBuilder::new()
.set_token(&token)
.add_handler_func(telegram_handler)
.build(),
listen_user: listen_user,
}
}
pub async fn listen_and_paste(&mut self, handle: BrowserHandle) -> Result<(), anyhow::Error> {
let listen_to = self.listen_user.clone();
let config = BotConfig {
handle: handle,
listen_user: listen_to,
};
// Scope it so it instantly drops. Might instantly drop anyway cause I don't use the result. Ah well.
{
self.client.data.write().insert::<BotConfigKey>(config);
}
self.client.start().await?;
println!("telegram bot finished early");
Ok(())
}
}
fn get_url(text: String, entities: Vec<MessageEntity>) -> Option<String> {
match entities
.into_iter()
.filter(|ent| match ent {
MessageEntity::Url(_) => true,
_ => false,
})
.take(1)
.collect::<Vec<MessageEntity>>()
.first()
{
Some(def) => match def {
MessageEntity::Url(url_def) => Some(
text.chars()
.skip(url_def.offset as usize)
.take(url_def.length as usize)
.collect(),
),
_ => None,
},
_ => None,
}
}

61
src/browser.rs Normal file
View File

@ -0,0 +1,61 @@
use std::{thread, time::Duration};
use futures::TryFutureExt;
use thirtyfour::{
prelude::ElementQueryable, session::handle::SessionHandle, By, DesiredCapabilities, WebDriver,
};
pub struct BrowserSession {
driver: WebDriver,
}
#[derive(Clone)]
pub struct BrowserHandle {
handle: SessionHandle,
}
impl BrowserSession {
pub async fn new() -> Result<Self, anyhow::Error> {
let caps = DesiredCapabilities::chrome();
let driver = WebDriver::new("http://localhost:6444", caps).await?;
Ok(Self { driver: driver })
}
pub fn handle(&self) -> BrowserHandle {
BrowserHandle {
handle: self.driver.clone(),
}
}
pub async fn stop(self) -> Result<(), anyhow::Error> {
self.driver.quit().await?;
Ok(())
}
}
impl BrowserHandle {
pub async fn start(&self, start_url: &str) -> Result<(), anyhow::Error> {
// self.handle.fullscreen_window().await?;
self.go_to(format!("http://{}", start_url).as_str()).await?;
Ok(())
}
async fn decline_cookies(&self) -> Result<(), anyhow::Error> {
Ok(())
}
async fn youtube(&self) -> Result<(), anyhow::Error> {
self.decline_cookies().await?;
Ok(())
}
pub async fn go_to(&self, url: &str) -> Result<(), anyhow::Error> {
self.handle.get(url).await?;
if let Some(domain) = self.handle.current_url().await?.domain() {
if domain == "www.youtube.com" {
self.youtube().await?;
}
}
Ok(())
}
}

66
src/chromedriver.rs Normal file
View File

@ -0,0 +1,66 @@
use std::{
fmt,
io::{BufRead, BufReader},
process::{Child, Command, Stdio},
};
use sysinfo::{Pid, ProcessRefreshKind, RefreshKind, System, SystemExt};
#[derive(Debug, Clone)]
struct AddrInUse;
impl fmt::Display for AddrInUse {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "address is in use")
}
}
pub struct Driver {
child: Child,
}
impl Driver {
pub fn new() -> Result<Self, anyhow::Error> {
let mut child = Command::new("chromedriver")
.stdout(Stdio::piped())
.arg("--port=6444")
.spawn()?;
let child_id = child.id();
let mut child_out = BufReader::new(child.stdout.as_mut().unwrap());
let mut line = String::new();
let mut sys_info = System::new_with_specifics(
RefreshKind::new().with_processes(ProcessRefreshKind::new()),
);
// Wait for it to say Chromedriver started successfully
loop {
child_out.read_line(&mut line).unwrap();
if line.contains("ChromeDriver was started successfully.") {
break;
}
if line.contains("bind() failed: Address already in use") || line.contains("Exiting...")
{
return Err(anyhow::anyhow!(line));
}
// Check if chromedriver has exited
sys_info.refresh_all();
if let None = sys_info.process(Pid::from(child_id.clone() as i32)) {
let status_code = child.wait()?;
println!("{}", status_code.code().unwrap());
return Err(anyhow::anyhow!(
"chromedriver exited with status code {}",
status_code.code().unwrap_or(1337),
));
}
}
println!("chromedriver started");
Ok(Self { child })
}
pub fn exit(&mut self) -> Result<(), anyhow::Error> {
self.child.kill()?;
Ok(())
}
}

89
src/main.rs Normal file
View File

@ -0,0 +1,89 @@
mod botdriver;
mod browser;
mod chromedriver;
mod webserve;
use std::io::{self, Write};
use botdriver::BotDriver;
use browser::BrowserSession;
use chromedriver::Driver;
use serde::{Deserialize, Serialize};
use signal_hook::iterator::Signals;
const STATIC_ADDR: &str = "127.0.0.1:8010";
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let mut cfg: FRCConfig = confy::load_path(CFG_NAME)?;
if cfg.username == None || cfg.token == None {
cfg = create_config()?;
confy::store_path(CFG_NAME, cfg.clone())?;
}
let username = cfg.username.unwrap();
let static_handle = webserve::start_server(STATIC_ADDR.to_string()).await?;
println!("Starting frc with username to listen to: {}", username);
let mut chr = Driver::new()?;
let driver = BrowserSession::new().await?;
let browser = driver.handle();
let mut bot = BotDriver::new(cfg.token.unwrap(), username);
browser.start(STATIC_ADDR).await?;
println!("################");
println!("# starting frc #");
println!("################");
tokio::spawn(async move {
let mut signals =
Signals::new(&[signal_hook::consts::SIGINT, signal_hook::consts::SIGTERM])
.expect("could not init listen signals");
for _ in signals.forever() {
println!();
println!("closing down");
static_handle.abort();
driver.stop().await.expect("failed killing browser");
chr.exit().expect("failed killing chromedriver");
break;
}
std::process::exit(0);
});
bot.listen_and_paste(browser).await.unwrap();
Ok(())
}
const CFG_NAME: &str = "frc.toml";
#[derive(Serialize, Deserialize, Clone, Debug)]
struct FRCConfig {
username: Option<String>,
token: Option<String>,
}
impl ::std::default::Default for FRCConfig {
fn default() -> Self {
Self {
username: None,
token: None,
}
}
}
fn create_config() -> Result<FRCConfig, anyhow::Error> {
let username = get_config_from_user("Enter username to listen to:")?;
let token = get_config_from_user("Enter telegram bot token:")?;
Ok(FRCConfig {
username: Some(username),
token: Some(token),
})
}
fn get_config_from_user(prompt: &str) -> Result<String, anyhow::Error> {
print!("{} ", prompt);
io::stdout().flush()?;
let mut value = String::new();
io::stdin().read_line(&mut value)?;
Ok(value.trim().to_string())
}

54
src/webserve.rs Normal file
View File

@ -0,0 +1,54 @@
use futures::channel::oneshot;
use rouille::{router, Response};
use tokio::task::JoinHandle;
static HTML: &str = r#"<!DOCTYPE html>
<html>
<head>
<title>frc</title>
<style>
* {
color: white;
background-color: black;
}
.center-big {
font-size: 10vw;
text-align: center;
font-weight: bold;
}
.center {
text-align: center;
font-weight: bold;
}
</style>
</head>
<body>
<p class="center">Send youtube links to the telegram bot</p>
<p class="center-big">frc</p>
</body>
</html>
"#;
pub async fn start_server(addr: String) -> Result<JoinHandle<()>, anyhow::Error> {
let (sender, receiver) = oneshot::channel::<()>();
let handle = tokio::spawn(async move {
println!("starting server on {}", addr.clone());
let _ = sender.send(());
rouille::start_server(addr, move |req| {
router!(req,
(GET) (/) => {
Response::html(HTML)
},
_ => Response::empty_404()
)
});
});
let _ = receiver.await;
println!("frc server started");
Ok(handle)
}