use std::collections::HashMap;
use dorsal::utility;
use serde::{Deserialize, Serialize};
use dorsal::query as sqlquery;
#[derive(Clone)]
pub struct AppData {
pub db: Database,
pub http_client: awc::Client,
}
pub use dorsal::db::special::auth_db::{FullUser, UserMetadata, UserState};
pub use dorsal::db::special::log_db::Log;
pub use dorsal::DefaultReturn;
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
pub struct Paste<M> {
pub custom_url: String,
pub id: String,
pub group_name: String,
pub edit_password: String,
pub pub_date: u128,
pub edit_date: u128,
pub content: String,
pub content_html: String, pub metadata: M,
pub views: usize,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
pub struct PasteIdentifier {
pub custom_url: String,
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum PastePermissionLevel {
Normal,
EditTextPasswordless,
Passwordless,
Blocked, }
impl Default for PastePermissionLevel {
fn default() -> Self {
PastePermissionLevel::Normal
}
}
pub type PastePermissions = HashMap<String, PastePermissionLevel>;
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PasteMetadata {
pub owner: String,
pub private_source: String,
#[serde(default = "default_paste_permissions")]
pub permissions_list: PastePermissions,
pub title: Option<String>,
pub description: Option<String>,
pub favicon: Option<String>,
pub embed_color: Option<String>,
pub view_password: Option<String>,
pub page_template: Option<String>, }
fn default_paste_permissions() -> PastePermissions {
let permissions: PastePermissions = HashMap::new();
permissions
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
pub struct FullPaste<M, U> {
pub paste: Paste<M>,
pub user: Option<FullUser<U>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AtomicPaste {
pub _is_atomic: bool, pub files: Vec<AtomicPasteFSFile>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AtomicPasteFSFile {
pub path: String,
pub content: String,
}
#[derive(Default, PartialEq, Clone, Serialize, Deserialize)]
pub struct Group<M> {
pub name: String,
pub submit_password: String,
pub metadata: M, }
#[derive(Clone, Serialize, Deserialize)]
pub struct GroupMetadata {
pub owner: 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>, }
pub fn derserialize_post(post: &String) -> BoardPostLog {
serde_json::from_str::<BoardPostLog>(post).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 \"Pastes\" (
custom_url VARCHAR(1000000),
id VARCHAR(1000000),
group_name VARCHAR(1000000),
edit_password VARCHAR(1000000),
pub_date VARCHAR(1000000),
edit_date VARCHAR(1000000),
content VARCHAR(1000000),
content_html VARCHAR(1000000),
metadata VARCHAR(1000000)
)",
)
.execute(c)
.await;
let _ = sqlquery(
"CREATE TABLE IF NOT EXISTS \"Groups\" (
name VARCHAR(1000000),
submit_password 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;
let _ = sqlquery(
"CREATE TABLE IF NOT EXISTS \"Boards\" (
name VARCHAR(1000000),
timestamp VARCHAR(1000000),
metadata VARCHAR(1000000)
)",
)
.execute(c)
.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_username(
&self,
username: String,
) -> DefaultReturn<Option<FullUser<String>>> {
self.auth.get_user_by_username(username).await
}
pub async fn ban_user_by_name(&self, name: String) -> DefaultReturn<Option<String>> {
let existing = &self.get_user_by_username(name.clone()).await;
if !existing.success {
return DefaultReturn {
success: false,
message: String::from("User does not exist!"),
payload: Option::None,
};
}
let user = &existing.payload.as_ref().unwrap().user;
if user.role != "member" {
return DefaultReturn {
success: false,
message: String::from("User must be of role \"member\""),
payload: Option::None,
};
}
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"UPDATE \"Users\" SET \"role\" = ? WHERE \"username\" = ?"
} else {
"UPDATE \"Users\" SET (\"role\") = ($1) WHERE \"username\" = $2"
};
let c = &self.base.db.client;
let res = sqlquery(query)
.bind::<&str>("banned")
.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") {
"UPDATE \"Pastes\" SET \"metadata\" = ? WHERE \"metadata\" LIKE ?"
} else {
"UPDATE \"Pastes\" SET (\"metadata\") = ($1) WHERE \"metadata\" LIKE $2"
};
let c = &self.base.db.client;
let res = sqlquery(query)
.bind::<&String>(
&serde_json::to_string::<PasteMetadata>(&PasteMetadata {
owner: String::new(),
private_source: String::from("on"),
permissions_list: HashMap::new(),
title: Option::Some(String::new()),
description: Option::Some(String::new()),
favicon: Option::None,
embed_color: Option::None,
view_password: Option::Some(format!(
"LOCKED(USER_BANNED)-{}",
utility::random_id()
)),
page_template: Option::None,
})
.unwrap(),
)
.bind::<&String>(&format!("%\"owner\":\"{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!("user:{}", name)).await;
if existing_in_cache.is_some() {
let mut user =
serde_json::from_str::<UserState<String>>(&existing_in_cache.unwrap()).unwrap();
user.role = String::from("banned"); self.base
.cachedb
.update(
format!("user:{}", name),
serde_json::to_string::<UserState<String>>(&user).unwrap(),
)
.await;
}
return DefaultReturn {
success: true,
message: String::from("User banned!"),
payload: Option::Some(name),
};
}
async fn count_paste_views(&self, custom_url: String) -> usize {
let c = &self.base.db.client;
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"SELECT \"ID\" FROM \"Logs\" WHERE \"logtype\" = 'view_paste' AND \"content\" LIKE ?"
} else {
"SELECT \"ID\" FROM \"Logs\" WHERE \"logtype\" = 'view_paste' AND \"content\" LIKE $1"
};
let views_res = sqlquery(query)
.bind::<&String>(&format!("{}::%", &custom_url))
.fetch_all(c)
.await;
if views_res.is_err() {
return 0;
}
return views_res.unwrap().len();
}
async fn build_result_from_query(
&self,
query: &str,
selector: &str,
) -> DefaultReturn<Option<FullPaste<PasteMetadata, String>>> {
let cached = self.base.cachedb.get(format!("paste:{}", selector)).await;
if cached.is_some() {
let paste =
serde_json::from_str::<Paste<PasteMetadata>>(cached.unwrap().as_str()).unwrap();
let user = if paste.metadata.owner.len() > 0 {
(self
.get_user_by_username(paste.clone().metadata.owner)
.await)
.payload
} else {
Option::None
};
return DefaultReturn {
success: true,
message: String::from("Paste exists (cache)"),
payload: Option::Some(FullPaste { paste, user }),
};
}
let c = &self.base.db.client;
let res = sqlquery(query)
.bind::<&String>(&selector.to_lowercase())
.fetch_one(c)
.await;
if res.is_err() {
return DefaultReturn {
success: false,
message: String::from("Paste does not exist"),
payload: Option::None,
};
}
let row = res.unwrap();
let row = self.base.textify_row(row).data;
let views = &self
.count_paste_views(row.get("custom_url").unwrap().to_owned())
.await;
let metadata = serde_json::from_str::<PasteMetadata>(row.get("metadata").unwrap()).unwrap();
let paste = Paste {
custom_url: row.get("custom_url").unwrap().to_string(),
id: row.get("id").unwrap().to_string(),
group_name: row.get("group_name").unwrap().to_string(),
edit_password: row.get("edit_password").unwrap().to_string(),
pub_date: row.get("pub_date").unwrap().parse::<u128>().unwrap(),
edit_date: row.get("edit_date").unwrap().parse::<u128>().unwrap(),
content: row.get("content").unwrap().to_string(),
content_html: row.get("content_html").unwrap().to_string(),
metadata,
views: views.to_owned(),
};
self.base
.cachedb
.set(
format!("paste:{}", paste.custom_url),
serde_json::to_string::<Paste<PasteMetadata>>(&paste).unwrap(),
)
.await;
let user = if paste.metadata.owner.len() > 0 {
(self
.get_user_by_username(paste.clone().metadata.owner)
.await)
.payload
} else {
Option::None
};
return DefaultReturn {
success: true,
message: String::from("Paste exists (new)"),
payload: Option::Some(FullPaste { paste, user }),
};
}
pub async fn get_paste_by_url(
&self,
mut url: String,
) -> DefaultReturn<Option<FullPaste<PasteMetadata, String>>> {
url = idna::punycode::encode_str(&url).unwrap();
if url.ends_with("-") {
url.pop();
}
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"SELECT * FROM \"Pastes\" WHERE \"custom_url\" = ?"
} else {
"SELECT * FROM \"Pastes\" WHERE \"custom_url\" = $1"
};
return self.build_result_from_query(query, &url).await;
}
pub async fn get_paste_by_id(
&self,
id: String,
) -> DefaultReturn<Option<FullPaste<PasteMetadata, String>>> {
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"SELECT * FROM \"Pastes\" WHERE \"id\" = ?"
} else {
"SELECT * FROM \"Pastes\" WHERE \"id\" = $1"
};
return self.build_result_from_query(query, &id).await;
}
pub async fn get_pastes_by_owner_limited(
&self,
owner: String,
offset: Option<i32>,
) -> DefaultReturn<Option<Vec<PasteIdentifier>>> {
let offset = if offset.is_some() { offset.unwrap() } else { 0 };
let cached = self
.base
.cachedb
.get(format!("pastes-by-owner:{}:offset{}", owner, offset))
.await;
if cached.is_some() {
let pastes =
serde_json::from_str::<Vec<PasteIdentifier>>(cached.unwrap().as_str()).unwrap();
return DefaultReturn {
success: true,
message: owner,
payload: Option::Some(pastes),
};
}
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"SELECT * FROM \"Pastes\" WHERE \"metadata\" LIKE ? ORDER BY \"pub_date\" DESC LIMIT 50 OFFSET ?"
} else {
"SELECT * FROM \"Pastes\" WHERE \"metadata\" LIKE $1 ORDER BY \"pub_date\" DESC LIMIT 50 OFFSET $2"
};
let c = &self.base.db.client;
let res = sqlquery(query)
.bind::<&String>(&format!("%\"owner\":\"{}\"%", &owner))
.bind(offset)
.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<PasteIdentifier> = Vec::new();
for row in res.unwrap() {
let row = self.base.textify_row(row).data;
full_res.push(PasteIdentifier {
custom_url: row.get("custom_url").unwrap().to_string(),
id: row.get("id").unwrap().to_string(),
});
}
self.base
.cachedb
.set(
format!("pastes-by-owner:{}:offset{}", owner, offset),
serde_json::to_string::<Vec<PasteIdentifier>>(&full_res).unwrap(),
)
.await;
return DefaultReturn {
success: true,
message: owner,
payload: Option::Some(full_res),
};
}
pub async fn get_all_pastes_limited(
&self,
offset: Option<i32>,
) -> DefaultReturn<Option<Vec<PasteIdentifier>>> {
let offset = if offset.is_some() { offset.unwrap() } else { 0 };
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"SELECT * FROM \"Pastes\" ORDER BY \"pub_date\" DESC LIMIT 50 OFFSET ?"
} else {
"SELECT * FROM \"Pastes\" ORDER BY \"pub_date\" DESC LIMIT 50 OFFSET $1"
};
let c = &self.base.db.client;
let res = sqlquery(query)
.bind(offset)
.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<PasteIdentifier> = Vec::new();
for row in res.unwrap() {
let row = self.base.textify_row(row).data;
full_res.push(PasteIdentifier {
custom_url: row.get("custom_url").unwrap().to_string(),
id: row.get("id").unwrap().to_string(),
});
}
return DefaultReturn {
success: true,
message: String::new(),
payload: Option::Some(full_res),
};
}
pub async fn get_all_pastes_by_content_limited(
&self,
content: String,
offset: Option<i32>,
) -> DefaultReturn<Option<Vec<PasteIdentifier>>> {
let offset = if offset.is_some() { offset.unwrap() } else { 0 };
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"SELECT * FROM \"Pastes\" WHERE \"content\" LIKE ? ORDER BY \"pub_date\" DESC LIMIT 50 OFFSET ?"
} else {
"SELECT * FROM \"Pastes\" WHERE \"content\" LIKE $1 ORDER BY \"pub_date\" DESC LIMIT 50 OFFSET $2"
};
let c = &self.base.db.client;
let res = sqlquery(query)
.bind(format!("%{content}%"))
.bind(offset)
.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<PasteIdentifier> = Vec::new();
for row in res.unwrap() {
let row = self.base.textify_row(row).data;
full_res.push(PasteIdentifier {
custom_url: row.get("custom_url").unwrap().to_string(),
id: row.get("id").unwrap().to_string(),
});
}
return DefaultReturn {
success: true,
message: String::new(),
payload: Option::Some(full_res),
};
}
pub async fn create_paste(
&self,
props: &mut Paste<String>,
as_user: Option<String>, ) -> DefaultReturn<Option<Paste<String>>> {
let p: &mut Paste<String> = props; let metadata: PasteMetadata = PasteMetadata {
owner: if as_user.is_some() {
as_user.clone().unwrap()
} else {
String::new()
},
private_source: String::from("off"),
permissions_list: default_paste_permissions(),
title: Option::Some(String::new()),
description: Option::Some(String::new()),
favicon: Option::None,
embed_color: Option::Some(String::from("#ff9999")),
view_password: Option::None,
page_template: Option::None,
};
if p.custom_url.is_empty() {
p.custom_url = utility::random_id().chars().take(10).collect();
}
if p.edit_password.is_empty() {
p.edit_password = utility::random_id().chars().take(10).collect();
}
if (p.custom_url.len() < 2) | (p.custom_url.len() > 500) {
return DefaultReturn {
success: false,
message: String::from("Custom URL is invalid"),
payload: Option::None,
};
}
if !p.group_name.is_empty() && (p.group_name.len() < 2) | (p.group_name.len() > 500) {
return DefaultReturn {
success: false,
message: String::from("Group Name is invalid"),
payload: Option::None,
};
}
if (p.content.len() < 1) | (p.content.len() > 400_000) {
return DefaultReturn {
success: false,
message: String::from("Content is invalid"),
payload: Option::None,
};
}
if ["dashboard", "api", "public", "static"].contains(&p.custom_url.as_str()) {
return DefaultReturn {
success: false,
message: String::from("Custom URL is invalid"),
payload: Option::None,
};
}
let regex = regex::RegexBuilder::new("^[\\w\\_\\-\\.\\!\\p{Extended_Pictographic}]+$")
.multi_line(true)
.build()
.unwrap();
if regex.captures(&p.custom_url).iter().len() < 1 {
return DefaultReturn {
success: false,
message: String::from("Custom URL is invalid"),
payload: Option::None,
};
}
if !p.group_name.is_empty() {
let n = &p.group_name;
let e = &p.edit_password;
let o = &p.custom_url;
let existing_group = self.get_group_by_name(n.to_string()).await;
if !existing_group.success {
let res = self
.create_group(Group {
name: n.to_string(),
submit_password: e.to_string(), metadata: GroupMetadata {
owner: metadata.clone().owner,
},
})
.await;
if !res.success {
return DefaultReturn {
success: false,
message: res.message,
payload: Option::None,
};
}
} else {
if utility::hash(e.to_string()) != existing_group.payload.unwrap().submit_password {
return DefaultReturn {
success: false,
message: String::from("The paste edit password must match the group submit password during creation."),
payload: Option::None,
};
}
}
p.custom_url = format!("{}/{}", n, o);
}
let existing: DefaultReturn<Option<FullPaste<PasteMetadata, String>>> =
self.get_paste_by_url(p.custom_url.to_owned()).await;
if existing.success | existing.payload.is_some() {
return DefaultReturn {
success: false,
message: String::from("Paste already exists!"),
payload: Option::None,
};
}
p.custom_url = idna::punycode::encode_str(&p.custom_url).unwrap();
if p.custom_url.ends_with("-") {
p.custom_url.pop();
}
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"INSERT INTO \"Pastes\" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
} else {
"INSERT INTO \"Pastes\" VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)"
};
let c = &self.base.db.client;
let p: &mut Paste<String> = &mut props.clone();
p.id = utility::random_id();
let edit_password = &p.edit_password;
let edit_password_hash = utility::hash(edit_password.to_string());
let edit_date = &p.edit_date;
let pub_date = &p.pub_date;
let res = sqlquery(query)
.bind::<&String>(&p.custom_url)
.bind::<&String>(&p.id)
.bind::<&String>(&p.group_name)
.bind::<&String>(&edit_password_hash)
.bind::<&String>(&pub_date.to_string())
.bind::<&String>(&edit_date.to_string())
.bind::<&String>(&p.content)
.bind::<&String>(&p.content_html)
.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,
};
}
if as_user.is_some() {
self.base
.cachedb
.remove_starting_with(format!("pastes-by-owner:{}*", as_user.unwrap()))
.await;
}
let pass = &p.edit_password;
return DefaultReturn {
success: true,
message: pass.to_string(),
payload: Option::Some(p.to_owned()),
};
}
pub async fn edit_paste_by_url(
&self,
mut url: String,
content: String,
edit_password: String,
new_url: Option<String>,
new_edit_password: Option<String>,
edit_as: Option<String>, ) -> DefaultReturn<Option<String>> {
url = idna::punycode::encode_str(&url).unwrap();
if url.ends_with("-") {
url.pop();
}
let existing = &self.get_paste_by_url(url.clone()).await;
if !existing.success {
return DefaultReturn {
success: false,
message: String::from("Paste does not exist!"),
payload: Option::None,
};
}
let existing_metadata = &existing.payload.as_ref().unwrap().paste.metadata;
let paste = &existing.payload.clone().unwrap().paste;
let skip_password_check = if edit_as.is_some() {
let edit_as = edit_as.as_ref().unwrap();
let in_permissions_list = existing_metadata.permissions_list.get(edit_as);
(edit_as == &existing_metadata.owner)
| if in_permissions_list.is_some() {
let permission = in_permissions_list.unwrap();
(permission == &PastePermissionLevel::EditTextPasswordless)
| (permission == &PastePermissionLevel::Passwordless)
} else {
false
}
} else {
false
};
if !skip_password_check && utility::hash(edit_password) != paste.edit_password {
return DefaultReturn {
success: false,
message: String::from("Password invalid"),
payload: Option::None,
};
}
let user_permission = if edit_as.is_none() {
Option::None
} else {
let edit_as = edit_as.as_ref().unwrap();
let in_permissions_list = existing_metadata.permissions_list.get(edit_as);
in_permissions_list
};
if user_permission.is_some() {
let user_permission = user_permission.unwrap();
if user_permission == &PastePermissionLevel::EditTextPasswordless
&& (new_url.is_some() | new_edit_password.is_some())
{
return DefaultReturn {
success: false,
message: String::from(
"You must have a higher paste permission level to do this.",
),
payload: Option::None,
};
}
}
let edit_password_hash = if new_edit_password.is_some() {
utility::hash(new_edit_password.unwrap())
} else {
let edit_password = &paste.edit_password;
edit_password.to_owned()
};
if new_url.is_some() {
let existing = &self.get_paste_by_url(new_url.clone().unwrap()).await;
if existing.success {
return DefaultReturn {
success: false,
message: String::from("A paste with this URL already exists!"),
payload: Option::None,
};
}
}
let mut custom_url = if new_url.is_some() {
idna::punycode::encode_str(new_url.as_ref().unwrap()).unwrap()
} else {
paste.custom_url.clone()
};
if custom_url.ends_with("-") {
custom_url.pop();
}
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"UPDATE \"Pastes\" SET \"content\" = ?, \"content_html\" = ?, \"edit_password\" = ?, \"custom_url\" = ?, \"edit_date\" = ? WHERE \"custom_url\" = ?"
} else {
"UPDATE \"Pastes\" SET (\"content\", \"content_html\", \"edit_password\", \"custom_url\", \"edit_date\") = ($1, $2, $3, $4, $5) WHERE \"custom_url\" = $6"
};
let content_html = &crate::markdown::render::parse_markdown(&content);
let edit_date = &utility::unix_epoch_timestamp().to_string();
let c = &self.base.db.client;
let res = sqlquery(query)
.bind::<&String>(&content)
.bind::<&String>(content_html)
.bind::<&String>(&edit_password_hash)
.bind::<&String>(&custom_url)
.bind::<&String>(edit_date) .bind::<&String>(&url)
.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!("paste:{}", url)).await;
if existing_in_cache.is_some() {
let mut paste =
serde_json::from_str::<Paste<PasteMetadata>>(&existing_in_cache.unwrap()).unwrap();
paste.content = content; paste.content_html = content_html.to_string(); paste.edit_password = edit_password_hash; paste.edit_date = edit_date.parse::<u128>().unwrap(); paste.custom_url = custom_url.to_string(); self.base
.cachedb
.update(
format!("paste:{}", url),
serde_json::to_string::<Paste<PasteMetadata>>(&paste).unwrap(),
)
.await;
}
return DefaultReturn {
success: true,
message: String::from("Paste updated!"),
payload: Option::Some(custom_url.to_string()),
};
}
pub async fn edit_paste_metadata_by_url(
&self,
mut url: String,
metadata: PasteMetadata,
edit_password: String,
edit_as: Option<String>, skip_edit_check: bool
) -> DefaultReturn<Option<String>> {
url = idna::punycode::encode_str(&url).unwrap();
if url.ends_with("-") {
url.pop();
}
let existing = &self.get_paste_by_url(url.clone()).await;
if !existing.success {
return DefaultReturn {
success: false,
message: String::from("Paste does not exist!"),
payload: Option::None,
};
}
let existing_metadata = &existing.payload.as_ref().unwrap().paste.metadata;
let ua = if edit_as.is_some() {
Option::Some(
self.get_user_by_username(edit_as.clone().unwrap())
.await
.payload,
)
} else {
Option::None
};
let paste = &existing.payload.clone().unwrap().paste;
let skip_password_check = (skip_edit_check == true) | if edit_as.is_some() {
let edit_as = edit_as.as_ref().unwrap();
let in_permissions_list = existing_metadata.permissions_list.get(edit_as);
(edit_as == &existing_metadata.owner)
| (ua.as_ref().is_some() && ua.as_ref().unwrap().is_some() && ua.unwrap().unwrap().level.permissions.contains(&String::from("ManagePastes")))
| if in_permissions_list.is_some() {
let permission = in_permissions_list.unwrap();
permission == &PastePermissionLevel::Passwordless
} else {
false
}
} else {
false
};
if !skip_password_check && utility::hash(edit_password) != paste.edit_password {
return DefaultReturn {
success: false,
message: String::from("Password invalid"),
payload: Option::None,
};
}
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"UPDATE \"Pastes\" SET \"metadata\" = ? WHERE \"custom_url\" = ?"
} else {
"UPDATE \"Pastes\" SET (\"metadata\") = ($1) WHERE \"custom_url\" = $2"
};
let c = &self.base.db.client;
let res = sqlquery(query)
.bind::<&String>(&serde_json::to_string(&metadata).unwrap())
.bind::<&String>(&url)
.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!("paste:{}", url)).await;
if existing_in_cache.is_some() {
let mut paste =
serde_json::from_str::<Paste<PasteMetadata>>(&existing_in_cache.unwrap()).unwrap();
paste.metadata = metadata; self.base
.cachedb
.update(
format!("paste:{}", url),
serde_json::to_string::<Paste<PasteMetadata>>(&paste).unwrap(),
)
.await;
}
return DefaultReturn {
success: true,
message: String::from("Paste updated!"),
payload: Option::Some(url),
};
}
pub async fn add_view_to_url(
&self,
url: &String,
view_as: &String, ) -> DefaultReturn<Option<String>> {
let mut url = idna::punycode::encode_str(&url).unwrap();
if url.ends_with("-") {
url.pop();
}
let existing = &self.get_paste_by_url(url.clone()).await;
if !existing.success {
return DefaultReturn {
success: false,
message: String::from("Paste does not exist!"),
payload: Option::None,
};
}
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"SELECT * FROM \"Logs\" WHERE \"logtype\" = 'view_paste' AND \"content\" LIKE ?"
} else {
"SELECT * FROM \"Logs\" WHERE \"logtype\" = 'view_paste' AND \"content\" LIKE $1"
};
let c = &self.base.db.client;
let res = sqlquery(query)
.bind::<&String>(&format!("{}::{}", &url, &view_as))
.fetch_one(c)
.await;
if res.is_err() {
let err = res.err().unwrap();
let err_message = err.to_string();
if err_message.starts_with("no rows returned") {
self.logs
.create_log(
String::from("view_paste"),
format!("{}::{}", &url, &view_as),
)
.await;
let existing_in_cache = self.base.cachedb.get(format!("paste:{}", url)).await;
if existing_in_cache.is_some() {
let mut paste =
serde_json::from_str::<Paste<PasteMetadata>>(&existing_in_cache.unwrap())
.unwrap();
paste.views += 1;
self.base
.cachedb
.update(
format!("paste:{}", url),
serde_json::to_string::<Paste<PasteMetadata>>(&paste).unwrap(),
)
.await;
}
return DefaultReturn {
success: true,
message: String::from("View counted!"),
payload: Option::Some(url.to_string()),
};
}
return DefaultReturn {
success: false,
message: String::from("Failed to check for existing view!"),
payload: Option::None,
};
}
return DefaultReturn {
success: true,
message: String::from("View counted!"),
payload: Option::Some(url.to_string()),
};
}
pub async fn delete_paste_by_url(
&self,
mut url: String,
edit_password: String,
delete_as: Option<String>,
) -> DefaultReturn<Option<String>> {
url = idna::punycode::encode_str(&url).unwrap();
if url.ends_with("-") {
url.pop();
}
let existing = &self.get_paste_by_url(url.clone()).await;
if !existing.success {
return DefaultReturn {
success: false,
message: String::from("Paste does not exist!"),
payload: Option::None,
};
}
let existing_metadata = &existing.payload.as_ref().unwrap().paste.metadata;
let ua = if delete_as.is_some() {
Option::Some(
self.get_user_by_username(delete_as.clone().unwrap())
.await
.payload,
)
} else {
Option::None
};
let paste = &existing.payload.clone().unwrap().paste;
let skip_password_check = if delete_as.is_some() {
let delete_as = delete_as.as_ref().unwrap();
let in_permissions_list = existing_metadata.permissions_list.get(delete_as);
(delete_as == &existing_metadata.owner)
| (ua.as_ref().is_some() && ua.as_ref().unwrap().is_some() && ua.unwrap().unwrap().level.permissions.contains(&String::from("ManagePastes")))
| if in_permissions_list.is_some() {
let permission = in_permissions_list.unwrap();
permission == &PastePermissionLevel::Passwordless
} else {
false
}
} else {
false
};
if !skip_password_check && utility::hash(edit_password) != paste.edit_password {
return DefaultReturn {
success: false,
message: String::from("Password invalid"),
payload: Option::None,
};
}
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"DELETE FROM \"Pastes\" WHERE \"custom_url\" = ?"
} else {
"DELETE FROM \"Pastes\" WHERE \"custom_url\" = $1"
};
let c = &self.base.db.client;
let res = sqlquery(query).bind::<&String>(&url).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!("{}::%", &url))
.execute(c)
.await;
if res.is_err() {
return DefaultReturn {
success: false,
message: String::from("Failed to delete paste"),
payload: Option::None,
};
}
self.base.cachedb.remove(format!("paste:{}", url)).await;
return DefaultReturn {
success: true,
message: String::from("Paste deleted!"),
payload: Option::Some(url),
};
}
pub async fn get_group_by_name(&self, url: String) -> DefaultReturn<Option<Group<String>>> {
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"SELECT * FROM \"Groups\" WHERE \"name\" = ?"
} else {
"SELECT * FROM \"Groups\" WHERE \"name\" = $1"
};
let c = &self.base.db.client;
let res = sqlquery(query).bind::<&String>(&url).fetch_one(c).await;
if res.is_err() {
return DefaultReturn {
success: false,
message: String::from("Group does not exist"),
payload: Option::None,
};
}
let row = res.unwrap();
let row = self.base.textify_row(row).data;
return DefaultReturn {
success: true,
message: String::from("Group exists"),
payload: Option::Some(Group {
name: row.get("name").unwrap().to_string(),
submit_password: row.get("submit_password").unwrap().to_string(),
metadata: row.get("metadata").unwrap().to_string(),
}),
};
}
pub async fn create_group(&self, props: Group<GroupMetadata>) -> DefaultReturn<Option<String>> {
let p: &Group<GroupMetadata> = &props; let existing: DefaultReturn<Option<Group<String>>> =
self.get_group_by_name(p.name.to_owned()).await;
if existing.success {
return DefaultReturn {
success: false,
message: String::from("Group already exists!"),
payload: Option::None,
};
}
let query: &str = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"INSERT INTO \"Groups\" VALUES (?, ?, ?)"
} else {
"INSERT INTO \"Groups\" VALUES ($1, $2, $3)"
};
let c = &self.base.db.client;
let p: &mut Group<GroupMetadata> = &mut props.clone();
p.submit_password = utility::hash(p.submit_password.clone());
let res = sqlquery(query)
.bind::<&String>(&p.name)
.bind::<&String>(&p.submit_password)
.bind::<&String>(&serde_json::to_string(&p.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("Paste created"),
payload: Option::Some(p.name.to_string()),
};
}
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),
};
}
}