first
This commit is contained in:
+165
@@ -0,0 +1,165 @@
|
||||
#![feature(lazy_cell)]
|
||||
mod writer;
|
||||
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::{HeaderMap, Request, StatusCode},
|
||||
middleware,
|
||||
middleware::Next,
|
||||
response::{Html, Redirect, Response, Result},
|
||||
routing::{get, post},
|
||||
Form, Router,
|
||||
};
|
||||
use axum_extra::extract::{
|
||||
cookie::{Cookie, Key},
|
||||
SignedCookieJar,
|
||||
};
|
||||
use minijinja::{context, Environment};
|
||||
use serde::Deserialize;
|
||||
use std::fs;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use std::{env, process::exit};
|
||||
use tokio::sync::Mutex;
|
||||
use writer::LinkFile;
|
||||
|
||||
static SECRET_KEY: LazyLock<Key> = LazyLock::new(Key::generate);
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() < 2 {
|
||||
println!("Need file argument.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let store = Arc::new(Mutex::new(LinkFile::new(&args[1])));
|
||||
|
||||
let authenticated = Router::new()
|
||||
.route("/rss.xml", get(get_rss))
|
||||
.route("/sendlink", post(send_link))
|
||||
.with_state(Arc::clone(&store))
|
||||
.layer(middleware::from_fn(authenticate));
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(get_index))
|
||||
.route("/login", get(get_login).post(post_login))
|
||||
.with_state(store)
|
||||
.merge(authenticated);
|
||||
|
||||
axum::Server::bind(&"0.0.0.0:8000".parse().unwrap())
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn get_index(
|
||||
State(_state): State<Arc<Mutex<LinkFile>>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Html<String>, Redirect> {
|
||||
let jar = SignedCookieJar::from_headers(&headers, LazyLock::force(&SECRET_KEY).clone());
|
||||
if let Some(x) = jar.get("auth") {
|
||||
if x.value() == "yeppers" {
|
||||
return Ok(Html(
|
||||
fs::read_to_string("templates/send_link.html").unwrap(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(Redirect::to("/login"))
|
||||
}
|
||||
|
||||
async fn get_login(State(_state): State<Arc<Mutex<LinkFile>>>) -> Result<Html<String>, Redirect> {
|
||||
Ok(Html(fs::read_to_string("templates/login.html").unwrap()))
|
||||
}
|
||||
|
||||
async fn get_rss(State(state): State<Arc<Mutex<LinkFile>>>) -> Result<String> {
|
||||
let template = fs::read_to_string("templates/rss.xml").unwrap();
|
||||
let linkfile = state.lock().await;
|
||||
let xmlfrag = fs::read_to_string(linkfile.get_path()).unwrap();
|
||||
|
||||
let link = match env::var("LINKY_LINK") {
|
||||
Ok(v) => v,
|
||||
Err(_) => "https://localhost:8000".to_string(),
|
||||
};
|
||||
|
||||
let mut env = Environment::new();
|
||||
env.add_template("rss_xml", &template).unwrap();
|
||||
let template = env.get_template("rss_xml").unwrap();
|
||||
|
||||
Ok(template
|
||||
.render(context! {
|
||||
xmlfrag => xmlfrag,
|
||||
link => link,
|
||||
})
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LinkData {
|
||||
pub linkdata: String,
|
||||
}
|
||||
|
||||
async fn send_link(
|
||||
State(state): State<Arc<Mutex<LinkFile>>>,
|
||||
Form(form): Form<LinkData>,
|
||||
) -> Result<Redirect> {
|
||||
let string = form.linkdata;
|
||||
let mut linkfile = state.lock().await;
|
||||
for str in string.split_whitespace() {
|
||||
linkfile.add_link(str).await.map_err(|e| {
|
||||
println!("ERROR: {:?}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
println!("Added: {}", str);
|
||||
}
|
||||
Ok(Redirect::to("/"))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LoginInfo {
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
async fn post_login(
|
||||
State(_state): State<Arc<Mutex<LinkFile>>>,
|
||||
headers: HeaderMap,
|
||||
Form(form): Form<LoginInfo>,
|
||||
) -> Result<(SignedCookieJar, Redirect)> {
|
||||
let mut jar = SignedCookieJar::from_headers(&headers, LazyLock::force(&SECRET_KEY).clone());
|
||||
|
||||
if let Some(x) = jar.get("auth") {
|
||||
if x.value() != "yeppers" {
|
||||
jar = jar.remove(x);
|
||||
} else {
|
||||
return Ok((jar, Redirect::to("/")));
|
||||
}
|
||||
}
|
||||
|
||||
let pswd = match env::var("LINKY_PASSWORD") {
|
||||
Ok(v) => v,
|
||||
Err(_) => "linkypassword".to_string(),
|
||||
};
|
||||
|
||||
if form.password == pswd {
|
||||
if let Some(x) = jar.get("auth") {
|
||||
jar = jar.remove(x);
|
||||
}
|
||||
jar = jar.add(Cookie::new("auth", "yeppers"));
|
||||
}
|
||||
|
||||
Ok((jar, Redirect::to("/")))
|
||||
}
|
||||
|
||||
async fn authenticate<B>(
|
||||
headers: HeaderMap,
|
||||
request: Request<B>,
|
||||
next: Next<B>,
|
||||
) -> Result<Response, Redirect> {
|
||||
let jar = SignedCookieJar::from_headers(&headers, LazyLock::force(&SECRET_KEY).clone());
|
||||
if let Some(x) = jar.get("auth") {
|
||||
if x.value() == "yeppers" {
|
||||
return Ok(next.run(request).await);
|
||||
}
|
||||
}
|
||||
|
||||
Err(Redirect::to("/login"))
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
use anyhow::{Context, Result};
|
||||
use minijinja::{context, Environment};
|
||||
use reqwest::Client;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const TEMPLATE_STRING: &str = "
|
||||
<item>
|
||||
<title> {{ title }} </title>
|
||||
<link> {{ link }} </link>
|
||||
</item>
|
||||
|
||||
";
|
||||
|
||||
pub struct LinkFile {
|
||||
file: File,
|
||||
client: Client,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
async fn get_link_title(client: &Client, link: &str) -> Result<String> {
|
||||
let res = timeout(
|
||||
Duration::from_secs(120),
|
||||
timeout(Duration::from_secs(120), client.get(link).send())
|
||||
.await??
|
||||
.text(),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let dom = tl::parse(&res, tl::ParserOptions::default())?;
|
||||
let elem = dom
|
||||
.query_selector("title")
|
||||
.and_then(|mut iter| iter.next())
|
||||
.context("No title")?;
|
||||
let node = elem.get(dom.parser()).context("No title")?;
|
||||
Ok(node.inner_text(dom.parser()).to_string())
|
||||
}
|
||||
|
||||
impl LinkFile {
|
||||
pub fn new(path: impl AsRef<Path> + Clone) -> Self {
|
||||
let file = OpenOptions::new().append(true).open(path.clone()).unwrap();
|
||||
let client = Client::new();
|
||||
Self {
|
||||
file,
|
||||
client,
|
||||
path: path.as_ref().to_path_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn add_link(&mut self, link: &str) -> Result<()> {
|
||||
let title = match get_link_title(&self.client, link).await {
|
||||
Ok(s) => s,
|
||||
Err(_) => link.to_string(),
|
||||
};
|
||||
|
||||
let mut env = Environment::new();
|
||||
env.add_template("rss_fragment", TEMPLATE_STRING)?;
|
||||
let template = env.get_template("rss_fragment")?;
|
||||
|
||||
self.file.write_all(
|
||||
template
|
||||
.render(context! {
|
||||
title => title,
|
||||
link => link,
|
||||
})?
|
||||
.as_bytes(),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_path(&self) -> PathBuf {
|
||||
self.path.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::writer::LinkFile;
|
||||
|
||||
#[test]
|
||||
fn test_linkfile() {
|
||||
let mut linkfile = LinkFile::new("./store.xmlfrag");
|
||||
linkfile.add_link("https://evilmuff.in").unwrap();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user