use crate::utility;
use serde::{Deserialize, Serialize};
use dorsal::query as sqlquery;
use std::collections::HashMap;
#[derive(Clone)]
pub struct AppData {
pub db: Database,
pub http_client: awc::Client,
}
pub use dorsal::db::special::auth_db::{
FullUser, RoleLevel, RoleLevelLog, UserMetadata, UserState,
};
pub use dorsal::db::special::log_db::{Log, LogIdentifier};
pub use dorsal::DefaultReturn;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DatabaseReturn {
pub data: HashMap<String, String>,
}
#[derive(Default, PartialEq, Clone, Serialize, Deserialize)]
pub struct Board<M> {
pub name: String,
pub timestamp: u128,
pub metadata: M,
}
#[derive(Default, Clone, Serialize, Deserialize, PartialEq)]
pub struct BoardMetadata {
pub owner: String, pub is_private: String, pub allow_anonymous: Option<String>, pub allow_open_posting: Option<String>, pub topic_required: Option<String>, pub about: Option<String>, pub tags: Option<String>, }
#[derive(Default, Clone, Serialize, Deserialize, PartialEq)]
pub struct BoardPostLog {
pub author: String, pub content: String,
pub content_html: String,
pub topic: Option<String>, pub board: String, pub is_hidden: bool, pub reply: Option<String>, pub pinned: Option<bool>, pub replies: Option<usize>, pub tags: Option<String>, }
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
pub struct BoardIdentifier {
pub name: String,
pub tags: String,
}
#[allow(dead_code)]
pub fn deserialize_post(input: Log) -> BoardPostLog {
serde_json::from_str::<BoardPostLog>(&input.content).unwrap()
}
#[derive(Clone)]
pub struct Database {
pub base: dorsal::StarterDatabase,
pub auth: dorsal::AuthDatabase,
pub logs: dorsal::LogDatabase,
}
impl Database {
pub async fn new(opts: dorsal::DatabaseOpts) -> Database {
let db = dorsal::StarterDatabase::new(opts).await;
Database {
base: db.clone(),
auth: dorsal::AuthDatabase { base: db.clone() },
logs: dorsal::LogDatabase { base: db },
}
}
pub async fn init(&self) {
let c = &self.base.db.client;
let _ = sqlquery(
"CREATE TABLE IF NOT EXISTS \"Boards\" (
name VARCHAR(1000000),
timestamp VARCHAR(1000000),
metadata VARCHAR(1000000)
)",
)
.execute(c)
.await;
let _ = sqlquery(
"CREATE TABLE IF NOT EXISTS \"Users\" (
username VARCHAR(1000000),
id_hashed VARCHAR(1000000),
role VARCHAR(1000000),
timestamp VARCHAR(1000000),
metadata VARCHAR(1000000)
)",
)
.execute(c)
.await;
let _ = sqlquery(
"CREATE TABLE IF NOT EXISTS \"Logs\" (
id VARCHAR(1000000),
logtype VARCHAR(1000000),
timestamp VARCHAR(1000000),
content VARCHAR(1000000)
)",
)
.execute(c)
.await;
}
pub async fn get_user_by_hashed(
&self,
hashed: String,
) -> DefaultReturn<Option<FullUser<String>>> {
self.auth.get_user_by_hashed(hashed).await
}
pub async fn get_user_by_unhashed(
&self,
unhashed: String,
) -> DefaultReturn<Option<FullUser<String>>> {
self.auth.get_user_by_unhashed(unhashed).await
}
pub async fn get_user_by_unhashed_st(
&self,
unhashed: String,
) -> DefaultReturn<Option<FullUser<String>>> {
self.auth.get_user_by_unhashed_st(unhashed).await
}
pub async fn get_user_by_username(
&self,
username: String,
) -> DefaultReturn<Option<FullUser<String>>> {
self.auth.get_user_by_username(username).await
}
pub async fn get_level_by_role(&self, name: String) -> DefaultReturn<RoleLevelLog> {
self.auth.get_level_by_role(name).await
}
pub async fn get_board_by_name(&self, url: String) -> DefaultReturn<Option<Board<String>>> {
let cached = self
.base
.cachedb
.get(format!("board:{}", url.to_lowercase()))
.await;
if cached.is_some() {
let board = serde_json::from_str::<Board<String>>(cached.unwrap().as_str()).unwrap();
return DefaultReturn {
success: true,
message: String::from("Board exists (cache)"),
payload: Option::Some(board),
};
}
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"SELECT * FROM \"Boards\" WHERE \"name\" = ?"
} else {
"SELECT * FROM \"Boards\" WHERE \"name\" = $1"
};
let c = &self.base.db.client;
let res = sqlquery(query)
.bind::<&String>(&url.to_lowercase())
.fetch_one(c)
.await;
if res.is_err() {
return DefaultReturn {
success: false,
message: String::from("Board does not exist"),
payload: Option::None,
};
}
let row = res.unwrap();
let row = self.base.textify_row(row).data;
let board = Board {
name: row.get("name").unwrap().to_string(),
timestamp: row.get("timestamp").unwrap().parse::<u128>().unwrap(),
metadata: row.get("metadata").unwrap().to_string(),
};
self.base
.cachedb
.set(
format!("board:{}", url.to_lowercase()),
serde_json::to_string::<Board<String>>(&board).unwrap(),
)
.await;
return DefaultReturn {
success: true,
message: String::from("Board exists (new)"),
payload: Option::Some(board),
};
}
pub async fn get_boards(
&self,
offset: Option<i32>,
) -> DefaultReturn<Option<Vec<BoardIdentifier>>> {
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"SELECT * FROM \"Boards\" WHERE \"metadata\" NOT LIKE '%\"is_private\":\"yes\"%' ORDER BY \"timestamp\" DESC LIMIT 50 OFFSET ?"
} else {
"SELECT * FROM \"Boards\" WHERE \"metadata\" NOT LIKE '%\"is_private\":\"yes\"%' ORDER BY \"timestamp\" DESC LIMIT 50 OFFSET $1"
};
let c = &self.base.db.client;
let res = sqlquery(query)
.bind(if offset.is_some() { offset.unwrap() } else { 0 })
.fetch_all(c)
.await;
if res.is_err() {
return DefaultReturn {
success: false,
message: String::from("Failed to fetch boards"),
payload: Option::None,
};
}
let rows = res.unwrap();
let mut output: Vec<BoardIdentifier> = Vec::new();
for row in rows {
let row = self.base.textify_row(row).data;
output.push(BoardIdentifier {
name: row.get("name").unwrap().to_string(),
tags: String::new(),
});
}
return DefaultReturn {
success: true,
message: String::from("Board exists"),
payload: Option::Some(output),
};
}
pub async fn get_boards_by_tags(
&self,
mut tags: String,
offset: Option<i32>,
) -> DefaultReturn<Option<Vec<BoardIdentifier>>> {
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"SELECT * FROM \"Boards\" WHERE \"metadata\" LIKE ? AND \"metadata\" NOT LIKE '%\"is_private\":\"yes\"%' ORDER BY \"timestamp\" DESC LIMIT 50 OFFSET ?"
} else {
"SELECT * FROM \"Boards\" WHERE \"metadata\" LIKE $1 AND \"metadata\" NOT LIKE '%\"is_private\":\"yes\"%' ORDER BY \"timestamp\" DESC LIMIT 50 OFFSET $2"
};
for tag in tags.clone().split(" ") {
if tag.starts_with("+") | (tag.len() == 0) {
continue;
}
tags = tags.replacen(tag, &format!("+{}", tag), 1);
}
let c = &self.base.db.client;
let res = sqlquery(query)
.bind::<&String>(&format!("%{tags}%"))
.bind(if offset.is_some() { offset.unwrap() } else { 0 })
.fetch_all(c)
.await;
if res.is_err() {
return DefaultReturn {
success: false,
message: String::from("Failed to fetch boards"),
payload: Option::None,
};
}
let rows = res.unwrap();
let mut output: Vec<BoardIdentifier> = Vec::new();
for row in rows {
let row = self.base.textify_row(row).data;
let metadata =
serde_json::from_str::<BoardMetadata>(&row.get("metadata").unwrap().to_string())
.unwrap();
output.push(BoardIdentifier {
name: row.get("name").unwrap().to_string(),
tags: if metadata.tags.is_some() {
metadata.tags.unwrap()
} else {
String::new()
},
});
}
return DefaultReturn {
success: true,
message: String::from("Board exists"),
payload: Option::Some(output),
};
}
pub async fn get_board_posts(
&self,
url: String,
offset: Option<i32>,
) -> DefaultReturn<Option<Vec<Log>>> {
let offset = if offset.is_some() { offset.unwrap() } else { 0 };
let existing: DefaultReturn<Option<Board<String>>> =
self.get_board_by_name(url.to_owned().to_lowercase()).await;
if existing.success == false {
return DefaultReturn {
success: false,
message: String::from("Board does not exist"),
payload: Option::None,
};
}
let cached = self
.base
.cachedb
.get(format!(
"board-posts:{}:offset{}",
url.to_lowercase(),
offset
))
.await;
if cached.is_some() {
let posts = serde_json::from_str::<Vec<Log>>(cached.unwrap().as_str()).unwrap();
return DefaultReturn {
success: true,
message: String::from("Successfully fetched posts"),
payload: Option::Some(posts),
};
}
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"SELECT * FROM \"Logs\" WHERE \"content\" LIKE ? AND \"content\" NOT LIKE '%\"reply\":\"%' ORDER BY \"timestamp\" DESC LIMIT 50 OFFSET ?"
} else {
"SELECT * FROM \"Logs\" WHERE \"content\" LIKE $1 AND \"content\" NOT LIKE '%\"reply\":\"%' ORDER BY \"timestamp\" DESC LIMIT 50 OFFSET $2"
};
let c = &self.base.db.client;
let res = sqlquery(query)
.bind::<&String>(&format!("%\"board\":\"{}\"%", url))
.bind(offset)
.fetch_all(c)
.await;
if res.is_err() {
return DefaultReturn {
success: false,
message: String::from("Failed to fetch posts"),
payload: Option::None,
};
}
let rows = res.unwrap();
let mut output: Vec<Log> = Vec::new();
for row in rows {
let row = self.base.textify_row(row).data;
output.push(Log {
id: row.get("id").unwrap().to_string(),
logtype: row.get("logtype").unwrap().to_string(),
timestamp: row.get("timestamp").unwrap().parse::<u128>().unwrap(),
content: row.get("content").unwrap().to_string(),
});
}
let mut true_output: Vec<Log> = Vec::new();
for mut post in output {
let mut parsed = serde_json::from_str::<BoardPostLog>(&post.content).unwrap();
let replies = &self.get_post_replies(post.clone().id, false).await;
if replies.payload.is_some() {
parsed.replies = Option::Some(replies.payload.as_ref().unwrap().len());
post.content = serde_json::to_string::<BoardPostLog>(&parsed).unwrap();
true_output.push(post);
continue;
}
continue;
}
self.base
.cachedb
.set(
format!("board-posts:{}:offset{}", url.to_lowercase(), offset),
serde_json::to_string::<Vec<Log>>(&true_output).unwrap(),
)
.await;
return DefaultReturn {
success: true,
message: String::from("Successfully fetched posts"),
payload: Option::Some(true_output),
};
}
pub async fn get_board_posts_by_tag(
&self,
url: String,
mut tags: String,
offset: Option<i32>,
) -> DefaultReturn<Option<Vec<Log>>> {
let existing: DefaultReturn<Option<Board<String>>> =
self.get_board_by_name(url.to_owned().to_lowercase()).await;
if existing.success == false {
return DefaultReturn {
success: false,
message: String::from("Board does not exist"),
payload: Option::None,
};
}
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"SELECT * FROM \"Logs\" WHERE \"content\" LIKE ? AND \"content\" LIKE ? AND \"content\" NOT LIKE '%\"reply\":\"%' ORDER BY \"timestamp\" DESC LIMIT 50 OFFSET ?"
} else {
"SELECT * FROM \"Logs\" WHERE \"content\" LIKE $1 AND \"content\" LIKE $2 AND \"content\" NOT LIKE '%\"reply\":\"%' ORDER BY \"timestamp\" DESC LIMIT 50 OFFSET $3"
};
for tag in tags.clone().split(" ") {
if tag.starts_with("+") | (tag.len() == 0) {
continue;
}
tags = tags.replacen(tag, &format!("+{}", tag), 1);
}
let c = &self.base.db.client;
let res = sqlquery(query)
.bind::<&String>(&format!("%\"board\":\"{}\"%", url))
.bind::<&String>(&format!("%{tags}%"))
.bind(if offset.is_some() { offset.unwrap() } else { 0 })
.fetch_all(c)
.await;
if res.is_err() {
return DefaultReturn {
success: false,
message: String::from("Failed to fetch posts"),
payload: Option::None,
};
}
let rows = res.unwrap();
let mut output: Vec<Log> = Vec::new();
for row in rows {
let row = self.base.textify_row(row).data;
output.push(Log {
id: row.get("id").unwrap().to_string(),
logtype: row.get("logtype").unwrap().to_string(),
timestamp: row.get("timestamp").unwrap().parse::<u128>().unwrap(),
content: row.get("content").unwrap().to_string(),
});
}
let mut true_output: Vec<Log> = Vec::new();
for mut post in output {
let mut parsed = serde_json::from_str::<BoardPostLog>(&post.content).unwrap();
let replies = &self.get_post_replies(post.clone().id, false).await;
if replies.payload.is_some() {
parsed.replies = Option::Some(replies.payload.as_ref().unwrap().len());
post.content = serde_json::to_string::<BoardPostLog>(&parsed).unwrap();
true_output.push(post);
continue;
}
continue;
}
return DefaultReturn {
success: true,
message: String::from("Successfully fetched posts"),
payload: Option::Some(true_output),
};
}
pub async fn get_pinned_board_posts(&self, url: String) -> DefaultReturn<Option<Vec<Log>>> {
let existing: DefaultReturn<Option<Board<String>>> =
self.get_board_by_name(url.to_owned().to_lowercase()).await;
if existing.success == false {
return DefaultReturn {
success: false,
message: String::from("Board does not exist"),
payload: Option::None,
};
}
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"SELECT * FROM \"Logs\" WHERE \"content\" LIKE ? AND \"content\" NOT LIKE '%\"reply\":\"%' AND \"content\" LIKE '%\"pinned\":true%' ORDER BY \"timestamp\" DESC LIMIT 50"
} else {
"SELECT * FROM \"Logs\" WHERE \"content\" LIKE $1 AND \"content\" NOT LIKE '%\"reply\":\"%' AND \"content\" LIKE '%\"pinned\":true%' ORDER BY \"timestamp\" DESC LIMIT 50"
};
let c = &self.base.db.client;
let res = sqlquery(query)
.bind::<&String>(&format!("%\"board\":\"{}\"%", url))
.fetch_all(c)
.await;
if res.is_err() {
return DefaultReturn {
success: false,
message: String::from("Failed to fetch posts"),
payload: Option::None,
};
}
let rows = res.unwrap();
let mut output: Vec<Log> = Vec::new();
for row in rows {
let row = self.base.textify_row(row).data;
output.push(Log {
id: row.get("id").unwrap().to_string(),
logtype: row.get("logtype").unwrap().to_string(),
timestamp: row.get("timestamp").unwrap().parse::<u128>().unwrap(),
content: row.get("content").unwrap().to_string(),
});
}
return DefaultReturn {
success: true,
message: String::from("Successfully fetched posts (pinned)"),
payload: Option::Some(output),
};
}
pub async fn get_post_replies_limited(
&self,
id: String,
run_existing_check: bool,
offset: Option<i32>,
) -> DefaultReturn<Option<Vec<Log>>> {
let offset = if offset.is_some() { offset.unwrap() } else { 0 };
if run_existing_check != false {
let existing: DefaultReturn<Option<Log>> = self.logs.get_log_by_id(id.to_owned()).await;
if existing.success == false {
return DefaultReturn {
success: false,
message: String::from("Post does not exist"),
payload: Option::None,
};
}
}
let cached = self
.base
.cachedb
.get(format!("post-replies:{}:offset{}", id, offset))
.await;
if cached.is_some() {
let posts = serde_json::from_str::<Vec<Log>>(cached.unwrap().as_str()).unwrap();
return DefaultReturn {
success: true,
message: String::from("Successfully fetched replies (limited)"),
payload: Option::Some(posts),
};
}
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"SELECT * FROM \"Logs\" WHERE \"content\" LIKE ? ORDER BY \"timestamp\" DESC LIMIT 50 OFFSET ?"
} else {
"SELECT * FROM \"Logs\" WHERE \"content\" LIKE $1 ORDER BY \"timestamp\" DESC LIMIT 50 OFFSET $2"
};
let c = &self.base.db.client;
let res = sqlquery(query)
.bind::<&String>(&format!("%\"reply\":\"{}\"%", id))
.bind(offset)
.fetch_all(c)
.await;
if res.is_err() {
return DefaultReturn {
success: false,
message: String::from("Failed to fetch posts"),
payload: Option::None,
};
}
let rows = res.unwrap();
let mut output: Vec<Log> = Vec::new();
for row in rows {
let row = self.base.textify_row(row).data;
output.push(Log {
id: row.get("id").unwrap().to_string(),
logtype: row.get("logtype").unwrap().to_string(),
timestamp: row.get("timestamp").unwrap().parse::<u128>().unwrap(),
content: row.get("content").unwrap().to_string(),
});
}
let mut true_output: Vec<Log> = Vec::new();
for mut post in output {
let mut parsed = serde_json::from_str::<BoardPostLog>(&post.content).unwrap();
let replies = &self.get_post_replies(post.clone().id, false).await;
if replies.payload.is_some() {
parsed.replies = Option::Some(replies.payload.as_ref().unwrap().len());
post.content = serde_json::to_string::<BoardPostLog>(&parsed).unwrap();
true_output.push(post);
continue;
}
continue;
}
self.base
.cachedb
.set(
format!("post-replies:{}:offset{}", id, offset),
serde_json::to_string::<Vec<Log>>(&true_output).unwrap(),
)
.await;
return DefaultReturn {
success: true,
message: String::from("Successfully fetched replies (limited)"),
payload: Option::Some(true_output),
};
}
pub async fn get_post_replies(
&self,
id: String,
run_existing_check: bool,
) -> DefaultReturn<Option<Vec<LogIdentifier>>> {
if run_existing_check != false {
let existing: DefaultReturn<Option<Log>> = self.logs.get_log_by_id(id.to_owned()).await;
if existing.success == false {
return DefaultReturn {
success: false,
message: String::from("Post does not exist"),
payload: Option::None,
};
}
}
let cached = self.base.cachedb.get(format!("post-replies:{}", id)).await;
if cached.is_some() {
let posts =
serde_json::from_str::<Vec<LogIdentifier>>(cached.unwrap().as_str()).unwrap();
return DefaultReturn {
success: true,
message: String::from("Successfully fetched posts (limited)"),
payload: Option::Some(posts),
};
}
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"SELECT \"ID\" FROM \"Logs\" WHERE \"content\" LIKE ?"
} else {
"SELECT \"ID\" FROM \"Logs\" WHERE \"content\" LIKE $1"
};
let c = &self.base.db.client;
let res = sqlquery(query)
.bind::<&String>(&format!("%\"reply\":\"{}\"%", id))
.fetch_all(c)
.await;
if res.is_err() {
return DefaultReturn {
success: false,
message: String::from("Failed to fetch replies"),
payload: Option::None,
};
}
let rows = res.unwrap();
let mut output: Vec<LogIdentifier> = Vec::new();
for row in rows {
let row = self.base.textify_row(row).data;
output.push(LogIdentifier {
id: row.get("id").unwrap_or(&String::new()).to_string(),
});
}
self.base
.cachedb
.set(
format!("post-replies:{}", id),
serde_json::to_string::<Vec<LogIdentifier>>(&output).unwrap(),
)
.await;
return DefaultReturn {
success: true,
message: String::from("Successfully fetched replies"),
payload: Option::Some(output),
};
}
pub async fn get_boards_by_owner(
&self,
owner: String,
) -> DefaultReturn<Option<Vec<BoardIdentifier>>> {
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"SELECT * FROM \"Boards\" WHERE \"metadata\" LIKE ?"
} else {
"SELECT * FROM \"Boards\" WHERE \"metadata\" LIKE $1"
};
let c = &self.base.db.client;
let res = sqlquery(query)
.bind::<&String>(&format!("%\"owner\":\"{}\"%", &owner))
.fetch_all(c)
.await;
if res.is_err() {
return DefaultReturn {
success: false,
message: String::from(res.err().unwrap().to_string()),
payload: Option::None,
};
}
let mut full_res: Vec<BoardIdentifier> = Vec::new();
for row in res.unwrap() {
let row = self.base.textify_row(row).data;
full_res.push(BoardIdentifier {
name: row.get("name").unwrap().to_string(),
tags: String::new(),
});
}
return DefaultReturn {
success: true,
message: owner,
payload: Option::Some(full_res),
};
}
pub async fn fetch_most_recent_posts(
&self,
offset: Option<i32>,
) -> DefaultReturn<Option<Vec<Log>>> {
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"SELECT * FROM \"Logs\" WHERE \"logtype\" = 'board_post' ORDER BY \"timestamp\" DESC LIMIT 50 OFFSET ?"
} else {
"SELECT * FROM \"Logs\" WHERE \"logtype\" = 'board_post' ORDER BY \"timestamp\" DESC LIMIT 50 OFFSET $1"
};
let c = &self.base.db.client;
let res = sqlquery(query)
.bind(if offset.is_some() { offset.unwrap() } else { 0 })
.fetch_all(c)
.await;
if res.is_err() {
return DefaultReturn {
success: false,
message: String::from("Failed to fetch posts"),
payload: Option::None,
};
}
let rows = res.unwrap();
let mut output: Vec<Log> = Vec::new();
for row in rows {
let row = self.base.textify_row(row).data;
output.push(Log {
id: row.get("id").unwrap().to_string(),
logtype: row.get("logtype").unwrap().to_string(),
timestamp: row.get("timestamp").unwrap().parse::<u128>().unwrap(),
content: row.get("content").unwrap().to_string(),
});
}
return DefaultReturn {
success: true,
message: String::from("Successfully fetched posts"),
payload: Option::Some(output),
};
}
pub async fn get_user_posts_count(&self, user: String) -> DefaultReturn<usize> {
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"SELECT * FROM \"Logs\" WHERE \"content\" LIKE ? AND \"logtype\" = 'board_post'"
} else {
"SELECT * FROM \"Logs\" WHERE \"content\" LIKE $1 AND \"logtype\" = 'board_post'"
};
let c = &self.base.db.client;
let res = sqlquery(query)
.bind::<&String>(&format!("%\"author\":\"{user}\"%"))
.fetch_all(c)
.await;
if res.is_err() {
return DefaultReturn {
success: false,
message: String::from("Failed to fetch posts"),
payload: 0,
};
}
let rows = res.unwrap();
return DefaultReturn {
success: true,
message: String::from("Posts exist"),
payload: rows.len(),
};
}
pub async fn create_board(
&self,
props: &mut Board<String>,
as_user: Option<String>, ) -> DefaultReturn<Option<Board<String>>> {
let p: &mut Board<String> = props; let metadata: BoardMetadata = BoardMetadata {
owner: as_user.clone().unwrap(),
is_private: String::from("no"), allow_anonymous: Option::Some(String::from("yes")),
allow_open_posting: Option::Some(String::from("yes")),
topic_required: Option::Some(String::from("no")),
about: Option::None,
tags: Option::None,
};
if (p.name.len() < 2) | (p.name.len() > 250) {
return DefaultReturn {
success: false,
message: String::from("Name is invalid"),
payload: Option::None,
};
}
let regex = regex::RegexBuilder::new("^[\\w\\_\\-\\.\\!]+$")
.multi_line(true)
.build()
.unwrap();
if regex.captures(&p.name).iter().len() < 1 {
return DefaultReturn {
success: false,
message: String::from("Name is invalid"),
payload: Option::None,
};
}
let existing: DefaultReturn<Option<Board<String>>> = self
.get_board_by_name(p.name.to_owned().to_lowercase())
.await;
if existing.success {
return DefaultReturn {
success: false,
message: String::from("Board already exists!"),
payload: Option::None,
};
}
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"INSERT INTO \"Boards\" VALUES (?, ?, ?)"
} else {
"INSERT INTO \"Boards\" VALUES ($1, $2, $3)"
};
let c = &self.base.db.client;
let p: &mut Board<String> = &mut props.clone();
p.timestamp = utility::unix_epoch_timestamp();
let res = sqlquery(query)
.bind::<&String>(&p.name)
.bind::<&String>(&p.timestamp.to_string())
.bind::<&String>(&serde_json::to_string(&metadata).unwrap())
.execute(c)
.await;
if res.is_err() {
return DefaultReturn {
success: false,
message: res.err().unwrap().to_string(),
payload: Option::None,
};
}
return DefaultReturn {
success: true,
message: String::from("Created board"),
payload: Option::Some(p.to_owned()),
};
}
pub async fn create_board_post(
&self,
props: &mut BoardPostLog,
as_user: Option<String>, as_role: Option<RoleLevel>, ) -> DefaultReturn<Option<String>> {
let p: &mut BoardPostLog = props; if (p.content.len() < 2) | (p.content.len() > 1_000) {
return DefaultReturn {
success: false,
message: String::from("Content is invalid"),
payload: Option::None,
};
}
let existing: DefaultReturn<Option<Board<String>>> = self
.get_board_by_name(p.board.to_owned().to_lowercase())
.await;
if !existing.success {
return DefaultReturn {
success: false,
message: String::from("Board does not exist!"),
payload: Option::None,
};
}
let board =
serde_json::from_str::<BoardMetadata>(&existing.payload.unwrap().metadata).unwrap();
if board.topic_required.is_some()
&& board.topic_required.unwrap() == "yes"
&& p.reply.is_none()
&& p.topic.is_none()
{
return DefaultReturn {
success: false,
message: String::from("This board requires a topic to be set before posting"),
payload: Option::None,
};
}
if board.allow_anonymous.is_some()
&& board.allow_anonymous.unwrap() == "no"
&& as_user.is_none()
{
return DefaultReturn {
success: false,
message: String::from("An account is required to do this"),
payload: Option::None,
};
}
if board.allow_open_posting.is_some()
&& board.allow_open_posting.unwrap() == String::from("no")
{
let can_post = as_user.is_some()
&& ((as_user.as_ref().unwrap() == &board.owner)
| (as_role
.unwrap()
.permissions
.contains(&String::from("ManageBoards"))));
if can_post == false {
return DefaultReturn {
success: false,
message: String::from("You do not have permission to do this"),
payload: Option::None,
};
}
}
let post = BoardPostLog {
author: if as_user.is_some() {
as_user.unwrap()
} else {
String::from("Anonymous")
},
content: p.content.clone(),
content_html: crate::markup::render(&p.content),
topic: p.topic.clone(),
board: p.board.clone().to_lowercase(),
is_hidden: false,
reply: p.reply.clone(),
pinned: Option::Some(false),
replies: Option::None,
tags: Option::None,
};
if p.reply.is_none() {
self.base
.cachedb
.remove(format!("board-posts:{}:offset0", p.board.to_lowercase()))
.await;
self.base
.cachedb
.remove(format!("board-posts:{}", p.board.to_lowercase()))
.await;
} else {
self.base
.cachedb
.remove(format!("post-replies:{}", p.reply.as_ref().unwrap()))
.await;
self.base
.cachedb
.remove(format!(
"post-replies:{}:offset0",
p.reply.as_ref().unwrap()
))
.await;
let replying_to = self
.logs
.get_log_by_id(p.reply.as_ref().unwrap().to_string())
.await;
let replying_to =
serde_json::from_str::<BoardPostLog>(&replying_to.payload.unwrap().content)
.unwrap();
if replying_to.reply.is_some() {
self.base
.cachedb
.remove(format!(
"post-replies:{}:offset0",
replying_to.reply.unwrap()
))
.await;
} else {
self.base
.cachedb
.remove(format!("board-posts:{}:offset0", p.board.to_lowercase()))
.await;
}
}
self.logs
.create_log(
String::from("board_post"),
serde_json::to_string::<BoardPostLog>(&post).unwrap(),
)
.await
}
pub async fn edit_board_metadata_by_name(
&self,
name: String,
metadata: BoardMetadata,
edit_as: Option<String>, ) -> DefaultReturn<Option<String>> {
let existing = &self.get_board_by_name(name.clone()).await;
if !existing.success {
return DefaultReturn {
success: false,
message: String::from("Board does not exist!"),
payload: Option::None,
};
}
let ua = if edit_as.is_some() {
Option::Some(
self.get_user_by_username(edit_as.clone().unwrap())
.await
.payload,
)
} else {
Option::None
};
if ua.is_none() {
return DefaultReturn {
success: false,
message: String::from("An account is required to do this"),
payload: Option::None,
};
}
let user = ua.unwrap().unwrap();
let can_edit: bool =
(user.user.username == metadata.owner) | (user.level.permissions.contains(&String::from("ManageBoards")));
if can_edit == false {
return DefaultReturn {
success: false,
message: String::from(
"You do not have permission to manage this board's contents.",
),
payload: Option::None,
};
}
if metadata.about.is_some()
&& metadata
.about
.as_ref()
.unwrap()
.contains("\"_is_user_mail_stream\":true")
{
return DefaultReturn {
success: false,
message: String::from("Cannot convert this board into a user mail stream."),
payload: Option::None,
};
}
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"UPDATE \"Boards\" SET \"metadata\" = ? WHERE \"name\" = ?"
} else {
"UPDATE \"Boards\" SET (\"metadata\") = ($1) WHERE \"name\" = $2"
};
let c = &self.base.db.client;
let metadata = serde_json::to_string(&metadata).unwrap();
let res = sqlquery(query)
.bind::<&String>(&metadata)
.bind::<&String>(&name)
.execute(c)
.await;
if res.is_err() {
return DefaultReturn {
success: false,
message: String::from(res.err().unwrap().to_string()),
payload: Option::None,
};
}
let existing_in_cache = self.base.cachedb.get(format!("board:{}", name)).await;
if existing_in_cache.is_some() {
let mut board =
serde_json::from_str::<Board<String>>(&existing_in_cache.unwrap()).unwrap();
board.metadata = metadata; self.base
.cachedb
.update(
format!("board:{}", name),
serde_json::to_string::<Board<String>>(&board).unwrap(),
)
.await;
}
return DefaultReturn {
success: true,
message: String::from("Board updated!"),
payload: Option::Some(name),
};
}
pub async fn delete_board(&self, name: String) -> DefaultReturn<Option<String>> {
let existing = &self.get_board_by_name(name.clone()).await;
if !existing.success {
return DefaultReturn {
success: false,
message: String::from("Board does not exist!"),
payload: Option::None,
};
}
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"DELETE FROM \"Boards\" WHERE \"name\" = ?"
} else {
"DELETE FROM \"Boards\" WHERE \"name\" = $1"
};
let c = &self.base.db.client;
let res = sqlquery(query).bind::<&String>(&name).execute(c).await;
if res.is_err() {
return DefaultReturn {
success: false,
message: String::from(res.err().unwrap().to_string()),
payload: Option::None,
};
}
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"DELETE FROM \"Logs\" WHERE \"content\" LIKE ?"
} else {
"DELETE FROM \"Logs\" WHERE \"content\" LIKE $1"
};
let c = &self.base.db.client;
let res = sqlquery(query)
.bind::<&String>(&format!("%\"board\":\"{}\"%", name))
.execute(c)
.await;
if res.is_err() {
return DefaultReturn {
success: false,
message: String::from(res.err().unwrap().to_string()),
payload: Option::None,
};
}
self.base.cachedb.remove(format!("board:{}", name)).await;
return DefaultReturn {
success: true,
message: String::from("Board deleted!"),
payload: Option::Some(name),
};
}
}