[add] guppy connection

[remove] boards (https://code.stellular.org/stellular/puffer)
[remove] users (https://code.stellular.org/stellular/guppy)
[chore] bump version (v0.11.8 -> v0.12.0)
This commit is contained in:
hkau 2024-03-29 23:55:15 -04:00
parent 1ad5aafe1a
commit 5035123a27
26 changed files with 98 additions and 6885 deletions

View File

@ -3,7 +3,7 @@ name = "bundlrs"
authors = ["hkau"]
license = "MIT"
version = "0.11.8"
version = "0.12.0"
edition = "2021"
rust-version = "1.75"
@ -43,5 +43,6 @@ sqlx = { version = "0.7.3", features = [
] }
uuid = { version = "1.6.1", features = ["v4"] }
yew = { version = "0.21.0", features = ["ssr"] }
handlebars = "5.1.2"
redis = "0.25.2"
handlebars = "5.1.2"
actix-cors = "0.7.0"

View File

@ -6,6 +6,8 @@ Bundlrs is a *super* lightweight and [anonymous](#user-accounts) social markdown
For migration from Bundles, please see [#3](https://code.stellular.org/stellular/bundlrs/issues/3).
> Also see [Puffer](https://code.stellular.org/stellular/puffer) and [Guppy](https://code.stellular.org/stellular/guppy)! (required)
## Install
Bundlrs provides build scripts using [just](https://github.com/casey/just). It is required that `bun`, `just`, `redis`, and (obviously) Rust are installed before running.

View File

@ -1,100 +1,32 @@
use actix_web::{get, post, web, HttpMessage, HttpRequest, HttpResponse, Responder};
use crate::db::bundlesdb::{self, AppData};
use actix_web::{get, post, web, HttpRequest, HttpResponse, Responder};
use crate::{
db::bundlesdb::{self, AppData, DefaultReturn, FullUser, UserFollow, UserMetadata},
utility,
};
#[derive(serde::Deserialize)]
struct RegisterInfo {
username: String,
#[derive(Default, PartialEq, serde::Deserialize)]
pub struct CallbackQueryProps {
pub uid: Option<String>, // this uid will need to be sent to the client as a token
// the uid will also be sent to the client as a token on GUPPY_ROOT, meaning we'll have signed in here and there!
}
#[derive(serde::Deserialize)]
struct LoginInfo {
uid: String,
}
#[derive(serde::Deserialize)]
struct UpdateAboutInfo {
about: String,
}
#[post("/api/auth/register")]
pub async fn register(body: web::Json<RegisterInfo>, data: web::Data<AppData>) -> impl Responder {
// if server disabled registration, return
let disabled = crate::config::get_var("REGISTRATION_DISABLED");
if disabled.is_some() {
return HttpResponse::NotAcceptable()
.body("This server requires has registration disabled");
}
// ...
let username = &body.username.trim();
let res = data.db.create_user(username.to_string()).await;
let c = res.clone();
let set_cookie = if res.success && res.payload.is_some() {
format!("__Secure-Token={}; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}", c.message, 60 * 60 * 24 * 365)
#[get("/api/auth/callback")]
pub async fn callback_request(info: web::Query<CallbackQueryProps>) -> impl Responder {
let set_cookie = if info.uid.is_some() {
format!("__Secure-Token={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}", info.uid.as_ref().unwrap(), 60 * 60 * 24 * 365)
} else {
String::new()
};
// return
return HttpResponse::Ok()
.append_header(("Set-Cookie", if res.success { &set_cookie } else { "" }))
.append_header(("Content-Type", "application/json"))
.body(serde_json::to_string(&res).unwrap());
}
#[post("/api/auth/login")]
pub async fn login(body: web::Json<LoginInfo>, data: web::Data<AppData>) -> impl Responder {
let id = body.uid.trim();
let id_hashed = utility::hash(id.to_string());
let res = data
.db
.get_user_by_hashed(id_hashed) // if the user is returned, that means the ID is valid
.await;
let set_cookie = if res.success && res.payload.is_some() {
format!("__Secure-Token={}; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}", body.uid, 60 * 60 * 24 * 365)
} else {
String::new()
};
// return
return HttpResponse::Ok()
.append_header(("Set-Cookie", if res.success { &set_cookie } else { "" }))
.append_header(("Content-Type", "application/json"))
.body(serde_json::to_string(&res).unwrap());
}
#[post("/api/auth/login-st")]
pub async fn login_secondary_token(
body: web::Json<LoginInfo>,
data: web::Data<AppData>,
) -> impl Responder {
let id = body.uid.trim();
let id_unhashed = id.to_string();
let res = data
.db
.get_user_by_unhashed_st(id_unhashed) // if the user is returned, that means the token is valid
.await;
let set_cookie = if res.success && res.payload.is_some() {
format!("__Secure-Token={}; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}", body.uid, 60 * 60 * 24 * 365)
} else {
String::new()
};
// return
return HttpResponse::Ok()
.append_header(("Set-Cookie", if res.success { &set_cookie } else { "" }))
.append_header(("Content-Type", "application/json"))
.body(serde_json::to_string(&res).unwrap());
.append_header((
"Set-Cookie",
if info.uid.is_some() { &set_cookie } else { "" },
))
.append_header(("Content-Type", "text/html"))
.body(
"<head>
<meta http-equiv=\"Refresh\" content=\"0; URL=/d\" />
</head>",
);
}
#[get("/api/auth/logout")]
@ -121,298 +53,30 @@ pub async fn logout(req: HttpRequest, data: web::Data<AppData>) -> impl Responde
.body("You have been signed out. You can now close this tab.");
}
#[post("/api/auth/users/{name:.*}/about")]
pub async fn edit_about_request(
#[get("/api/auth/users/{name:.*?}/pastes")]
/// Get all pastes by owner
pub async fn get_from_owner_request(
req: HttpRequest,
body: web::Json<UpdateAboutInfo>,
data: web::Data<AppData>,
data: web::Data<bundlesdb::AppData>,
info: web::Query<crate::api::pastes::OffsetQueryProps>,
) -> impl Responder {
let name: String = req.match_info().get("name").unwrap().to_string();
// get token user
let token_cookie = req.cookie("__Secure-Token");
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
} else {
Option::None
};
if token_user.is_some() {
// make sure user exists
if token_user.as_ref().unwrap().success == false {
return HttpResponse::NotFound().body("Invalid token");
}
} else {
return HttpResponse::NotAcceptable().body("An account is required to do this");
}
let token_user = token_user.unwrap().payload.unwrap();
// make sure profile exists
let profile: DefaultReturn<Option<FullUser<String>>> =
data.db.get_user_by_username(name.to_owned()).await;
if !profile.success {
return HttpResponse::NotFound()
.append_header(("Content-Type", "application/json"))
.body(
serde_json::to_string::<DefaultReturn<Option<String>>>(&DefaultReturn {
success: false,
message: String::from("Profile does not exist!"),
payload: Option::None,
})
.unwrap(),
);
}
let profile = profile.payload.unwrap();
let mut user = serde_json::from_str::<UserMetadata>(&profile.user.metadata).unwrap();
// check if we can update this user
// must be authenticated AND same user OR staff
let can_update: bool = (token_user.user.username == profile.user.username)
| (token_user
.level
.permissions
.contains(&String::from("ManageUsers")));
if can_update == false {
return HttpResponse::NotFound()
.body("You do not have permission to manage this user's contents.");
}
// (check length)
if (body.about.len() < 2) | (body.about.len() > 200_000) {
return HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))
.body(
serde_json::to_string::<DefaultReturn<Option<String>>>(&DefaultReturn {
success: false,
message: String::from("Content is invalid"),
payload: Option::None,
})
.unwrap(),
);
}
// update about
user.about = body.about.clone();
// ...
let res = data.db.edit_user_metadata_by_name(name, user).await;
// return
return HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))
.body(serde_json::to_string(&res).unwrap());
}
#[post("/api/auth/users/{name:.*}/secondary-token")]
pub async fn refresh_secondary_token_request(
req: HttpRequest,
data: web::Data<AppData>,
) -> impl Responder {
let name: String = req.match_info().get("name").unwrap().to_string();
// get token user
let token_cookie = req.cookie("__Secure-Token");
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
} else {
Option::None
};
if token_user.is_some() {
// make sure user exists
if token_user.as_ref().unwrap().success == false {
return HttpResponse::NotFound().body("Invalid token");
}
} else {
return HttpResponse::NotAcceptable().body("An account is required to do this");
}
let token_user = token_user.unwrap().payload.unwrap();
// make sure profile exists
let profile: DefaultReturn<Option<FullUser<String>>> =
data.db.get_user_by_username(name.to_owned()).await;
if !profile.success {
return HttpResponse::NotFound()
.append_header(("Content-Type", "application/json"))
.body(
serde_json::to_string::<DefaultReturn<Option<String>>>(&DefaultReturn {
success: false,
message: String::from("Profile does not exist!"),
payload: Option::None,
})
.unwrap(),
);
}
let profile = profile.payload.unwrap();
let mut user = serde_json::from_str::<UserMetadata>(&profile.user.metadata).unwrap();
// check if we can update this user
// must be authenticated AND same user OR staff
let can_update: bool = (token_user.user.username == profile.user.username)
| (token_user
.level
.permissions
.contains(&String::from("ManageUsers")));
if can_update == false {
return HttpResponse::NotFound()
.body("You do not have permission to manage this user's contents.");
}
// update secondary token
let token = crate::utility::uuid();
user.secondary_token = Option::Some(crate::utility::hash(token.clone())); // this is essentially just a second ID the user can signin with
// ...
let res = data.db.edit_user_metadata_by_name(name, user).await;
// get pastes
let res: bundlesdb::DefaultReturn<Option<Vec<bundlesdb::PasteIdentifier>>> =
data.db.get_pastes_by_owner_limited(name, info.offset).await;
// return
return HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))
.body(
serde_json::to_string::<DefaultReturn<String>>(&DefaultReturn {
success: res.success,
message: res.message,
payload: token,
})
serde_json::to_string::<
bundlesdb::DefaultReturn<Option<Vec<bundlesdb::PasteIdentifier>>>,
>(&res)
.unwrap(),
);
}
#[post("/api/auth/users/{name:.*}/follow")]
pub async fn follow_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
let name: String = req.match_info().get("name").unwrap().to_string();
// get token user
let token_cookie = req.cookie("__Secure-Token");
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
} else {
Option::None
};
if token_user.is_some() {
// make sure user exists
if token_user.as_ref().unwrap().success == false {
return HttpResponse::NotFound().body("Invalid token");
}
} else {
return HttpResponse::NotAcceptable().body("An account is required to do this");
}
let token_user = token_user.unwrap().payload.unwrap();
// ...
let res = data
.db
.toggle_user_follow(&mut UserFollow {
user: token_user.user.username,
is_following: name,
})
.await;
// return
return HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))
.body(serde_json::to_string(&res).unwrap());
}
#[post("/api/auth/users/{name:.*}/update")]
pub async fn update_request(
req: HttpRequest,
body: web::Json<UserMetadata>,
data: web::Data<AppData>,
) -> impl Responder {
let name: String = req.match_info().get("name").unwrap().to_string();
// get token user
let token_cookie = req.cookie("__Secure-Token");
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
} else {
Option::None
};
if token_user.is_some() {
// make sure user exists
if token_user.as_ref().unwrap().success == false {
return HttpResponse::NotFound().body("Invalid token");
}
} else {
return HttpResponse::NotAcceptable().body("An account is required to do this");
}
// make sure profile exists
let profile: DefaultReturn<Option<FullUser<String>>> =
data.db.get_user_by_username(name.to_owned()).await;
if !profile.success {
return HttpResponse::NotFound()
.append_header(("Content-Type", "application/json"))
.body(
serde_json::to_string::<DefaultReturn<Option<String>>>(&DefaultReturn {
success: false,
message: String::from("Profile does not exist!"),
payload: Option::None,
})
.unwrap(),
);
}
let token_user = token_user.unwrap().payload.unwrap();
let profile = profile.payload.unwrap();
// check if we can update this user
// must be authenticated AND same user OR staff
let can_update: bool = (token_user.user.username == profile.user.username)
| (token_user
.level
.permissions
.contains(&String::from("ManageUsers")));
if can_update == false {
return HttpResponse::NotFound()
.body("You do not have permission to manage this user's contents.");
}
// ...
let res = data
.db
.edit_user_metadata_by_name(
name, // select user
body.to_owned(), // new metadata
)
.await;
// return
return HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))
.body(serde_json::to_string(&res).unwrap());
}
#[post("/api/auth/users/{name:.*?}/ban")]
/// Ban user
pub async fn ban_request(req: HttpRequest, data: web::Data<bundlesdb::AppData>) -> impl Responder {
@ -459,158 +123,3 @@ pub async fn ban_request(req: HttpRequest, data: web::Data<bundlesdb::AppData>)
.append_header(("Content-Type", "application/json"))
.body(serde_json::to_string::<bundlesdb::DefaultReturn<Option<String>>>(&res).unwrap());
}
#[get("/api/auth/users/{name:.*}/followers")]
pub async fn followers_request(
req: HttpRequest,
data: web::Data<AppData>,
info: web::Query<crate::pages::boards::ViewBoardQueryProps>,
) -> impl Responder {
let name: String = req.match_info().get("name").unwrap().to_string();
// get followers
let res: DefaultReturn<Option<Vec<bundlesdb::Log>>> = data
.db
.get_user_followers(name.to_owned(), info.offset)
.await;
// return
return HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))
.body(serde_json::to_string::<DefaultReturn<Option<Vec<bundlesdb::Log>>>>(&res).unwrap());
}
#[get("/api/auth/users/{name:.*}/following")]
pub async fn following_request(
req: HttpRequest,
data: web::Data<AppData>,
info: web::Query<crate::pages::boards::ViewBoardQueryProps>,
) -> impl Responder {
let name: String = req.match_info().get("name").unwrap().to_string();
// get following
let res: DefaultReturn<Option<Vec<bundlesdb::Log>>> = data
.db
.get_user_following(name.to_owned(), info.offset)
.await;
// return
return HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))
.body(serde_json::to_string::<DefaultReturn<Option<Vec<bundlesdb::Log>>>>(&res).unwrap());
}
#[get("/api/auth/users/{name:.*}/avatar")]
pub async fn avatar_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
let name: String = req.match_info().get("name").unwrap().to_string();
// make sure profile exists
let profile: DefaultReturn<Option<FullUser<String>>> =
data.db.get_user_by_username(name.to_owned()).await;
if !profile.success {
return HttpResponse::NotFound()
.append_header(("Content-Type", "application/json"))
.body(
serde_json::to_string::<DefaultReturn<Option<String>>>(&DefaultReturn {
success: false,
message: String::from("Profile does not exist!"),
payload: Option::None,
})
.unwrap(),
);
}
let profile = profile.payload.unwrap();
let user = serde_json::from_str::<UserMetadata>(&profile.user.metadata).unwrap();
if user.avatar_url.is_none() {
return HttpResponse::NotFound().body("User does not have an avatar set");
}
let avatar = user.avatar_url.unwrap();
// fetch avatar
let res = data
.http_client
.get(avatar)
.timeout(std::time::Duration::from_millis(5_000))
.insert_header(("User-Agent", "stellular-bundlrs/1.0"))
.send()
.await;
if res.is_err() {
return HttpResponse::NotFound().body(format!(
"Failed to fetch avatar on server: {}",
res.err().unwrap()
));
}
// ...
let mut res = res.unwrap();
let body = res.body().limit(10_000_000).await;
if body.is_err() {
return HttpResponse::NotFound().body(
"Failed to fetch avatar on server (image likely too large, please keep under 10 MB)",
);
}
let body = body.unwrap();
// return
return HttpResponse::Ok()
.append_header(("Content-Type", res.content_type()))
.body(body);
}
#[get("/api/auth/users/{name:.*?}/pastes")]
/// Get all pastes by owner
pub async fn get_from_owner_request(
req: HttpRequest,
data: web::Data<bundlesdb::AppData>,
info: web::Query<crate::pages::boards::ViewBoardQueryProps>,
) -> impl Responder {
let name: String = req.match_info().get("name").unwrap().to_string();
// get pastes
let res: bundlesdb::DefaultReturn<Option<Vec<bundlesdb::PasteIdentifier>>> =
data.db.get_pastes_by_owner_limited(name, info.offset).await;
// return
return HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))
.body(
serde_json::to_string::<
bundlesdb::DefaultReturn<Option<Vec<bundlesdb::PasteIdentifier>>>,
>(&res)
.unwrap(),
);
}
#[get("/api/auth/users/{name:.*}/level")]
pub async fn level_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
let name: String = req.match_info().get("name").unwrap().to_string();
// get user
let res: DefaultReturn<Option<FullUser<String>>> =
data.db.get_user_by_username(name.to_owned()).await;
if res.success == false {
return HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))
.body(
serde_json::to_string::<bundlesdb::RoleLevel>(&bundlesdb::RoleLevel {
elevation: -1000,
name: String::from("anonymous"),
permissions: Vec::new(),
})
.unwrap(),
);
}
// return
return HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))
.body(serde_json::to_string::<bundlesdb::RoleLevel>(&res.payload.unwrap().level).unwrap());
}

View File

@ -1,839 +0,0 @@
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse, Responder};
use crate::{
db::bundlesdb::{
AppData, Board, BoardMetadata, BoardPostLog, DefaultReturn, UserMailStreamIdentifier,
},
pages::boards,
};
#[derive(serde::Deserialize)]
struct CreateInfo {
name: String,
}
#[derive(serde::Deserialize)]
struct CreatePostInfo {
content: String,
reply: Option<String>,
topic: Option<String>,
}
#[derive(serde::Deserialize)]
struct UpdatePostInfo {
content: String,
topic: Option<String>,
}
#[derive(serde::Deserialize)]
struct UpdatePostTagsInfo {
tags: String,
}
#[post("/api/board/new")]
pub async fn create_request(
req: HttpRequest,
body: web::Json<CreateInfo>,
data: web::Data<AppData>,
) -> impl Responder {
// get token user
let token_cookie = req.cookie("__Secure-Token");
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
} else {
Option::None
};
if token_user.is_some() {
// make sure user exists
if token_user.as_ref().unwrap().success == false {
return HttpResponse::NotFound().body("Invalid token");
}
} else {
return HttpResponse::NotAcceptable().body("An account is required to do this");
}
// ...
let res = data
.db
.create_board(
&mut Board {
name: body.name.clone(),
timestamp: 0,
metadata: String::new(),
},
if token_user.is_some() {
Option::Some(token_user.unwrap().payload.unwrap().user.username)
} else {
Option::None
},
)
.await;
// return
return HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))
.body(serde_json::to_string(&res).unwrap());
}
#[post("/api/auth/users/{name:.*}/mail")]
pub async fn create_mail_stream_request(
req: HttpRequest,
data: web::Data<AppData>,
) -> impl Responder {
let name: String = req.match_info().get("name").unwrap().to_string();
// get token user
let token_cookie = req.cookie("__Secure-Token");
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
} else {
Option::None
};
if token_user.is_some() {
// make sure user exists
if token_user.as_ref().unwrap().success == false {
return HttpResponse::NotFound().body("Invalid token");
}
} else {
return HttpResponse::NotAcceptable().body("An account is required to do this");
}
// ...
let res = data
.db
.create_mail_stream(&mut UserMailStreamIdentifier {
_is_user_mail_stream: true,
user1: token_user.unwrap().payload.unwrap().user.username,
user2: name,
})
.await;
// return
return HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))
.body(serde_json::to_string(&res).unwrap());
}
#[get("/api/board/{name:.*}/posts")]
pub async fn get_posts_request(
req: HttpRequest,
data: web::Data<AppData>,
info: web::Query<boards::ViewBoardQueryProps>,
) -> impl Responder {
let name: String = req.match_info().get("name").unwrap().to_string();
let board: DefaultReturn<Option<Board<String>>> = data.db.get_board_by_name(name.clone()).await;
// get
let token_cookie = req.cookie("__Secure-Token");
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
} else {
Option::None
};
// check if board is private
// if it is, only the owner and users with the "staff" role can view it
if board.payload.is_some() {
let metadata =
serde_json::from_str::<BoardMetadata>(&board.payload.as_ref().unwrap().metadata)
.unwrap();
if metadata.is_private == String::from("yes") {
// anonymous
if token_user.is_none() {
return HttpResponse::NotFound()
.body("You do not have permission to view this board's contents.");
}
// not owner
let user = token_user.unwrap().payload.unwrap();
if (user.user.username != metadata.owner)
&& (user
.level
.permissions
.contains(&String::from("ManageBoards")))
{
return HttpResponse::NotFound()
.body("You do not have permission to view this board's contents.");
}
}
}
// ...
let res = data.db.get_board_posts(name, info.offset).await;
// return
return HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))
.body(serde_json::to_string(&res).unwrap());
}
#[get("/api/board/{name:.*}/posts/{id:.*}")]
pub async fn get_post_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
let name: String = req.match_info().get("name").unwrap().to_string();
let id: String = req.match_info().get("id").unwrap().to_string();
let board: DefaultReturn<Option<Board<String>>> = data.db.get_board_by_name(name.clone()).await;
// get
let token_cookie = req.cookie("__Secure-Token");
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
} else {
Option::None
};
// check if board is private
// if it is, only the owner and users with the "staff" role can view it
if board.payload.is_some() {
let metadata =
serde_json::from_str::<BoardMetadata>(&board.payload.as_ref().unwrap().metadata)
.unwrap();
if metadata.is_private == String::from("yes") {
// anonymous
if token_user.is_none() {
return HttpResponse::NotFound()
.body("You do not have permission to view this board's contents.");
}
// not owner
let user = token_user.unwrap().payload.unwrap();
if (user.user.username != metadata.owner)
&& (user
.level
.permissions
.contains(&String::from("ManageBoards")))
{
return HttpResponse::NotFound()
.body("You do not have permission to view this board's contents.");
}
}
}
// ...
let res = data.db.get_log_by_id(id).await;
// return
return HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))
.body(serde_json::to_string(&res).unwrap());
}
#[post("/api/board/{name:.*}/posts")]
pub async fn create_post_request(
req: HttpRequest,
body: web::Json<CreatePostInfo>,
data: web::Data<AppData>,
) -> impl Responder {
let name: String = req.match_info().get("name").unwrap().to_string();
// get token user
let token_cookie = req.cookie("__Secure-Token");
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
} else {
Option::None
};
if token_user.is_some() {
// make sure user exists
if token_user.as_ref().unwrap().success == false {
return HttpResponse::NotFound().body("Invalid token");
}
}
// ...
let token_user = if token_user.is_some() {
Option::Some(token_user.unwrap().payload.unwrap())
} else {
Option::None
};
let res = data
.db
.create_board_post(
&mut BoardPostLog {
author: String::new(),
content: body.content.clone(), // use given content
content_html: String::new(),
topic: body.topic.clone(),
board: name,
is_hidden: false,
reply: if body.reply.is_some() {
Option::Some(body.reply.as_ref().unwrap().to_string())
} else {
Option::None
},
pinned: Option::Some(false),
replies: Option::None,
tags: Option::None,
},
if token_user.is_some() {
Option::Some(token_user.clone().unwrap().user.username)
} else {
Option::None
},
if token_user.is_some() {
Option::Some(token_user.clone().unwrap().user.role)
} else {
Option::None
},
)
.await;
// return
return HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))
.body(serde_json::to_string(&res).unwrap());
}
#[post("/api/board/{name:.*}/posts/{id:.*}/pin")]
pub async fn pin_post_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
let name: String = req.match_info().get("name").unwrap().to_string();
let id: String = req.match_info().get("id").unwrap().to_string();
// get token user
let token_cookie = req.cookie("__Secure-Token");
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
} else {
Option::None
};
if token_user.is_some() {
// make sure user exists
if token_user.as_ref().unwrap().success == false {
return HttpResponse::NotFound().body("Invalid token");
}
} else {
return HttpResponse::NotAcceptable().body("An account is required to do this");
}
// make sure board exists
let board: DefaultReturn<Option<Board<String>>> =
data.db.get_board_by_name(name.to_owned()).await;
if !board.success {
return HttpResponse::NotFound()
.append_header(("Content-Type", "application/json"))
.body(
serde_json::to_string::<DefaultReturn<Option<String>>>(&DefaultReturn {
success: false,
message: String::from("Board does not exist!"),
payload: Option::None,
})
.unwrap(),
);
}
let board = serde_json::from_str::<BoardMetadata>(&board.payload.unwrap().metadata).unwrap();
// get post
let p = data.db.get_log_by_id(id.to_owned()).await;
let mut post = serde_json::from_str::<BoardPostLog>(&p.payload.unwrap().content).unwrap();
// check if we can pin this post
// must be authenticated AND board owner OR staff
let user = token_user.unwrap().payload.unwrap();
let can_pin: bool = (user.user.username != String::from("Anonymous"))
&& ((user.user.username == board.owner)
| (user
.level
.permissions
.contains(&String::from("ManageBoards"))));
if can_pin == false {
return HttpResponse::NotFound()
.body("You do not have permission to manage this board's contents.");
}
// toggle pinned
if post.pinned.is_some() {
post.pinned = Option::Some(!post.pinned.unwrap())
} else {
// update to "true" by default
post.pinned = Option::Some(true);
}
// ...
let res = data
.db
.edit_log(id, serde_json::to_string::<BoardPostLog>(&post).unwrap())
.await;
// return
return HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))
.body(serde_json::to_string(&res).unwrap());
}
#[post("/api/board/{name:.*}/posts/{id:.*}/update")]
pub async fn update_post_request(
req: HttpRequest,
body: web::Json<UpdatePostInfo>,
data: web::Data<AppData>,
) -> impl Responder {
let name: String = req.match_info().get("name").unwrap().to_string();
let id: String = req.match_info().get("id").unwrap().to_string();
// get token user
let token_cookie = req.cookie("__Secure-Token");
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
} else {
Option::None
};
if token_user.is_some() {
// make sure user exists
if token_user.as_ref().unwrap().success == false {
return HttpResponse::NotFound().body("Invalid token");
}
} else {
return HttpResponse::NotAcceptable().body("An account is required to do this");
}
// make sure board exists
let board: DefaultReturn<Option<Board<String>>> =
data.db.get_board_by_name(name.to_owned()).await;
if !board.success {
return HttpResponse::NotFound()
.append_header(("Content-Type", "application/json"))
.body(
serde_json::to_string::<DefaultReturn<Option<String>>>(&DefaultReturn {
success: false,
message: String::from("Board does not exist!"),
payload: Option::None,
})
.unwrap(),
);
}
let board = serde_json::from_str::<BoardMetadata>(&board.payload.unwrap().metadata).unwrap();
// get post
let p = data.db.get_log_by_id(id.to_owned()).await;
let mut post = serde_json::from_str::<BoardPostLog>(&p.payload.unwrap().content).unwrap();
// check board "topic_required" setting (make sure we can't edit to remove topic)
// if it is set to "yes", make sure we provided a topic AND this is not a reply (replies to not count)
if board.topic_required.is_some()
&& board.topic_required.unwrap() == "yes"
&& post.reply.is_none()
&& body.topic.is_none()
{
return HttpResponse::NotFound()
.append_header(("Content-Type", "application/json"))
.body(
serde_json::to_string::<DefaultReturn<Option<String>>>(&DefaultReturn {
success: false,
message: String::from("This board requires a topic to be set before posting"),
payload: Option::None,
})
.unwrap(),
);
}
// check if we can update this post
// must be authenticated AND post author OR staff
let user = token_user.unwrap().payload.unwrap();
let can_update: bool = (user.user.username != String::from("Anonymous"))
&& ((user.user.username == post.author)
| (user
.level
.permissions
.contains(&String::from("EditBoardPosts"))));
if can_update == false {
return HttpResponse::NotFound()
.body("You do not have permission to manage this post's contents.");
}
// update content
post.content = body.content.clone();
post.content_html = format!(
// we'll add the "(edited)" tag to this post in its rendered content so it doesn't impact the markdown content
"{}<hr /><p style=\"opacity: 75%;\">&lpar;edited <span class=\"date-time-to-localize\">{}</span>&rpar;</p>",
crate::markdown::render::parse_markdown(&body.content),
crate::utility::unix_epoch_timestamp()
);
post.topic = body.topic.clone();
// ...
let res = data
.db
.edit_log(id, serde_json::to_string::<BoardPostLog>(&post).unwrap())
.await;
// update cache
if post.reply.is_some() {
// this doesn't change the number of posts so we only need to refresh ALL OFFSETS
// TODO: maybe do something to figure out the post offset if possible so we don't clear all offsets every time
data.db
.cachedb
.remove_starting_with(format!("post-replies:{}:*", post.reply.as_ref().unwrap()))
.await;
} else {
data.db
.cachedb
.remove_starting_with(format!("board-posts:{}:*", post.board))
.await;
}
// return
return HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))
.body(serde_json::to_string(&res).unwrap());
}
#[post("/api/board/{name:.*}/posts/{id:.*}/tags")]
pub async fn update_post_tags_request(
req: HttpRequest,
body: web::Json<UpdatePostTagsInfo>,
data: web::Data<AppData>,
) -> impl Responder {
let name: String = req.match_info().get("name").unwrap().to_string();
let id: String = req.match_info().get("id").unwrap().to_string();
// get token user
let token_cookie = req.cookie("__Secure-Token");
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
} else {
Option::None
};
if token_user.is_some() {
// make sure user exists
if token_user.as_ref().unwrap().success == false {
return HttpResponse::NotFound().body("Invalid token");
}
} else {
return HttpResponse::NotAcceptable().body("An account is required to do this");
}
// make sure board exists
let board: DefaultReturn<Option<Board<String>>> =
data.db.get_board_by_name(name.to_owned()).await;
if !board.success {
return HttpResponse::NotFound()
.append_header(("Content-Type", "application/json"))
.body(
serde_json::to_string::<DefaultReturn<Option<String>>>(&DefaultReturn {
success: false,
message: String::from("Board does not exist!"),
payload: Option::None,
})
.unwrap(),
);
}
let board = serde_json::from_str::<BoardMetadata>(&board.payload.unwrap().metadata).unwrap();
// get post
let p = data.db.get_log_by_id(id.to_owned()).await;
let mut post = serde_json::from_str::<BoardPostLog>(&p.payload.unwrap().content).unwrap();
// check if we can update this post
// must be authenticated AND post author OR staff OR board owner
let user = token_user.unwrap().payload.unwrap();
let can_update: bool = (user.user.username != String::from("Anonymous"))
&& ((user.user.username == board.owner)
| (user.user.username == post.author)
| (user
.level
.permissions
.contains(&String::from("ManageBoards"))));
if can_update == false {
return HttpResponse::NotFound()
.body("You do not have permission to manage this post's contents.");
}
// update tags
post.tags = Option::Some(body.tags.clone());
// ...
let res = data
.db
.edit_log(id, serde_json::to_string::<BoardPostLog>(&post).unwrap())
.await;
// return
return HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))
.body(serde_json::to_string(&res).unwrap());
}
#[delete("/api/board/{name:.*}/posts/{id:.*}")]
pub async fn delete_post_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
let name: String = req.match_info().get("name").unwrap().to_string();
let id: String = req.match_info().get("id").unwrap().to_string();
// get token user
let token_cookie = req.cookie("__Secure-Token");
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
} else {
Option::None
};
if token_user.is_some() {
// make sure user exists
if token_user.as_ref().unwrap().success == false {
return HttpResponse::NotFound().body("Invalid token");
}
} else {
return HttpResponse::NotAcceptable().body("An account is required to do this");
}
// make sure board exists
let board: DefaultReturn<Option<Board<String>>> =
data.db.get_board_by_name(name.to_owned()).await;
if !board.success {
return HttpResponse::NotFound()
.append_header(("Content-Type", "application/json"))
.body(
serde_json::to_string::<DefaultReturn<Option<String>>>(&DefaultReturn {
success: false,
message: String::from("Board does not exist!"),
payload: Option::None,
})
.unwrap(),
);
}
let board = serde_json::from_str::<BoardMetadata>(&board.payload.unwrap().metadata).unwrap();
// get post
let p = data.db.get_log_by_id(id.to_owned()).await;
if p.success == false {
return HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))
.body(serde_json::to_string(&p).unwrap());
}
let post = serde_json::from_str::<BoardPostLog>(&p.payload.unwrap().content).unwrap();
// check if we can delete this post
// must be authenticated AND board owner OR staff OR post author
let user = token_user.unwrap().payload.unwrap();
let can_delete: bool = (user.user.username != String::from("Anonymous"))
&& ((user.user.username == board.owner)
| (user
.level
.permissions
.contains(&String::from("ManageBoardPosts")))
| (user.user.username == post.author));
if can_delete == false {
return HttpResponse::NotFound()
.body("You do not have permission to manage this board's contents.");
}
// ...
let res = data.db.delete_log(id).await;
// update cache
if post.reply.is_some() {
data.db
.cachedb
.remove(format!("post-replies:{}", post.reply.as_ref().unwrap()))
.await;
data.db
.cachedb
.remove(format!(
"post-replies:{}:offset0",
post.reply.as_ref().unwrap()
))
.await;
// technically we should do that whole reply parent thing from create_board_post
// to update the number when viewing this post in a feed, but that's a waste of time and memory
} else {
data.db
.cachedb
.remove(format!("board-posts:{}", post.board))
.await;
data.db
.cachedb
.remove(format!("board-posts:{}:offset0", post.board))
.await;
}
// return
return HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))
.body(serde_json::to_string(&res).unwrap());
}
#[post("/api/board/{name:.*}/update")]
pub async fn metadata_request(
req: HttpRequest,
body: web::Json<BoardMetadata>,
data: web::Data<AppData>,
) -> impl Responder {
let name: String = req.match_info().get("name").unwrap().to_string();
// get board
let board: DefaultReturn<Option<Board<String>>> = data.db.get_board_by_name(name.clone()).await;
if board.success == false {
return HttpResponse::NotFound().body(board.message);
}
// get token user
let token_cookie = req.cookie("__Secure-Token");
let token_user = if token_cookie.is_some() {
Option::Some(
data.db
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
.await,
)
} else {
Option::None
};
if token_user.is_some() {
// make sure user exists
if token_user.as_ref().unwrap().success == false {
return