use crate::model::{PasteCreate, PasteError, Paste, PasteMetadata};
use dorsal::utility;
use dorsal::query as sqlquery;
use dorsal::db::special::auth_db::{FullUser, UserMetadata};
pub type Result<T> = std::result::Result<T, PasteError>;
#[derive(Clone, Debug, PartialEq)]
pub enum ViewMode {
AuthenticatedOnce,
OpenMultiple,
}
#[derive(Clone, Debug)]
pub struct PastesTableConfig {
pub table_name: String,
pub prefix: String,
pub id: String,
pub url: String,
pub password: String,
pub content: String,
pub date_published: String,
pub date_edited: String,
pub metadata: String,
}
impl Default for PastesTableConfig {
fn default() -> Self {
Self {
table_name: "pastes".to_string(),
prefix: "paste".to_string(),
id: "id".to_string(),
url: "url".to_string(),
password: "password".to_string(),
content: "content".to_string(),
date_published: "date_published".to_string(),
date_edited: "date_edited".to_string(),
metadata: "metadata".to_string(),
}
}
}
#[derive(Clone, Debug)]
pub struct ViewsTableConfig {
pub table_name: String,
pub prefix: String,
}
impl Default for ViewsTableConfig {
fn default() -> Self {
Self {
table_name: "views".to_string(),
prefix: "views".to_string(),
}
}
}
#[derive(Clone, Debug)]
pub struct ServerOptions {
pub view_password: bool,
pub guppy: bool,
pub paste_ownership: bool,
pub view_mode: ViewMode,
pub table_pastes: PastesTableConfig,
pub table_views: ViewsTableConfig,
}
impl ServerOptions {
pub fn truthy() -> Self {
Self {
view_password: true,
guppy: true,
paste_ownership: true,
view_mode: ViewMode::OpenMultiple,
table_pastes: PastesTableConfig::default(),
table_views: ViewsTableConfig::default(),
}
}
}
impl Default for ServerOptions {
fn default() -> Self {
Self {
view_password: false,
guppy: false,
paste_ownership: false,
view_mode: ViewMode::OpenMultiple,
table_pastes: PastesTableConfig::default(),
table_views: ViewsTableConfig::default(),
}
}
}
#[derive(Clone)]
pub struct Database {
pub base: dorsal::StarterDatabase,
pub auth: dorsal::AuthDatabase,
pub options: ServerOptions,
}
impl Database {
pub async fn new(opts: dorsal::DatabaseOpts, opts1: ServerOptions) -> Self {
let base = dorsal::StarterDatabase::new(opts).await;
Self {
base: base.clone(),
auth: dorsal::AuthDatabase::new(base).await,
options: opts1,
}
}
pub async fn init(&self) {
let c = &self.base.db.client;
let _ = sqlquery(&format!(
"CREATE TABLE IF NOT EXISTS \"{}\" (
{} TEXT,
{} TEXT,
{} TEXT,
{} TEXT,
{} TEXT,
{} TEXT,
{} TEXT
)",
self.options.table_pastes.table_name,
self.options.table_pastes.id,
self.options.table_pastes.url,
self.options.table_pastes.password,
self.options.table_pastes.content,
self.options.table_pastes.date_published,
self.options.table_pastes.date_edited,
self.options.table_pastes.metadata
))
.execute(c)
.await;
if self.options.view_mode == ViewMode::AuthenticatedOnce {
let _ = sqlquery(&format!(
"CREATE TABLE IF NOT EXISTS \"{}\" (
url TEXT,
username TEXT
)",
self.options.table_views.table_name
))
.execute(c)
.await;
}
}
pub async fn get_paste_by_url(&self, mut url: String) -> Result<Paste> {
url = idna::punycode::encode_str(&url).unwrap().to_lowercase();
if url.ends_with("-") {
url.pop();
}
match self
.base
.cachedb
.get(format!("{}:{}", self.options.table_pastes.prefix, url))
.await
{
Some(c) => return Ok(serde_json::from_str::<Paste>(c.as_str()).unwrap()),
None => (),
};
let query: String = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"SELECT * FROM \":t\" WHERE \":url\" = ?"
} else {
"SELECT * FROM \":t\" WHERE \":url\" = $1"
}
.to_string()
.replace(":t", &self.options.table_pastes.table_name)
.replace(":url", &self.options.table_pastes.url);
let c = &self.base.db.client;
let res = match sqlquery(&query)
.bind::<&String>(&url.to_lowercase())
.fetch_one(c)
.await
{
Ok(p) => self.base.textify_row(p).data,
Err(_) => return Err(PasteError::NotFound),
};
let paste = Paste {
id: res.get(&self.options.table_pastes.id).unwrap().to_string(),
url: res.get(&self.options.table_pastes.url).unwrap().to_string(),
password: res
.get(&self.options.table_pastes.password)
.unwrap()
.to_string(),
content: res
.get(&self.options.table_pastes.content)
.unwrap()
.to_string(),
date_published: res
.get(&self.options.table_pastes.date_published)
.unwrap()
.parse::<u128>()
.unwrap(),
date_edited: res
.get(&self.options.table_pastes.date_edited)
.unwrap()
.parse::<u128>()
.unwrap(),
metadata: match serde_json::from_str(
res.get(&self.options.table_pastes.metadata).unwrap(),
) {
Ok(m) => m,
Err(_) => return Err(PasteError::ValueError),
},
};
self.base
.cachedb
.set(
format!("{}:{}", self.options.table_pastes.prefix, url),
serde_json::to_string::<Paste>(&paste).unwrap(),
)
.await;
Ok(paste)
}
pub async fn create_paste(&self, mut props: PasteCreate) -> Result<(String, Paste)> {
props.url = idna::punycode::encode_str(&props.url)
.unwrap()
.to_lowercase();
if props.url.ends_with("-") {
props.url.pop();
}
if let Ok(_) = self.get_paste_by_url(props.url.clone()).await {
return Err(PasteError::AlreadyExists);
}
if props.url.is_empty() {
props.url = utility::random_id().chars().take(10).collect();
}
if props.password.is_empty() {
props.password = utility::random_id().chars().take(10).collect();
}
if (props.url.len() > 250) | (props.url.len() < 3) {
return Err(PasteError::ValueError);
}
if (props.content.len() > 200_000) | (props.content.len() < 1) {
return Err(PasteError::ValueError);
}
let regex = regex::RegexBuilder::new("^[\\w\\_\\-\\.\\!\\p{Extended_Pictographic}]+$")
.multi_line(true)
.build()
.unwrap();
if regex.captures(&props.url).iter().len() < 1 {
return Err(PasteError::ValueError);
}
let paste = Paste {
id: utility::random_id(),
url: props.url,
content: props.content,
password: utility::hash(props.password.clone()),
date_published: utility::unix_epoch_timestamp(),
date_edited: utility::unix_epoch_timestamp(),
metadata: super::model::PasteMetadata::default(),
};
let query: String = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"INSERT INTO \":t\" VALUES (?, ?, ?, ?, ?, ?, ?)"
} else {
"INSERT INTO \":t\" VALEUS ($1, $2, $3, $4, $5, $6, $7)"
}
.to_string()
.replace(":t", &self.options.table_pastes.table_name);
let c = &self.base.db.client;
match sqlquery(&query)
.bind::<&String>(&paste.id)
.bind::<&String>(&paste.url)
.bind::<&String>(&paste.password)
.bind::<&String>(&paste.content)
.bind::<&String>(&paste.date_published.to_string())
.bind::<&String>(&paste.date_edited.to_string())
.bind::<&String>(match serde_json::to_string(&paste.metadata) {
Ok(ref s) => s,
Err(_) => return Err(PasteError::ValueError),
})
.execute(c)
.await
{
Ok(_) => return Ok((props.password, paste)),
Err(_) => return Err(PasteError::Other),
};
}
pub async fn delete_paste_by_url(&self, mut url: String, password: String) -> Result<()> {
url = idna::punycode::encode_str(&url).unwrap().to_lowercase();
if url.ends_with("-") {
url.pop();
}
let existing = match self.get_paste_by_url(url.clone()).await {
Ok(p) => p,
Err(err) => return Err(err),
};
if utility::hash(password) != existing.password {
return Err(PasteError::PasswordIncorrect);
}
self.base
.cachedb
.remove(format!("{}:{}", self.options.table_views.prefix, url))
.await;
let query: String = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"DELETE FROM \":t\" WHERE \":url\" = ?"
} else {
"DELETE FROM \":t\" WHERE \":url\" = $1"
}
.to_string()
.replace(":t", &self.options.table_pastes.table_name)
.replace(":url", &self.options.table_pastes.url);
let c = &self.base.db.client;
match sqlquery(&query).bind::<&String>(&url).execute(c).await {
Ok(_) => {
self.base
.cachedb
.remove(format!("{}:{}", self.options.table_pastes.prefix, url))
.await;
if self.options.view_mode == ViewMode::AuthenticatedOnce {
let query: String =
if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"DELETE FROM \":t\" WHERE \"url\" = ?"
} else {
"DELETE FROM \":t\" WHERE \"url\" = $1"
}
.replace(":t", &self.options.table_views.table_name);
if let Err(_) = sqlquery(&query).bind::<&String>(&url).execute(c).await {
return Err(PasteError::Other);
};
}
return Ok(());
}
Err(_) => return Err(PasteError::Other),
};
}
pub async fn edit_paste_by_url(
&self,
mut url: String,
password: String,
new_content: String,
mut new_url: String,
mut new_password: String,
editing_as: Option<FullUser<UserMetadata>>,
) -> Result<()> {
url = idna::punycode::encode_str(&url).unwrap().to_lowercase();
if url.ends_with("-") {
url.pop();
}
let existing = match self.get_paste_by_url(url.clone()).await {
Ok(p) => p,
Err(err) => return Err(err),
};
let mut skip_password_check: bool = false;
if let Some(ua) = editing_as {
if ua.user.username == existing.metadata.owner {
skip_password_check = true;
}
else if ua.level.permissions.contains(&"ManagePastes".to_string()) {
skip_password_check = true;
}
}
if skip_password_check == false {
if utility::hash(password) != existing.password {
return Err(PasteError::PasswordIncorrect);
}
}
if !new_password.is_empty() {
new_password = utility::hash(new_password);
} else {
new_password = existing.password;
}
if new_url.is_empty() {
new_url = existing.url;
}
new_url = idna::punycode::encode_str(&new_url).unwrap();
if new_url.ends_with("-") {
new_url.pop();
}
let query: String = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"UPDATE \":t\" SET \":content\" = ?, \":password\" = ?, \":url\" = ?, \":date_edited\" = ? WHERE \":url\" = ?"
} else {
"UPDATE \":t\" SET (\":content\" = $1, \":password\" = $2, \":url\" = $3, \":date_edited\" = $4) WHERE \":url\" = $5"
}
.to_string()
.replace(":t", &self.options.table_pastes.table_name)
.replace(":url", &self.options.table_pastes.url)
.replace(":content", &self.options.table_pastes.content)
.replace(":password", &self.options.table_pastes.password)
.replace(":date_edited", &self.options.table_pastes.date_edited);
let c = &self.base.db.client;
match sqlquery(&query)
.bind::<&String>(&new_content)
.bind::<&String>(&new_password)
.bind::<&String>(&new_url)
.bind::<&String>(&utility::unix_epoch_timestamp().to_string())
.bind::<&String>(&url)
.execute(c)
.await
{
Ok(_) => {
self.base
.cachedb
.remove(format!("{}:{}", self.options.table_pastes.prefix, url))
.await;
return Ok(());
}
Err(_) => return Err(PasteError::Other),
};
}
pub async fn edit_paste_metadata_by_url(
&self,
mut url: String,
password: String,
metadata: PasteMetadata,
editing_as: Option<FullUser<UserMetadata>>,
) -> Result<()> {
url = idna::punycode::encode_str(&url).unwrap().to_lowercase();
if url.ends_with("-") {
url.pop();
}
let existing = match self.get_paste_by_url(url.clone()).await {
Ok(p) => p,
Err(err) => return Err(err),
};
let mut skip_password_check: bool = false;
if let Some(ua) = editing_as {
if ua.user.username == existing.metadata.owner {
skip_password_check = true;
}
else if ua.level.permissions.contains(&"ManagePastes".to_string()) {
skip_password_check = true;
}
}
if skip_password_check == false {
if utility::hash(password) != existing.password {
return Err(PasteError::PasswordIncorrect);
}
}
let query: String = if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"UPDATE \":t\" SET \":metadata\" = ? WHERE \":url\" = ?"
} else {
"UPDATE \":t\" SET (\":metadata\" = $1) WHERE \":url\" = $2"
}
.to_string()
.replace(":t", &self.options.table_pastes.table_name)
.replace(":url", &self.options.table_pastes.url)
.replace(":metadata", &self.options.table_pastes.metadata);
let c = &self.base.db.client;
match sqlquery(&query)
.bind::<&String>(match serde_json::to_string(&metadata) {
Ok(ref m) => m,
Err(_) => return Err(PasteError::ValueError),
})
.bind::<&String>(&url)
.execute(c)
.await
{
Ok(_) => {
self.base
.cachedb
.remove(format!("{}:{}", self.options.table_pastes.prefix, url))
.await;
return Ok(());
}
Err(_) => return Err(PasteError::Other),
};
}
pub async fn get_views_by_url(&self, mut url: String) -> i32 {
url = idna::punycode::encode_str(&url).unwrap().to_lowercase();
if url.ends_with("-") {
url.pop();
}
match self
.base
.cachedb
.get(format!("{}:{}", self.options.table_views.prefix, url))
.await
{
Some(c) => c.parse::<i32>().unwrap(),
None => {
if self.options.view_mode == ViewMode::AuthenticatedOnce {
let query: String =
if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"SELECT * FROM \":t\" WHERE \"url\" = ?"
} else {
"SELECT * FROM \":t\" WHERE \"url\" = $1"
}
.to_string()
.replace(":t", &self.options.table_views.table_name);
let c = &self.base.db.client;
match sqlquery(&query).bind::<&String>(&url).fetch_all(c).await {
Ok(views) => {
let views = views.len();
self.base
.cachedb
.set(
format!("{}:{}", self.options.table_views.prefix, url),
views.to_string(),
)
.await;
return views as i32;
}
Err(_) => return 0,
};
}
0
}
}
}
pub async fn incr_views_by_url(
&self,
mut url: String,
as_user: Option<FullUser<UserMetadata>>,
) -> Result<()> {
url = idna::punycode::encode_str(&url).unwrap().to_lowercase();
if url.ends_with("-") {
url.pop();
}
if self.options.view_mode == ViewMode::AuthenticatedOnce {
match as_user {
Some(ua) => {
if self
.user_has_viewed_paste(url.clone(), ua.user.username.clone())
.await
{
return Ok(());
}
let query: String =
if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"INSERT INTO \":t\" VALUES (?, ?)"
} else {
"INSERT INTO \":t\" VALEUS ($1, $2)"
}
.to_string()
.replace(":t", &self.options.table_views.table_name);
let c = &self.base.db.client;
match sqlquery(&query)
.bind::<&String>(&url)
.bind::<&String>(&ua.user.username)
.execute(c)
.await
{
Ok(_) => (), Err(_) => return Err(PasteError::Other),
};
}
None => return Ok(()), }
}
match self
.base
.cachedb
.incr(format!("{}:{}", self.options.table_views.prefix, url))
.await
{
false => Ok(()),
true => Err(PasteError::Other),
}
}
pub async fn user_has_viewed_paste(&self, url: String, username: String) -> bool {
if self.options.view_mode == ViewMode::AuthenticatedOnce {
let query: String =
if (self.base.db._type == "sqlite") | (self.base.db._type == "mysql") {
"SELECT * FROM \":t\" WHERE \"url\" = ? AND \"username\" = ?"
} else {
"SELECT * FROM \":t\" WHERE \"url\" = $1 AND \"username\" = ?"
}
.to_string()
.replace(":t", &self.options.table_views.table_name);
let c = &self.base.db.client;
match sqlquery(&query)
.bind::<&String>(&url)
.bind::<&String>(&username)
.fetch_one(c)
.await
{
Ok(_) => return true,
Err(_) => return false,
};
}
false
}
}