Compare commits

..

10 Commits

Author SHA1 Message Date
2571ce5756 Created Boardersv2 moved old to Boardersv1 2024-05-17 14:46:41 -04:00
saw
fa924285b0 v2.2.1 - Compatibility, logging
Added feature that checks for backwards compatibility issues and fixes
them if present. It checks for a version number in `data/app.json` and
compares it to the current version number.

The amount of logs has been severely reduced to prevent the sheer amount
of clutter being produced.
2021-08-20 09:11:20 -04:00
saw
093b6cd868 v2.2.0 - Pirate Stache - Markdown and Clensing
Message format has been changed so that all text boxes will be the same
width, rather than they change depending on name length.
TL;DR: changed location of name tag

You can now format your messages with cmark flavored markdown. There
still may be rendering bugs, but I will fix them as they come.

Messages are now properly sanitized with the ammonia package.

Messages now have proper timestamps with help from chrono. Right now
previous versions are not compatible with the new timestamp format. A
patch will be introduced in the next couple of updates to remedy that
issue.

Did some housekeeping in `static/index.css`
2021-08-06 01:01:22 -04:00
saw
b92d1e6236 v2.1.0 - Pirate Stache - HTML Rendering
Fixed some HTML rendering issues, the largest being text overflow with
the `<pre>` elements.

Centered text input box

Made text content background different

When opening a board it will now trim excess whitespace around strings
for storage efficiency and backward compatability.
2021-07-15 20:19:29 -04:00
saw
1a3db7f676 Fixed bugs
Fixed some page rendering errors and updated gitignore
2021-07-15 00:59:35 -04:00
saw
1535e037ca version number update
forgot to update the version number in the last commit

noting to see here...
2021-07-14 22:51:04 -04:00
saw
b093903fbf v2.0.1 - Pirate Stache - Unix Sockets
Added basic setup for Unix sockets.

Need to add shell REPL so a host can talk to the server. It currently
only connects and echoes back commands.
2021-07-14 22:46:06 -04:00
saw
2404b04e76 Borders v2.0.0 - Pirate Stache
Now using handlebars instead of format! for page generation. Added
quick and dirty logging with SimpleLogger. `Board` objects now have a
`last_active` field.

Bit of a version gap, but whatever. This is a significant enough update
I think it deserves a change in the major version.

0 Warnings, 0 Errors

Handlebars eliminated the XSS vulnerability because it automatically
escapes text.

Added comments to src/main.rs

Moved some static HTML blobs into static/ rather than baking them into
the binary.

Now licensed under Zlib

libBoarders is now compiled separately as a library. It may have VERY
little application, but whatever.

Made banner 3D, and thus cooler. Replaced "Boarders" with the banner
used in the backend.
2021-07-14 22:30:53 -04:00
saw
f10be383f9 Post error message, build number
Made the error when you try to post without being logged in more
informative. It used to be very vague and confusing.

Added build number to home page and backend banner. It pulls the version
number from cargo at compile time.
2021-06-04 13:03:16 -04:00
saw
38a42fdbe3 Removed browser dependant CSS, board description
I removed some browser dependant CSS that I mistakenly put in.
Everything now renders correctly on FF browsers.

I added the board description to the board view so you can see the board
topic while looking at the board.
2021-06-04 12:21:42 -04:00
44 changed files with 1227 additions and 876 deletions

9
.gitignore vendored
View File

@ -1,9 +0,0 @@
/target
# old data directory
/boards
# new data directory, specified in default config file
/data
Cargo.lock

View File

@ -1,16 +0,0 @@
[package]
name = "board_server"
version = "1.0.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = "3.3.2"
actix-files = "0.5.0"
toml = "0.5"
serde = "1.0.125"
serde_json = "1.0"
bcrypt-bsd = "0.1.3"
rand = "0.8.3"
colored = "2.0.0"

View File

@ -1,136 +0,0 @@
use actix_web::{
get, post, web,
HttpResponse, Responder,
cookie::Cookie
};
use crate::api::types::*;
use crate::lib::users::{User, Account};
#[post("/account/auth")]
pub async fn auth_html(
data: web::Data<AppState>,
form: web::Form<CryptoUserForm>
) -> HttpResponse {
let user = data.user_manager.lock().unwrap().get_user_by_uname(form.username.clone());
if let Some(u) = user {
match u.verify(form.password.clone()) {
Ok(a) => if a {
let mut response = HttpResponse::Ok()
.body(format!(r#"
<html><head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head><body>
<div id="title" class="bar">
<h1>Hello, {0}</h1>
</div><div id="index">
<p>
Logged in as {0}, return to <a href="/boards/">index</a>
</p>
</body></html></div></body></html>
"#, u.username));
response.add_cookie(
&Cookie::build("auth", format!("{}&{}", u.user_id, u.secret))
.domain("boards.solow.xyz")
//.domain("localhost")
.path("/")
.same_site(actix_web::cookie::SameSite::Strict)
//.secure(true)
.finish()
).unwrap();
response
} else {
HttpResponse::Forbidden().body("Forbidden: Could not authenticate user")
}
Err(e) => HttpResponse::BadRequest().body(format!("Bad Request: {}", e))
}
} else {
HttpResponse::BadRequest().body("Bad Request: User does not exist")
}
}
#[get("/account/login/")]
pub async fn login() -> impl Responder {
HttpResponse::Ok().body(r#"
<html><head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head><body>
<div id="title" class="bar">
<h1>Login</h1>
</div><div id="index">
<div class="auth">
<form action="/account/auth" method="post">
<div>
<label for="username">Username: </label>
<input type="text" id="username" name="username" required>
</div>
<div>
<label for="password">Password: </label>
<input type="password" id="password" name="password" required>
</div>
<input type="submit" value="Login">
</form>
</div>
</body></html></div></body></html>
"#)
}
#[post("/account/new")]
pub async fn sign_up_result(
data: web::Data<AppState>,
form: web::Form<CryptoUserForm>
) -> HttpResponse {
println!("new user");
let mut um = data.user_manager.lock().unwrap();
let user = {
if let Some(_a) = um.get_user_by_uname(form.username.clone()) {
return HttpResponse::Conflict().body("Conflict: user with that username already exists");
} else {
let new_id = um.assign_id();
User::new(new_id, &form.username).encrypt(form.password.clone())
}
};
um.add_user(user.clone());
HttpResponse::Created().body(format!(r#"
<html><head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head><body>
<div id="title" class="bar">
<h1>Hello, {0}</h1>
</div><div id="index">
<p>
Hooray! You can log into you new account <a href="/account/login/">here</a>
</p>
</body></html></div></body></html>
"#, user.username))
}
#[get("/account/new/")]
pub async fn sign_up() -> impl Responder {
HttpResponse::Ok().body(r#"
<html><head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head><body>
<div id="title" class="bar">
<h1>Sign Up</h1>
</div><div id="index">
<div class="auth">
<form action="/account/new" method="post">
<div>
<label for="username">Username: </label>
<input type="text" id="username" name="username" required>
</div>
<div>
<label for="password">Password: </label>
<input type="password" id="password" name="password" required>
</div>
<input type="submit" value="Sign Up">
</form>
</div>
</body></html></div></body></html>
"#)
}

View File

@ -1,154 +0,0 @@
body {
margin: unset !important;
padding: unset;
max-width: unset;
}
.bar {
padding: 0.5em;
background-color: #111111;
z-index: 1;
display: flex;
flex-direction: row;
}
.bar > div {
flex-grow: 1;
align-self: center;
}
.bar > div > h1 {
font-size: xxx-large;
margin: unset;
width: fit-content;
height: fit-content;
}
.bar > div > p {
width: fit-content;
height: fit-content;
margin-left: auto;
margin-right: 0;
margin-right: 2em;
}
.bar > h1 {
width: fit-content;
height: fit-content;
}
.bar > h1 > a {
font-size: 0.5em;
vertical-align: middle;
}
#title {
width: 100%;
height: 3em;
position: inherit;
top: 0;
left: 0;
margin-bottom: 1rem;
padding-left: 1em;
}
#title > h1 {
font-size: 2.5em !important;
}
#nav {
width: 3em;
position: fixed;
right: 0;
top: 0;
margin-left: 3rem;
height: 100%;
}
.arrow {
width: 2.5rem;
background-color: #07a6ea;
color: white;
padding: 0.25rem;
border-radius: 0.5em;
}
.orange-arrow {
width: 2.5rem;
color: orange;
padding: 0.25rem;
border-radius: 0.5em;
}
.down {
transform: rotate(180deg);
-webkit-transform:rotate(180deg);
-moz-transform: rotate(180deg);
-ms-transform: rotate(180deg);
-o-transform: rotate(180deg);
}
#navbar > img.arrow.down {
unset: top;
bottom: 0;
margin-bottom: 0.5em;
}
#navbar > img.arrow {
position: relative;
}
.message {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.message.right {
}
.message.left {
}
.vertical-center {
margin: 0;
position: absolute;
top: 50%;
-ms-transform: translateY(-50%);
transform: translateY(-50%);
}
table {
font-size: 1em;
}
table, th, td {
border-collapse: collapse;
}
th {
text-align: left;
margin-bottom: 1em;
}
tr.a {
background-color: #3A3A3A;
}
tr.b {
background-color: inherit;
}
.auth {
margin: auto;
width: 20em;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
border: 2px solid white;
border-radius: 5px;
background-color: #3A3A3A;
}
#index {
padding: 3em;
padding-top: 0;
}

View File

@ -1,2 +0,0 @@
pub mod accounts;
pub mod boards;

View File

@ -1,241 +0,0 @@
use std::{
env, fs,
sync::Mutex,
fs::{
ReadDir
},
path::Path,
};
use actix_web::{
get, web,
App, HttpResponse, HttpServer, Responder,
};
mod lib;
use lib::{
*,
Archiveable,
boards::{
Board,
BoardFactory
},
users::UserManager,
messages::MessageFactory
};
mod api;
use api::types::AppState;
mod html;
use html::{
accounts::*,
boards::*
};
/*
struct PageBuilder {
body: &'static str,
style: Option<String>,
table_labels: Option<String>
}
impl PageBuilder {
fn new(text: &'static str) -> PageBuilder {
PageBuilder {
body: text,
style: None,
table_labels: None
}
}
fn build<S>(self, style: S, table_labels: S) -> String where S: AsRef<str> {
String::from(self.body)
.replacen("{}", style.as_ref(), 1)
.replacen("{}", table_labels.as_ref(), 1)
}
}
*/
#[get("/")]
async fn index() -> impl Responder {
HttpResponse::Ok().body(r#"
<html><head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
</head>
<body>
<h1>Boarders v1.0.0</h1>
<p>
<h2>What is it?</h2>
Boarders is an easily deployable message board written in Rust.
It is extremely efficient on both the frontend and backend.
On the frontent all pages are static and (at this time) have no JS, only CSS/HTML.
The backend uses the 5th fastest web framework, Actix, according to <a href="https://www.techempower.com/benchmarks/">techempower.com</a>.
Which ensures that response times will always be snappy.
</p>
<p>
<hr>
<h2>Rules</h2>
<ol>
<li>No NSFW<br>
At least until NSFW channels are implemented</li>
<li>Don't spam boards or user accounts<br>
It's just anoying, and if it gets too bad I may have to hard reset everything<br>
Don't ruin the fun for everyone</li>
<li>Have Fun!<br>Pretty much anything goes, but if people don't like you, that's your fault</li>
</ol>
</p>
<h3 style="text-align: center;">------<a href="/boards/">[Get Started]</a>------</h3>
</body>
</html>
"#)
}
#[get("/debug/")]
async fn debug(
data: web::Data<AppState>
) -> impl Responder {
HttpResponse::Ok().body(serde_json::to_string_pretty(data.as_ref()).unwrap())
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
println!("Initializing...");
let data_dir = Path::new("data/");
let mut pwd = env::current_dir().unwrap();
let board_files: ReadDir = match fs::read_dir({
pwd.push(data_dir);
pwd.push("boards/");
&pwd
}) {
Ok(a) => a,
Err(_e) => {
fs::create_dir_all(&pwd).unwrap();
fs::read_dir(pwd).unwrap()
}
};
let mut boards: Vec<Board> = Vec::new();
for file in board_files.map(|x| x.unwrap()) {
let mut path = file.path().to_path_buf();
//let meta = fs::metadata(&path).unwrap();
let ftype = file.file_type().unwrap();
if ftype.is_dir() {
path.push("board.json");
println!("loading board: {:?}", path);
boards.push(
Board::open(&path).unwrap_or_else(|e| {
panic!("Could not open board {:?}\n{}", path, e)
})
);
}
}
boards.sort_by(|a,b| a.id.cmp(&b.id));
let mut board_data = std::path::PathBuf::new();
board_data.push(&data_dir);
let ff = FactoryFactory::new(data_dir);
let (mut bf, mf, um) = ff.build();
board_data.push("boards/");
bf.set_data_dir(board_data);
let web_data = web::Data::new(AppState {
boards: Mutex::new(boards.clone()),
board_factory: Mutex::new(BoardFactory::load_state({
println!("Loading board factory file...");
let mut dd = data_dir.to_path_buf();
dd.push("boards/factory.json");
dd
}).unwrap_or(bf)),
msg_factory: Mutex::new(MessageFactory::load_state({
println!("Loading message factory file...");
let mut dd = data_dir.to_path_buf();
dd.push("message_factory.json");
dd
}).unwrap_or(mf)),
user_manager: Mutex::new(UserManager::load_state({
println!("Loading user manager file...");
let mut dd = data_dir.to_path_buf();
dd.push("users.json");
dd
}).unwrap_or(um))
});
let wb = web_data.clone();
println!("Finished initialization");
println!("Starting server...");
let server = HttpServer::new(move || {
App::new()
.app_data(web_data.clone())
.service(index)
.service(debug)
.service(list_boards)
.service(login)
.service(auth_html)
.service(sign_up)
.service(sign_up_result)
.service(new_board)
.service(new_board_result)
.service(get_board)
.service(board_post)
.service(
web::scope("/api")
.service(api::boards::new)
.service(api::boards::list)
.service(api::messages::new)
.service(api::users::new)
.service(api::users::list)
.service(api::users::get)
.service(api::users::auth))
.service(actix_files::Files::new("/static/", "./static/").show_files_listing())
}).bind("127.0.0.1:8080")?
.run();
println!("Started server");
println!("{}", include_str!("res/banner"));
let res = server.await;
println!("\nExiting...");
/*
* Note to self: handle all errors as much as possible
* Use unwrap() as little as possible
* If anything goes wrong while saving data data WILL get lost
*/
println!("Saving application state...");
for b in wb.boards.lock().unwrap().iter() {
fs::create_dir_all(&b.path).unwrap_or_else(
|x| { println!("Could not create directory {:?}: {}", b.path, x) }
);
b.save_state("board.json").unwrap_or_else(
|x| { println!("Could not save board {:?}: {}", b.title, x) }
);
}
wb.board_factory
.lock()
.unwrap()
.save_state("factory.json")
.unwrap_or_else(
|x| { println!("Could not save user factory state: {}", x) }
);
wb.msg_factory
.lock()
.unwrap()
.save_state("message_factory.json")
.unwrap_or_else(
|x| { println!("Could not save message factory state: {}", x) }
);
wb.user_manager
.lock()
.unwrap()
.save_state("users.json")
.unwrap_or_else(
|x| { println!("Could not save user data: {}", x) }
);
println!("Done!");
res
}

View File

@ -1,5 +0,0 @@
__ __
/ /_ ____ ____ _ _____ ____/ / _____ _____
/ __ \ / __ \ / __ `// ___// __ / / ___// ___/
/ /_/ // /_/ // /_/ // / / /_/ /_ / / (__ )
/_.___/ \____/ \__,_//_/ \__,_/(_)/_/ /____/

View File

@ -1,154 +0,0 @@
body {
margin: unset !important;
padding: unset;
max-width: unset;
}
.bar {
padding: 0.5em;
background-color: #111111;
z-index: 1;
display: flex;
flex-direction: row;
}
.bar > div {
flex-grow: 1;
align-self: center;
}
.bar > div > h1 {
font-size: xxx-large;
margin: unset;
width: fit-content;
height: fit-content;
}
.bar > div > p {
width: fit-content;
height: fit-content;
margin-left: auto;
margin-right: 0;
margin-right: 2em;
}
.bar > h1 {
width: fit-content;
height: fit-content;
}
.bar > h1 > a {
font-size: 0.5em;
vertical-align: middle;
}
#title {
width: 100%;
height: 3em;
position: inherit;
top: 0;
left: 0;
margin-bottom: 1rem;
padding-left: 1em;
}
#title > h1 {
font-size: 2.5em !important;
}
#nav {
width: 3em;
position: fixed;
right: 0;
top: 0;
margin-left: 3rem;
height: 100%;
}
.arrow {
width: 2.5rem;
background-color: #07a6ea;
color: white;
padding: 0.25rem;
border-radius: 0.5em;
}
.orange-arrow {
width: 2.5rem;
color: orange;
padding: 0.25rem;
border-radius: 0.5em;
}
.down {
transform: rotate(180deg);
-webkit-transform:rotate(180deg);
-moz-transform: rotate(180deg);
-ms-transform: rotate(180deg);
-o-transform: rotate(180deg);
}
#navbar > img.arrow.down {
unset: top;
bottom: 0;
margin-bottom: 0.5em;
}
#navbar > img.arrow {
position: relative;
}
.message {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.message.right {
}
.message.left {
}
.vertical-center {
margin: 0;
position: absolute;
top: 50%;
-ms-transform: translateY(-50%);
transform: translateY(-50%);
}
table {
font-size: 1em;
}
table, th, td {
border-collapse: collapse;
}
th {
text-align: left;
margin-bottom: 1em;
}
tr.a {
background-color: #3A3A3A;
}
tr.b {
background-color: inherit;
}
.auth {
margin: auto;
width: 20em;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
border: 2px solid white;
border-radius: 5px;
background-color: #3A3A3A;
}
#index {
padding: 3em;
padding-top: 0;
}

View File

@ -1,17 +0,0 @@
<div id="nav" class="bar">
<img class="arrow up" src="https://static.solow.xyz/boards/arrow.svg">
<img class="arrow down" style="bottom: 0; position: absolute; right: 0; margin-right: 9; margin-bottom: 28;" src="https://static.solow.xyz/boards/arrow.svg">
<div class="vertical-center">
<!--<img class="orange-arrow right" src="https://static.solow.xyz/boards/arrow.svg">-->
<a href="../">
Next<br>
Page
</a>
<hr>
<a href="../">
Prev<br>
Page
</a>
<!--<img class="orange-arrow left" src="https://static.solow.xyz/boards/arrow.svg">-->
</div>
</div>

6
v1/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
target
data
Cargo.lock

37
v1/Cargo.toml Normal file
View File

@ -0,0 +1,37 @@
[package]
name = "board_server"
version = "2.2.1"
edition = "2018"
license = "Zlib"
repository = "https://git.solow.xyz/cgit.cgi/Boarders/"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = "3.3.2"
actix-files = "0.5.0"
toml = "0.5"
serde = "1.0.125"
serde_json = "1.0"
bcrypt-bsd = "0.1.3"
rand = "0.8.3"
console = "0.14.1"
colored = "2.0.0"
tokio = {version = "1.7.1", features=["net", "rt-multi-thread", "sync", "macros"]}
futures = "0.3.15"
handlebars = "4.0.1"
maplit = "1.0.2"
log = "0.4.14"
simple_logger = "1.11.0"
chrono = "0.4.19"
ammonia = "3.1.2"
pulldown-cmark = "0.8.0"
[lib]
name = "boarders"
path = "src/lib/mod.rs"
test = false
bench = false
doc = false
harness = false
edition = "2018"

8
v1/compat/2.2.0.sh Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
for board in ./data/boards/*/ ; do
FILE="$board/board.json"
sed -E 's/"timestamp"\:([0-9]+)/"timestamp":{"secs_since_epoch":\1,"nanos_since_epoch":0}/g' $FILE > "${FILE}.new"
mv "${FILE}.new" $FILE
done

View File

@ -61,7 +61,7 @@ pub async fn new(
}
}, form.content.clone());
println!("new message");
a.add_message(msg.clone());
a.clone().add_message(msg.clone());
Ok(HttpResponse::Created().body(serde_json::to_string(&msg).unwrap()))
},
None => Err(HttpResponse::BadRequest().body("Bad Request: Board does not exist"))

View File

@ -0,0 +1,3 @@
fn main() {
println!("Client");
}

78
v1/src/html/accounts.rs Normal file
View File

@ -0,0 +1,78 @@
use actix_web::{
get, post, web,
HttpResponse,
cookie::Cookie
};
use actix_files::NamedFile;
use crate::api::types::*;
use crate::lib::users::{User, Account};
use crate::ThreadData;
use handlebars::to_json;
use maplit::btreemap;
#[post("/account/auth")]
pub async fn auth_html(
tdata: web::Data<crate::ThreadData<'_>>,
data: web::Data<AppState>,
form: web::Form<CryptoUserForm>
) -> HttpResponse {
let user = data.user_manager.lock().unwrap().get_user_by_uname(form.username.clone());
if let Some(u) = user {
match u.verify(form.password.clone()) {
Ok(a) => if a {
let mut response = HttpResponse::Ok()
.body(tdata.handlebars.render("auth", &btreemap!("user" => u.clone())).unwrap());
response.add_cookie(
&Cookie::build("auth", format!("{}&{}", u.user_id, u.secret))
.domain("localhost")
//.domain("localhost")
.path("/")
.same_site(actix_web::cookie::SameSite::Strict)
//.secure(true)
.finish()
).unwrap();
response
} else {
HttpResponse::Forbidden().body("Forbidden: Could not authenticate user")
}
Err(e) => HttpResponse::BadRequest().body(format!("Bad Request: {}", e))
}
} else {
HttpResponse::BadRequest().body("Bad Request: User does not exist")
}
}
#[get("/account/login/")]
pub async fn login() -> actix_web::Result<NamedFile> {
Ok(NamedFile::open("static/account_login.html")?)
}
#[post("/account/new")]
pub async fn sign_up_result(
data: web::Data<AppState>,
form: web::Form<CryptoUserForm>,
tdata: web::Data<ThreadData<'_>>
) -> HttpResponse {
println!("new user");
let mut um = data.user_manager.lock().unwrap();
let user = {
if let Some(_a) = um.get_user_by_uname(form.username.clone()) {
return HttpResponse::Conflict().body("Conflict: user with that username already exists");
} else {
let new_id = um.assign_id();
User::new(new_id, &form.username).encrypt(form.password.clone())
}
};
um.add_user(user.clone());
HttpResponse::Created().body(tdata.handlebars.render("new_user_redirect", &btreemap! {
"user" => to_json(user.username)
}).unwrap())
}
#[get("/account/new/")]
pub async fn sign_up() -> actix_web::Result<NamedFile> {
Ok(NamedFile::open("static/new_account.html")?)
}

View File

@ -1,7 +1,6 @@
use std::{
time,
time::Duration,
path::PathBuf
path::PathBuf,
time::SystemTime
};
use actix_web::{
@ -14,59 +13,61 @@ use crate::{
boards::Board,
users::{UserCookie, User}
},
api::types::*
api::types::*,
ThreadData
};
use handlebars::to_json;
use maplit::btreemap;
use super::TimeDerive;
use chrono::{DateTime, Utc};
use ammonia::clean;
use pulldown_cmark::{Parser, Options, html::push_html};
#[get("/boards/")]
pub async fn list_boards(
req: HttpRequest,
data: web::Data<AppState>,
tdata: web::Data<ThreadData<'_>>
) -> impl Responder {
let now = time::SystemTime::now().duration_since(time::UNIX_EPOCH).unwrap();
let mut ab = false;
let cookie = req.cookie("auth");
let user = if let Some(c) = cookie {
if let Ok(v) = UserCookie::parse(c.value()) {
data.user_manager.lock().unwrap().get_user_by_id(v.id)
} else {
None
}
} else {
None
};
let boards = (*data.boards.lock().unwrap()).clone();
HttpResponse::Ok().body(format!(
r#"
<html><head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head><body>
<div id="title" class="bar">
<div>
<h1>Board Index</h1>
</div>
<div>
<p><a href="/account/login/">Login</a>/<a href="/account/new/">Sign Up</a></p>
</div>
</div><div id="index">
<a href="/boards/new/" style="margin-bottom: 0.5em;">New Board [+]</a>
<table style="width: 100%;" cellpadding=8px>
<tr><th>Name</th><th>Description</th><th>Last Active</th></tr>
{}
</table></body></html></div></body></html>
"#,
boards.iter().map(|b| {
ab = !ab;
format!(
"<tr class=\"{}\"><td><a href=\"/board/{}/\">{}</a></td><td>{}</td><td>{}</td></tr>",
if ab { "a" } else { "b" }, b.id, b.title, b.desc,
if let Some(a) = b.messages.borrow().last() {
format!("{:.2} minutes", now.checked_sub(Duration::from_secs(a.timestamp)).unwrap().as_secs()/60)
} else {
String::from("Never")
}
)
}).collect::<String>()
))
let mut boards_json = to_json(&boards);
let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap();
let boards_json = boards_json.as_array_mut().unwrap().iter_mut().zip(boards).map(|x| -> &serde_json::Value {
x.0.as_object_mut().unwrap().insert(
"time_meta".to_string(),
if let Some(t) = x.1.last_active {
to_json(std::time::Duration::from_secs(t).derive_time_since(now).map(|d| (format!("{:.2}", d.0), d.1)))
} else {
to_json::<Option<u8>>(None)
}
);
//println!("{:?}", x.0);
x.0
}).collect::<Vec<&serde_json::Value>>();
HttpResponse::Ok().body(tdata.handlebars.render("board_index", &btreemap!(
"boards" => to_json(boards_json),
"user" => to_json(user)
)).unwrap())
}
#[get("/board/{board_id}/")]
pub async fn get_board(
req: HttpRequest,
tdata: web::Data<ThreadData<'_>>,
data: web::Data<AppState>,
web::Path(board_id): web::Path<u32>
) -> HttpResponse {
let mut ab = false;
let now = time::SystemTime::now().duration_since(time::UNIX_EPOCH).unwrap();
let boards = data.boards.lock().unwrap();
let cookie = req.cookie("auth");
let uid = if cookie.is_some() {
@ -75,49 +76,38 @@ pub async fn get_board(
UserCookie::parse("null")
};
if let Some(a) = Board::search(boards.iter(), board_id) {
HttpResponse::Ok().body(format!(
r#"
<html><head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head>
<body>
<div id="title" class="bar"><h1>
{} | <a href="/boards/"><- Back to index</a>
</h1></div>
<div id="index">
<table style="width: 100%;" cellpadding=8px>
{}
</table>
<form action="/boards/post" method="post">
<label for="content">Content:</label><br/>
<input type="hidden" id="board_id" name="board_id" value="{}">
<input type="hidden" id="user_id" name="user_id" value="{}">
<textarea name="content" id="content" rows=12 cols=50 placeholder="Enter message here..."></textarea>
<br/><input type="submit" value="Post Message" style="margin: 0.5em;">
</form>
</div></body></html>
"#,
a.title,
a.messages.borrow().iter().map(|x| {
ab = !ab;
format!(
"<tr class=\"message {}\"><td>{}</td><td>{}</td><td>{}</td></tr>",
if ab { "a" } else { "b" },
{
let author = x.author.clone().into_inner();
author.nick.clone().or_else(|| Some(author.username.clone())).unwrap()
},
x.text.replace('\n', "<br>"),
format!("{:.2} min.", now.checked_sub(Duration::from_secs(x.timestamp)).unwrap().as_secs()/60)//x.timestamp
)
}).collect::<String>(),
board_id,
match uid {
Ok(a) => a.id.to_string(),
Err(_e) => "null".to_string()
}
))
let data = btreemap!(
"messages" => {
let mut msg_json = to_json(&a.messages);
let mut op = Options::empty();
op.insert(Options::ENABLE_TABLES);
let msg_json = msg_json.as_array_mut().unwrap().iter_mut().zip(a.messages.borrow().iter()).map(|x| {
let parse = Parser::new_ext(&x.1.text, op);
let o = x.0.as_object_mut().unwrap();
o.insert(
"time_meta".to_string(),
to_json(DateTime::<Utc>::from(x.1.timestamp).to_rfc2822().trim_matches(|c| c=='0' || c=='+' || c==' '))
);
o.insert(
"render_text".to_string(),
{
let mut dirty_html = String::new();
push_html(&mut dirty_html, parse);
to_json(dirty_html)
}
);
x.0
}).collect::<Vec<&mut serde_json::Value>>();
to_json(msg_json)
},
"user" => match uid {
Ok(a) => to_json(data.user_manager.lock().unwrap().get_user_by_id(a.id)),
Err(_) => to_json(None::<Option<u8>>)
},
"board" => to_json(a)
);
HttpResponse::Ok().body(tdata.handlebars.render("board", &data).unwrap())
} else {
HttpResponse::NotFound().body("That board could not be found")
}
@ -137,15 +127,15 @@ pub async fn board_post(
Ok(a) => a,
Err(e) => {
println!("{}", e);
return Err(HttpResponse::BadRequest().body("Bad Request: Could not parse cookie data"));
return Err(HttpResponse::BadRequest().body("Bad Request: Could not parse cookie data.\n Not logged in, or bad cookie."));
}
};
let mut mf = data.msg_factory.lock().unwrap();
let b = data.boards.lock().unwrap();
let board = b.iter().find(|x| (x.id == form.board_id));
let mut b = data.boards.lock().unwrap();
let mut board = b.iter_mut().find(|x| (x.id == form.board_id));
let um = data.user_manager.lock().unwrap();
match board {
Some(a) => {
Some(ref mut a) => {
let msg = mf.create_message({
let user = um.get_user_by_id(form.user_id).ok_or_else(|| {
HttpResponse::NotFound().body("Not Found: User does not exist")
@ -155,8 +145,7 @@ pub async fn board_post(
} else {
return Err(HttpResponse::Unauthorized().body("Unauthorized: Bad login attempt, invalid auth token"));
}
}, form.content.clone());
println!("new message");
}, clean(&form.content));
a.add_message(msg);
Ok(HttpResponse::SeeOther().header("location", format!("/board/{}/", a.id)).finish())
},
@ -189,29 +178,7 @@ pub async fn new_board(
return HttpResponse::BadRequest().body("Bad Request: User does not exist");
}
HttpResponse::Ok().body(r#"
<html><head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head><body>
<div id="title" class="bar">
<h1>New Board</h1>
</div><div id="index">
<div class="auth">
<form action="/boards/new" method="post">
<div>
<label for="name">Board Title: </label><br>
<input type="text" id="name" name="name" required>
</div>
<div>
<label for="description">Board Description: </label>
<input type="text" id="description" name="description" required>
</div>
<input type="submit" value="Create Board">
</form>
</div>
</body></html></div></body></html>
"#)
HttpResponse::Ok().body(include_str!("../../static/new_board.html"))
}
#[post("/boards/new")]

22
v1/src/html/mod.rs Normal file
View File

@ -0,0 +1,22 @@
pub mod accounts;
pub mod boards;
use std::time::Duration;
trait TimeDerive {
fn derive_time_since(&self, time: Duration) -> Option<(f64, String)>;
}
impl TimeDerive for Duration {
fn derive_time_since(&self, time: Duration) -> Option<(f64, String)> {
time.checked_sub(*self).map(|a| {
match a.as_secs() {
t if t > 604800 => (t as f64/60.0/60.0/24.0/7.0, "weeks".to_string()),
t if t > 86400 => (t as f64/60.0/60.0/24.0, "days".to_string()),
t if t > 3600 => (t as f64/60.0/60.0, "hours".to_string()),
t if t > 120 => (t as f64/60.0, "mins".to_string()),
t => (t as f64, "secs".to_string())
}
})
}
}

View File

@ -13,8 +13,6 @@ use super::{
messages::Message
};
//use colored::*;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Board {
pub title: String,
@ -23,15 +21,22 @@ pub struct Board {
pub creation: u64,
pub id: u32,
pub path: String,
pub owner: u32
pub owner: u32,
pub last_active: Option<u64>
}
impl Board {
pub fn open<P>(path: P) -> Result<Board> where P: AsRef<Path> {
match File::open(&path) {
Ok(a) => Ok(serde_json::from_reader(a).unwrap()),
Err(e) => Err(e)
let mut b: std::result::Result<Board, _> = File::open(&path).map(
|a| serde_json::from_reader(a).unwrap()
);
/* backwards compatability */
if let Ok(ref mut b) = b {
if b.last_active.is_none() {
b.last_active = b.messages.borrow().last().map(|x| x.as_secs());
}
}
b
}
pub fn search<'a, I>(
@ -44,8 +49,10 @@ impl Board {
iter.find(|x| x.id == id)
}
pub fn add_message(&self, msg: Message) {
pub fn add_message(&mut self, msg: Message) -> &Board {
self.last_active = Some(msg.as_secs());
self.messages.borrow_mut().push(msg);
self
}
}
@ -89,21 +96,10 @@ impl BoardFactory {
creation: SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(),
id: self.assign_id(),
path: path.to_str().unwrap().to_string(),
owner: creator
owner: creator,
last_active: None
};
Ok(b)
/*
println!("board path: {:?}", path);
fs::create_dir_all(&path).unwrap();
path.push("board.json");
Board::open(&path).or_else(|_e| -> Result<Board> {
if !path.exists() {
let mut file = File::create(&path).unwrap();
file.write_all(serde_json::to_string(&b).unwrap().as_bytes())?;
}
Board::open(path.parent().unwrap())
})
*/
}
}

View File

@ -14,10 +14,16 @@ use super::{
pub struct Message {
pub text: String,
pub id: u32,
pub timestamp: u64,
pub timestamp: SystemTime,
pub author: RefCell<User>
}
impl Message {
pub fn as_secs(&self) -> u64 {
self.timestamp.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct MessageFactory {
pub next_id: u32,
@ -36,11 +42,10 @@ impl MessageFactory {
pub fn create_message<S>(&mut self, user: User, content: S) -> Message where S: AsRef<str> {
self.next_id += 1;
let time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
Message {
text: String::from(content.as_ref()),
id: self.next_id-1,
timestamp: time,
timestamp: SystemTime::now(),
author: RefCell::new(user)
}
}

View File

@ -58,11 +58,7 @@ impl FactoryFactory {
pub fn build(&self) -> (BoardFactory, MessageFactory, UserManager) {
(BoardFactory::new(self.data_dir.clone()),
MessageFactory {
next_id: 0,
last_write: None,
data_dir: self.data_dir.clone()
},
MessageFactory::new(self.data_dir.clone()),
UserManager::new(self.data_dir.clone()))
}
}

480
v1/src/main.rs Normal file
View File

@ -0,0 +1,480 @@
use std::{
env,
error::Error,
fs,
fs::ReadDir,
io,
io::{Read, Write},
path::Path,
process::Command,
sync::Mutex,
};
use actix_web::{get, web, App, HttpResponse, HttpServer, Responder};
mod lib;
use lib::{
boards::{Board, BoardFactory},
messages::MessageFactory,
users::UserManager,
Archiveable, *,
};
mod api;
use api::types::AppState;
mod html;
use html::{accounts::*, boards::*};
use tokio::{
net::{UnixListener, UnixStream},
runtime::Runtime,
select,
sync::{oneshot, watch},
};
use console::style;
use handlebars::{handlebars_helper, Handlebars};
use maplit::btreemap;
use log::LevelFilter;
use simple_logger::SimpleLogger;
use serde::{Deserialize, Serialize};
pub struct ThreadData<'a> {
handlebars: Handlebars<'a>,
}
#[get("/")]
async fn index(tdata: web::Data<ThreadData<'_>>) -> impl Responder {
HttpResponse::Ok().body(
tdata
.handlebars
.render(
"welcome",
&btreemap!(
"version" => env!("CARGO_PKG_VERSION").to_string()
),
)
.unwrap(),
)
}
#[get("/debug/")]
async fn debug(data: web::Data<AppState>) -> impl Responder {
HttpResponse::Ok().body(serde_json::to_string_pretty(data.as_ref()).unwrap())
}
async fn socket_loop(socket: &UnixStream) -> Result<(), Box<dyn Error>> {
loop {
let mut data = vec![0; 1024];
socket.readable().await?;
let mut command = String::new();
loop {
match socket.try_read(&mut data) {
Ok(0) => break,
Ok(_n) => {
command.push_str(String::from_utf8(data.clone()).unwrap().trim());
println!("command: {}", command);
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
break;
}
Err(e) => {
return Err(e.into());
}
}
}
socket.writable().await?;
match socket.try_write(&command.into_bytes()) {
Ok(_n) => {}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
continue;
}
Err(e) => {
return Err(e.into());
}
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct AppInfo<'s> {
version: &'s str,
}
impl<'s> AppInfo<'s> {
fn get_version(&self) -> Option<[u8; 3]> {
let mut a: [u8; 3] = [0; 3];
let mut ver = self.version.split('.');
a[0] = ver.next()?.parse().ok()?;
a[1] = ver.next()?.parse().ok()?;
a[2] = ver.next()?.parse().ok()?;
Some(a)
}
}
static mut APP_INFO: AppInfo = AppInfo {
version: env!("CARGO_PKG_VERSION"),
};
#[actix_web::main]
async fn main() -> io::Result<()> {
let args: Vec<String> = env::args().collect();
let mut args = args.iter();
args.next().unwrap();
for arg in args {
match arg.as_str() {
"-v" | "--version" => {
println!("Boarders v{}", unsafe { APP_INFO.version });
std::process::exit(0);
}
_ => {
println!("Unknown argument \"{}\"", arg);
}
}
}
/* quick and dirty logging */
SimpleLogger::new()
.with_module_level("mio::poll", LevelFilter::Off)
.with_module_level("actix_server::worker", LevelFilter::Debug)
.with_module_level("handlebars", LevelFilter::Info)
.init()
.unwrap();
println!("Initializing...");
/*
* TODO: config files
* TODO: allow host to specify the data dir via a config file
* why? why not? maybe you want multiple servers running on
* the same machine
* TODO: allow host to specify config file via cmd line (overrides default name)
*/
let data_dir = Path::new("data/");
let mut pwd = env::current_dir().unwrap();
/* Getting app version and checking for backwards compatability issues */
let mut app_info_file = data_dir.to_path_buf();
app_info_file.push("app.json");
let mut app_info_file = fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(app_info_file)
.unwrap();
let mut app_info_str = String::new();
app_info_file.read_to_string(&mut app_info_str).unwrap();
let cur_app_info: AppInfo = match serde_json::from_str(&app_info_str) {
Ok(a) => a,
Err(_) => {
println!("Malformed app.json file, backing up then creating new");
let s = unsafe { serde_json::to_string(&APP_INFO).unwrap() };
app_info_file.write_all(s.as_bytes()).unwrap();
unsafe { APP_INFO.clone() }
}
};
let ver = cur_app_info.get_version().unwrap();
match ver[0] {
x if x <= 2 => match ver[1] {
y if y <= 2 => match ver[2] {
0 => {
if Command::new("./compat/2.2.0.sh").output().is_err() {
println!("Error, could not complete compatability upgrade");
} else {
println!("Fixed compat issues for v2.2.0");
}
}
_ => {
println!("Invalid version number?")
}
},
_ => {
println!("Invalid version number?")
}
},
_ => {
println!("Invalid version number?")
}
}
/* open/create the directory where board data is placed */
let board_files: ReadDir = fs::read_dir({
pwd.push(data_dir);
pwd.push("boards/");
&pwd
})
.unwrap_or({
fs::create_dir_all(&pwd).unwrap();
fs::read_dir(pwd).unwrap()
});
let mut boards: Vec<Board> = Vec::new();
/*
* Iterate over all the files inside the board data dir and
* open/create them. Will panic! if there is an error opening
* a board
*/
for file in board_files.map(|x| x.unwrap()) {
if file.file_type().unwrap().is_dir() {
let mut path = file.path().to_path_buf();
path.push("board.json");
println!("loading board: {:?}", path);
boards.push(
Board::open(&path)
.unwrap_or_else(|e| panic!("Could not open board {:?}\n{}", path, e)),
);
}
}
/* sort boards by id so they display correctly on the board index */
boards.sort_by(|a, b| a.id.cmp(&b.id));
println!(
"{}",
style(format!("Found and loaded {} boards!", boards.len())).green()
);
let mut board_data = std::path::PathBuf::new();
board_data.push(data_dir);
let ff = FactoryFactory::new(data_dir);
let (mut bf, mf, um) = ff.build();
board_data.push("boards/");
bf.set_data_dir(board_data);
/* Initialize all the the shared data crap. copy/pasting code time!! */
let web_data = web::Data::new(AppState {
boards: Mutex::new(boards.clone()),
board_factory: Mutex::new(
BoardFactory::load_state({
println!("Loading board factory file...");
let mut dd = data_dir.to_path_buf();
dd.push("boards/factory.json");
dd
})
.unwrap_or(bf),
),
msg_factory: Mutex::new(
MessageFactory::load_state({
println!("Loading message factory file...");
let mut dd = data_dir.to_path_buf();
dd.push("message_factory.json");
dd
})
.unwrap_or(mf),
),
user_manager: Mutex::new(
UserManager::load_state({
println!("Loading user manager file...");
let mut dd = data_dir.to_path_buf();
dd.push("users.json");
dd
})
.unwrap_or(um),
),
});
let wb = web_data.clone();
println!(
"{}",
style("Loaded/Created application data files!").green()
);
println!("Connecting to Unix socket...");
let (tx0, rx0) = oneshot::channel::<bool>();
/* NOTE: More phases will probably exist in the future */
enum AppPhase {
Normal,
Shutdown,
}
let (s_tx, mut s_rx) = watch::channel::<AppPhase>(AppPhase::Normal);
let rt = Runtime::new().unwrap();
let _guard = rt.enter();
/* spawn the thread that manages the unix socket */
tokio::spawn(async move {
let p = Path::new("./boarders.sock");
if p.exists() {
std::fs::remove_file(p).unwrap();
}
let socket = UnixListener::bind("./boarders.sock").unwrap_or_else(|_| {
panic!("{}", style("Could not connect to Unix socket").red());
});
println!("{}", style("Listening on Unix socket!").green());
if tx0.send(true).is_err() {
panic!(
"{}",
style("[FATAL] Thread reciever was dropped! (should never happen)")
.red()
.bold()
);
}
/*
* HACK: clone the reciever a bunch of times to get around moves
* not sure if there's a better way
*/
let s_rx_0 = s_rx.clone();
let spawn_t = tokio::spawn(async move {
let s_rx = s_rx_0.clone();
loop {
let mut s_rx = s_rx.clone();
/* accept socket then wait for exit, or shutdown signal */
let (conn, _addr) = socket.accept().await.unwrap();
tokio::spawn(async move {
select! {
_socket = socket_loop(&conn) => 0,
_state = s_rx.changed() => 1
}
});
}
});
/*
* FIXME: use enums when I feel like it (not urgent)
*/
match select! {
_st = spawn_t => 0,
state = s_rx.changed() => if state.is_ok() {
match *s_rx.borrow() {
AppPhase::Shutdown => 1,
_ => 0
}
} else {
0
}
} {
0 => println!("Client disconnected"),
1 => println!("Shut down client thread(s)"),
_ => println!("Unexpected value from client thread"),
}
});
/* why do I want a special message for that? Honestly I don't know... */
rx0.await.unwrap_or_else(|_| {
panic!(
"{}",
style("[FATAL] Thread transmitter was dropped! (should never happen)")
.red()
.bold()
)
});
println!("{}", style("Finished initialization").green().bold());
println!("Starting server...");
let server =
HttpServer::new(move || {
App::new()
.data(ThreadData {
handlebars: {
let mut h = Handlebars::new();
/*
* TODO: allow user to specify wether or not templates should be
* loaded dynamically or statically, probably via environment
* variable at compile time
*/
/* register handlebars partials here */
handlebars_helper!(msg_helper: |s: str| format!("helped: {}", s.to_string()));
h.register_helper("msg", Box::new(msg_helper));
h.register_partial("head", include_str!("res/snippets/head.hbs")).unwrap();
h.register_partial("login_status", include_str!("res/snippets/login_status.hbs")).unwrap();
h.register_partial("board_bar", include_str!("res/snippets/board_bar.hbs")).unwrap();
/* register handlebars templates here */
h.register_template_string("board_index", include_str!("res/board_index.hbs")).unwrap();
h.register_template_string("welcome", include_str!("res/welcome.hbs")).unwrap();
h.register_template_string("auth", include_str!("res/auth.hbs")).unwrap();
h.register_template_string("board", include_str!("res/board.hbs")).unwrap();
h
}
}).app_data(web_data.clone())
.service(index)
.service(debug)
.service(list_boards)
.service(login)
.service(auth_html)
.service(sign_up)
.service(sign_up_result)
.service(new_board)
.service(new_board_result)
.service(get_board)
.service(board_post)
.service(
/* add all api services under the /api/ scope */
web::scope("/api")
.service(api::boards::new)
.service(api::boards::list)
.service(api::messages::new)
.service(api::users::new)
.service(api::users::list)
.service(api::users::get)
.service(api::users::auth))
/* serve all static files inside ../static/ */
.service(actix_files::Files::new("/static/", "./static/").show_files_listing())
})
.bind("127.0.0.1:8080")?
.run();
println!("Started server");
println!(
"{}v{}",
include_str!("res/banner"),
env!("CARGO_PKG_VERSION")
);
/*
* Program will block here until it is Ctrl-C 'd on the command line
* TODO: Block until cmd line exit OR client shutdown command
*/
let res = server.await;
println!("\nExiting...");
/*
* Note to self: handle all errors as much as possible
* Use unwrap() as little as possible
* If anything goes wrong while saving data data WILL get lost
*/
println!("Saving application state...");
for b in wb.boards.lock().unwrap().iter() {
fs::create_dir_all(&b.path)
.unwrap_or_else(|x| println!("Could not create directory {:?}: {}", b.path, x));
b.save_state("board.json")
.unwrap_or_else(|x| println!("Could not save board {:?}: {}", b.title, x));
}
wb.board_factory
.lock()
.unwrap()
.save_state("factory.json")
.unwrap_or_else(|x| println!("Could not save user factory state: {}", x));
wb.msg_factory
.lock()
.unwrap()
.save_state("message_factory.json")
.unwrap_or_else(|x| println!("Could not save message factory state: {}", x));
wb.user_manager
.lock()
.unwrap()
.save_state("users.json")
.unwrap_or_else(|x| println!("Could not save user data: {}", x));
/* send shutdown signal to all unix socket threads */
println!("Shutting down client threads...");
if s_tx.send(AppPhase::Shutdown).is_err() {
println!(
"{}",
style("Could not update the app phase to Shutdown")
.red()
.bold()
);
}
rt.shutdown_timeout(std::time::Duration::from_secs(10));
println!("Done!");
res
}

17
v1/src/res/auth.hbs Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
{{> head}}
<body>
<div id="title" class="bar">
{{#if user.nick}}
<h1>Hello, {{user.nick}}</h1>
{{else}}
<h1>Hello, {{user.nickname}}</h1>
{{/if}}
</div><div id="index">
<p>
Logged in as {{user.username}}, return to <a href="/boards/">index</a>
</p>
</div>
</body>
</html>

6
v1/src/res/banner Normal file
View File

@ -0,0 +1,6 @@
____ __
/ __ )\ ____ ____ _ _____ ____/ /\__ _____ _____
/ __ | / __ \ / __ `/\/ ___// __ // _ \ / ___// ___/\
/ /_/ / / /_/ // /_/ / / / _// /_/ // __/\/ / _/(__ )\/
/_____/ /\____/ \__,_/ /_/ / \__,_/ \___/ /_/ / /____/\)
\_____\/ \___\/ \___\/\_\/ \___\/ \__\/\_\/ \____\/

36
v1/src/res/board.hbs Normal file
View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
{{> head}}
<body style="overflow-x: hidden;">
{{> board_bar board=board user=user}}
<div id="index">
<!--messages-->
{{#each messages as | msg |}}
<div style="border-bottom: 1px solid #555" class="message">
{{#if msg.author.nick}}
<div style="display: flex; flex-direction: column; align-items: center;">
{{msg.author.nick}}<br>
<div style="color: #777777;">{{author.username}}</div>
</div>
{{else}}
<b>{{msg.author.username}}</b>
{{/if}}
<div class="message-content">
<pre>{{{msg.render_text}}}</pre>
</div>
<div class="message-time">
{{msg.time_meta}}
</div>
</div>
{{/each}}
</div>
<form action="/boards/post" method="post" style="display: flex; align-items: center; flex-direction: column;">
<label for="content">Write a message:</label><br/>
<input type="hidden" id="board_id" name="board_id" value="{{board.id}}">
<input type="hidden" id="user_id" name="user_id" value="{{user.user_id}}">
<textarea name="content" id="content" rows=12 cols=50 placeholder="Enter message here..."></textarea>
<br/>
<input type="submit" value="Post Message" style="margin: 0.5em;">
</form>
</body>
</html>

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
{{> head}}
<body style="overflow-x: hidden;">
<div id="title" class="bar">
<div>
<h1>Board Index</h1>
</div>
{{> login_status}}
</div>
<div id="index">
<a href="/boards/new/" style="margin-bottom: 0.5em;">New Board [+]</a>
<table style="width: 100%;" cellpadding=8px>
<tr><th>Name</th><th>Description</th><th>Last Active</th></tr>
{{#each boards}}
<tr class="board">
<td><a href="/board/{{id}}/">{{title}}</a></td>
<td>{{desc}}</td>
{{#if time_meta}}
<td>{{time_meta.0}} {{time_meta.1}}</td>
{{else}}
<td>Never</td>
{{/if}}
</tr>
{{/each}}
</table>
</div>
</body>
</html>

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
{{> head}}
<body>
<div id="title" class="bar">
<h1>Hello, {{name}}</h1>
</div>
<div id="index">
<p>
Hooray! You can log into you new account <a href="/account/login/">here</a>
</p>
</body>
</html>

View File

@ -0,0 +1,11 @@
<div id="title" class="bar">
<div>
<h1>
{{board.title}} | <a href="/boards/">&lt;- Back to index</a>
</h1>
</div>
<div id="description">
{{board.desc}}
</div>
{{> login_status user=user}}
</div>

View File

@ -0,0 +1,9 @@
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon"/>
{{#if nolocal}}
{{else}}
<link rel="stylesheet" type="text/css" href="/static/index.css">
{{/if}}
</head>

View File

@ -0,0 +1,17 @@
<div id="login-status">
<p>
{{#if user}}
<a href="/account/dashboard/">
{{#with user}}
{{#if nick}}
Welcome, {{nick}}
{{else}}
Welcome, {{username}}
{{/if}}
{{/with}}
</a>
{{else}}
<a href="/account/login/">Login</a>/<a href="/account/new/">Sign Up</a>
{{/if}}
</p>
</div>

49
v1/src/res/welcome.hbs Normal file
View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
{{> head nolocal=1}}
<body>
<h1>
<div style="display: flex; align-items: center; flex-direction: column;">
<pre style="font-size: 1.5rem; background-color: unset; overflow-x: unset; margin: unset; padding: unset;">
<b>
____ __
/ __ )\ ____ ____ _ _____ ____/ /\__ _____ _____
/ __ | / __ \ / __ `/\/ ___// __ // _ \ / ___// ___/\
/ /_/ / / /_/ // /_/ / / / _// /_/ // __/\/ / _/(__ )\/
/_____/ /\____/ \__,_/ /_/ / \__,_/ \___/ /_/ / /____/\)
\_____\/ \___\/ \___\/\_\/ \___\/ \__\/\_\/ \____\/
</b>
</pre>
<div style="font-size: 2rem;">
v{{version}}
</div>
</div>
</h1>
<p>
<h2>What is it?</h2>
Boarders is an easily deployable message board written in Rust.
It is extremely efficient on both the frontend and backend.
On the frontent all pages are static and (at this time) have no JS, only CSS/HTML.
The backend uses the 5th fastest web framework, Actix, according to
<a href="https://www.techempower.com/benchmarks/">techempower.com</a>.
Which ensures that response times will always be snappy.
</p>
<p>
<hr>
<h2>Rules</h2>
<ol>
<li>No NSFW<br>
At least until NSFW channels are implemented</li>
<li>
Don't spam boards or user accounts<br>
It's just anoying, and if it gets too bad I may have to hard reset everything<br>
Don't ruin the fun for everyone
</li>
<li>Have Fun!<br>
Pretty much anything goes, but if people don't like you, that's your fault
</li>
</ol>
</p>
<h3 style="text-align: center;">------<a href="/boards/">[Get Started]</a>------</h3>
</body>
</html>

View File

@ -0,0 +1,25 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head>
<body>
<div id="title" class="bar">
<h1>Login</h1>
</div>
<div id="index">
<div class="auth">
<form action="/account/auth" method="post">
<div>
<label for="username">Username: </label>
<input type="text" id="username" name="username" required>
</div>
<div>
<label for="password">Password: </label>
<input type="password" id="password" name="password" required>
</div>
<input type="submit" value="Login">
</form>
</div>
</body>
</html>

210
v1/static/index.css Normal file
View File

@ -0,0 +1,210 @@
body {
margin: unset !important;
padding: unset;
max-width: unset;
}
.bar {
padding: 0.5em;
background-color: #111111;
z-index: 1;
display: flex;
flex-direction: row;
}
.bar > div {
flex-grow: 1;
align-self: center;
}
.bar > div > h1 {
font-size: xx-large;
margin: unset;
}
.bar > div > p {
margin-left: auto;
margin-right: 0;
margin-right: 2em;
}
.bar > h1 {
width: fit-content;
height: fit-content;
}
.bar > h1 > a {
font-size: 0.5em;
vertical-align: middle;
}
#login-status {
display: contents;
}
#description {
color: #777777;
}
#title {
width: 100%;
height: 3em;
position: inherit;
top: 0;
left: 0;
margin-bottom: 1rem;
padding-left: 1em;
}
#title > h1 {
font-size: 2.5em !important;
}
.message {
margin-top: 0.5em;
margin-bottom: 0.5em;
border-bottom: 1px solid #555;
display: flex;
flex-direction: inherit;
justify-content: flex-start;
}
.message > b {
padding-left: 1em;
}
.message > * {
padding: 0.2em;
}
.message:hover {
background-color: #3a3a3a;
animation-duration: 0.1s;
animation-name: msg-hover;
}
@keyframes msg-hover {
from {
background-color: inherit;
}
to {
background-color: #3a3a3a;
}
}
.vertical-center {
margin: 0;
position: absolute;
top: 50%;
-ms-transform: translateY(-50%);
transform: translateY(-50%);
}
table {
font-size: 1em;
}
table, th, td {
border-collapse: collapse;
}
th {
text-align: left;
margin-bottom: 1em;
}
.auth {
margin: auto;
width: 20em;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
border: 2px solid white;
border-radius: 5px;
background-color: #3A3A3A;
}
#index {
padding: 3em;
padding-top: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
}
@keyframes board-hover {
from {
background-color: inherit;
}
to {
background-color: #444;
}
}
tr.board {
border-bottom: 1px solid #555;
}
tr.board:nth-child(1n+1) {
font-weight: lighter;
}
tr.board:hover {
animation-duration: 0.25s;
animation-name: board-hover;
background-color: #444;
}
.message-content {
flex-grow: 1;
/*max-width: 90%; */
}
.message-content > pre {
margin-top: 0.1rem;
margin-bottom: 0.1rem;
}
pre {
white-space: pre-wrap; /* Since CSS 2.1 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
}
pre > h1 {
font-size: 2rem;
}
pre > h2 {
font-size: 1.75rem;
}
pre > h3 {
font-size: 1.5rem;
}
pre > h4 {
font-size: 1.25rem;
}
pre > h5 {
font-size: 1rem;
}
pre > *:last-child {
margin-bottom: unset;
}
code {
background-color: #222;
padding: 0.5rem;
border-radius: 0.25rem;
}
.message-time {
font-size: 0.75rem;
color: #555
}

View File

@ -0,0 +1,24 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head>
<body>
<div id="title" class="bar">
<h1>Sign Up</h1>
</div><div id="index">
<div class="auth">
<form action="/account/new" method="post">
<div>
<label for="username">Username: </label>
<input type="text" id="username" name="username" required>
</div>
<div>
<label for="password">Password: </label>
<input type="password" id="password" name="password" required>
</div>
<input type="submit" value="Sign Up">
</form>
</div>
</body>
</html>

28
v1/static/new_board.html Normal file
View File

@ -0,0 +1,28 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="https://orbitalfox.us/Music/index.css">
<link rel="stylesheet" type="text/css" href="/static/index.css">
</head>
<body>
<div id="title" class="bar">
<h1>New Board</h1>
</div>
<div id="index">
<div class="auth">
<form action="/boards/new" method="post">
<div>
<label for="name">Board Title: </label><br>
<input type="text" id="name" name="name" required>
</div>
<div>
<label for="description">Board Description: </label>
<input type="text" id="description" name="description" required>
</div>
<input type="submit" value="Create Board">
</form>
</div>
</body>
</html>
</div>
</body>
</html>

1
v2/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
boarders.v2

3
v2/go.mod Normal file
View File

@ -0,0 +1,3 @@
module boarders.v2
go 1.22.0

9
v2/main.go Normal file
View File

@ -0,0 +1,9 @@
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello Boarders!")
}