use std::collections::HashMap;
use std::str::Split;
use crate::model::{
DatabaseError, IpBan, IpBanCreate, IpBlock, IpBlockCreate, Mail, MailCreate, MailState,
Profile, ProfileCreate, ProfileMetadata, RelationshipStatus, TokenContext, UserLabel, Warning,
WarningCreate,
};
use crate::model::{Group, Notification, NotificationCreate, Permission, UserFollow};
use citrus_client::{
CitrusClient, TemplateBuilder,
model::{CitrusID, HttpProtocol},
};
use hcaptcha_no_wasm::Hcaptcha;
use reqwest::Client as HttpClient;
use serde::{Deserialize, Serialize};
use databeam::{query as sqlquery, utility, DefaultReturn};
pub type Result<T> = std::result::Result<T, DatabaseError>;
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct HCaptchaConfig {
pub site_key: String,
pub secret: String,
}
impl Default for HCaptchaConfig {
fn default() -> Self {
Self {
site_key: "10000000-ffff-ffff-ffff-000000000001".to_string(),
secret: "0x0000000000000000000000000000000000000000".to_string(),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ServerOptions {
#[serde(default)]
pub registration_enabled: bool,
#[serde(default)]
pub captcha: HCaptchaConfig,
#[serde(default)]
pub real_ip_header: Option<String>,
#[serde(default)]
pub static_dir: String,
#[serde(default)]
pub host: String,
#[serde(default)]
pub citrus_id: String,
#[serde(default)]
pub blocked_hosts: Vec<String>,
#[serde(default = "secure_default")]
pub secure: bool,
}
pub fn secure_default() -> bool {
true
}
impl Default for ServerOptions {
fn default() -> Self {
Self {
registration_enabled: true,
captcha: HCaptchaConfig::default(),
real_ip_header: Option::None,
static_dir: String::new(),
host: String::new(),
citrus_id: String::new(),
blocked_hosts: Vec::new(),
secure: true,
}
}
}
#[derive(Clone)]
pub struct Database {
pub base: databeam::StarterDatabase,
pub config: ServerOptions,
pub http: HttpClient,
pub citrus: CitrusClient,
}
impl Database {
pub async fn new(
database_options: databeam::DatabaseOpts,
server_options: ServerOptions,
) -> Self {
let base = databeam::StarterDatabase::new(database_options).await;
Self {
base: base.clone(),
http: HttpClient::new(),
citrus: if server_options.secure {
CitrusClient::new(HttpProtocol::Https)
} else {
CitrusClient::new(HttpProtocol::Http)
},
config: server_options,
}
}
pub fn env_options() -> databeam::DatabaseOpts {
use std::env::var;
databeam::DatabaseOpts {
r#type: match var("DB_TYPE") {
Ok(v) => Option::Some(v),
Err(_) => Option::None,
},
host: match var("DB_HOST") {
Ok(v) => Option::Some(v),
Err(_) => Option::None,
},
user: var("DB_USER").unwrap_or(String::new()),
pass: var("DB_PASS").unwrap_or(String::new()),
name: var("DB_NAME").unwrap_or(String::new()),
}
}
pub async fn init(&self) {
let c = &self.base.db.client;
let _ = sqlquery(
"CREATE TABLE IF NOT EXISTS \"xprofiles\" (
id TEXT,
username TEXT,
password TEXT,
tokens TEXT,
metadata TEXT,
joined TEXT,
gid TEXT,
salt TEXT,
ips TEXT,
badges TEXT,
tier TEXT,
token_context TEXT,
labels TEXT
)",
)
.execute(c)
.await;
let _ = sqlquery(
"CREATE TABLE IF NOT EXISTS \"xgroups\" (
name TEXT,
id TEXT,
permissions TEXT
)",
)
.execute(c)
.await;
let _ = sqlquery(
"CREATE TABLE IF NOT EXISTS \"xfollows\" (
user TEXT,
following TEXT
)",
)
.execute(c)
.await;
let _ = sqlquery(
"CREATE TABLE IF NOT EXISTS \"xnotifications\" (
title TEXT,
content TEXT,
address TEXT,
timestamp TEXT,
id TEXT,
recipient TEXT
)",
)
.execute(c)
.await;
let _ = sqlquery(
"CREATE TABLE IF NOT EXISTS \"xwarnings\" (
id TEXT,
content TEXT,
timestamp TEXT,
recipient TEXT,
moderator TEXT
)",
)
.execute(c)
.await;
let _ = sqlquery(
"CREATE TABLE IF NOT EXISTS \"xbans\" (
id TEXT,
ip TEXT,
reason TEXT,
moderator TEXT,
timestamp TEXT
)",
)
.execute(c)
.await;
let _ = sqlquery(
"CREATE TABLE IF NOT EXISTS \"xrelationships\" (
one TEXT,
two TEXT,
status TEXT,
timestamp TEXT
)",
)
.execute(c)
.await;
let _ = sqlquery(
"CREATE TABLE IF NOT EXISTS \"xipblocks\" (
id TEXT,
ip TEXT,
user TEXT,
context TEXT,
timestamp TEXT
)",
)
.execute(c)
.await;
let _ = sqlquery(
"CREATE TABLE IF NOT EXISTS \"xmail\" (
title TEXT,
content TEXT,
timestamp TEXT,
id TEXT,
state TEXT,
author TEXT,
recipient TEXT
)",
)
.execute(c)
.await;
let _ = sqlquery(
"CREATE TABLE IF NOT EXISTS \"xlabels\" (
id TEXT,
name TEXT,
timestamp TEXT,
creator TEXT
)",
)
.execute(c)
.await;
}
pub fn collect_split_without_stupid_reference(input: Split<&str>) -> Vec<String> {
let mut out = Vec::new();
for section in input {
out.push(section.to_owned())
}
out
}
pub async fn audit(&self, actor_id: String, content: String) -> Result<()> {
match self
.create_notification(
NotificationCreate {
title: format!("[{actor_id}](/+u/{actor_id})"),
content,
address: format!("/+u/{actor_id}"),
recipient: "*(audit)".to_string(), },
None,
)
.await
{
Ok(_) => Ok(()),
Err(_) => Err(DatabaseError::Other),
}
}
pub async fn get_profile(&self, mut id: String) -> Result<Box<Profile>> {
if id.starts_with("ANSWERED:") {
id = id.replace("ANSWERED:", "");
}
if id == "@" {
return Ok(Box::new(Profile::global()));
} else if id.starts_with("anonymous#") | (id == "anonymous") | (id == "#") {
let tag = Profile::anonymous_tag(&id);
return Ok(Box::new(Profile::anonymous(tag.3)));
} else if (id == "0") | (id == "system") {
return Ok(Box::new(Profile::system()));
}
let cid = CitrusID(id.clone()).fields();
if cid.0 != self.config.citrus_id && !cid.0.is_empty() {
let server = match self.citrus.server(cid.0.to_string()).await {
Ok(s) => s,
Err(_) => return Err(DatabaseError::Other),
};
if server.get_schema("net.rainbeam.structs.Profile").is_none() {
return Err(DatabaseError::Other);
}
match self
.citrus
.get::<DefaultReturn<Option<Profile>>>(
server,
"net.rainbeam.structs.Profile",
&TemplateBuilder("/api/v0/auth/profile/<field>".to_string())
.build(vec![&cid.1])
.0,
)
.await
{
Ok(p) => {
if let Some(p) = p.payload {
return Ok(Box::new(p));
} else {
return Err(DatabaseError::NotFound);
}
}
Err(_) => return Err(DatabaseError::Other),
}
}
if id.contains("%") {
id = id
.split("%")
.collect::<Vec<&str>>()
.get(0)
.unwrap()
.to_string();
}
if id.len() <= 32 {
return match self.get_profile_by_username(id).await {
Ok(ua) => Ok(ua),
Err(e) => return Err(e),
};
}
match self.get_profile_by_id(id).await {
Ok(ua) => Ok(ua),
Err(e) => return Err(e),
}
}
pub async fn get_profile_by_hashed(&self, hashed: String) -> Result<Box<Profile>> {
let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"SELECT * FROM \"xprofiles\" WHERE \"tokens\" LIKE ?"
} else {
"SELECT * FROM \"xprofiles\" WHERE \"tokens\" LIKE $1"
};
let c = &self.base.db.client;
let row = match sqlquery(query)
.bind::<&String>(&format!("%\"{hashed}\"%"))
.fetch_one(c)
.await
{
Ok(u) => self.base.textify_row(u, Vec::new()).0,
Err(_) => return Err(DatabaseError::Other),
};
Ok(Box::new(Profile {
id: row.get("id").unwrap().to_string(),
username: row.get("username").unwrap().to_string(),
password: row.get("password").unwrap().to_string(),
salt: row.get("salt").unwrap_or(&"".to_string()).to_string(),
tokens: match serde_json::from_str(row.get("tokens").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
ips: match serde_json::from_str(row.get("ips").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
token_context: match serde_json::from_str(row.get("token_context").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
metadata: match serde_json::from_str(row.get("metadata").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
badges: match serde_json::from_str(row.get("badges").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
group: row.get("gid").unwrap().parse::<i32>().unwrap_or(0),
joined: row.get("joined").unwrap().parse::<u128>().unwrap(),
tier: row.get("tier").unwrap().parse::<i32>().unwrap_or(0),
labels: Database::collect_split_without_stupid_reference(
row.get("labels").unwrap().split(","),
),
}))
}
pub async fn get_profile_by_unhashed(&self, unhashed: String) -> Result<Box<Profile>> {
self.get_profile_by_hashed(utility::hash(unhashed.clone()))
.await
}
pub async fn get_profile_by_ip(&self, ip: String) -> Result<Box<Profile>> {
let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"SELECT * FROM \"xprofiles\" WHERE \"ips\" LIKE ?"
} else {
"SELECT * FROM \"xprofiles\" WHERE \"ips\" LIKE $1"
};
let c = &self.base.db.client;
let row = match sqlquery(query)
.bind::<&String>(&format!("%\"{ip}\"%"))
.fetch_one(c)
.await
{
Ok(u) => self.base.textify_row(u, Vec::new()).0,
Err(_) => return Err(DatabaseError::Other),
};
Ok(Box::new(Profile {
id: row.get("id").unwrap().to_string(),
username: row.get("username").unwrap().to_string(),
password: row.get("password").unwrap().to_string(),
salt: row.get("salt").unwrap_or(&"".to_string()).to_string(),
tokens: match serde_json::from_str(row.get("tokens").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
ips: match serde_json::from_str(row.get("ips").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
token_context: match serde_json::from_str(row.get("token_context").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
metadata: match serde_json::from_str(row.get("metadata").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
badges: match serde_json::from_str(row.get("badges").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
group: row.get("gid").unwrap().parse::<i32>().unwrap_or(0),
joined: row.get("joined").unwrap().parse::<u128>().unwrap(),
tier: row.get("tier").unwrap().parse::<i32>().unwrap_or(0),
labels: Database::collect_split_without_stupid_reference(
row.get("labels").unwrap().split(","),
),
}))
}
pub async fn get_profile_by_username_password(
&self,
username: String,
mut password: String,
) -> Result<Profile> {
password = databeam::utility::hash(password);
let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"SELECT * FROM \"xprofiles\" WHERE \"username\" = ? AND \"password\" = ?"
} else {
"SELECT * FROM \"xprofiles\" WHERE \"username\" = $1 AND \"password\" = $2"
};
let c = &self.base.db.client;
let row = match sqlquery(query)
.bind::<&String>(&username)
.bind::<&String>(&password)
.fetch_one(c)
.await
{
Ok(r) => self.base.textify_row(r, Vec::new()).0,
Err(_) => return Err(DatabaseError::Other),
};
Ok(Profile {
id: row.get("id").unwrap().to_string(),
username: row.get("username").unwrap().to_string(),
password: row.get("password").unwrap().to_string(),
salt: row.get("salt").unwrap_or(&"".to_string()).to_string(),
tokens: match serde_json::from_str(row.get("tokens").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
ips: match serde_json::from_str(row.get("ips").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
token_context: match serde_json::from_str(row.get("token_context").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
metadata: match serde_json::from_str(row.get("metadata").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
badges: match serde_json::from_str(row.get("badges").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
group: row.get("gid").unwrap().parse::<i32>().unwrap_or(0),
joined: row.get("joined").unwrap().parse::<u128>().unwrap(),
tier: row.get("tier").unwrap().parse::<i32>().unwrap_or(0),
labels: Database::collect_split_without_stupid_reference(
row.get("labels").unwrap().split(","),
),
})
}
pub async fn get_profile_by_username(&self, mut username: String) -> Result<Box<Profile>> {
username = username.to_lowercase();
let cached = self
.base
.cachedb
.get(format!("rbeam.auth.profile:{}", username))
.await;
if cached.is_some() {
match serde_json::from_str::<Profile>(cached.unwrap().as_str()) {
Ok(p) => return Ok(Box::new(p)),
Err(_) => {
self.base
.cachedb
.remove(format!("rbeam.auth.profile:{}", username))
.await;
}
};
}
let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"SELECT * FROM \"xprofiles\" WHERE \"username\" = ?"
} else {
"SELECT * FROM \"xprofiles\" WHERE \"username\" = $1"
};
let c = &self.base.db.client;
let row = match sqlquery(query)
.bind::<&String>(&username)
.fetch_one(c)
.await
{
Ok(r) => self.base.textify_row(r, Vec::new()).0,
Err(_) => return Err(DatabaseError::NotFound),
};
let user = Profile {
id: row.get("id").unwrap().to_string(),
username: row.get("username").unwrap().to_string(),
password: row.get("password").unwrap().to_string(),
salt: row.get("salt").unwrap_or(&"".to_string()).to_string(),
tokens: match serde_json::from_str(row.get("tokens").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
ips: match serde_json::from_str(row.get("ips").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
token_context: match serde_json::from_str(row.get("token_context").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
metadata: match serde_json::from_str(row.get("metadata").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
badges: match serde_json::from_str(row.get("badges").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
group: row.get("gid").unwrap().parse::<i32>().unwrap_or(0),
joined: row.get("joined").unwrap().parse::<u128>().unwrap(),
tier: row.get("tier").unwrap().parse::<i32>().unwrap_or(0),
labels: Database::collect_split_without_stupid_reference(
row.get("labels").unwrap().split(","),
),
};
self.base
.cachedb
.set(
format!("rbeam.auth.profile:{}", username),
serde_json::to_string::<Profile>(&user).unwrap(),
)
.await;
Ok(Box::new(user))
}
pub async fn get_profile_by_id(&self, mut id: String) -> Result<Box<Profile>> {
id = id.to_lowercase();
let cached = self
.base
.cachedb
.get(format!("rbeam.auth.profile:{}", id))
.await;
if cached.is_some() {
match serde_json::from_str::<Profile>(cached.unwrap().as_str()) {
Ok(p) => return Ok(Box::new(p)),
Err(_) => {
self.base
.cachedb
.remove(format!("rbeam.auth.profile:{}", id))
.await;
}
};
}
let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"SELECT * FROM \"xprofiles\" WHERE \"id\" = ?"
} else {
"SELECT * FROM \"xprofiles\" WHERE \"id\" = $1"
};
let c = &self.base.db.client;
let row = match sqlquery(query).bind::<&String>(&id).fetch_one(c).await {
Ok(r) => self.base.textify_row(r, Vec::new()).0,
Err(_) => return Err(DatabaseError::NotFound),
};
let user = Profile {
id: row.get("id").unwrap().to_string(),
username: row.get("username").unwrap().to_string(),
password: row.get("password").unwrap().to_string(),
salt: row.get("salt").unwrap_or(&"".to_string()).to_string(),
tokens: match serde_json::from_str(row.get("tokens").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
ips: match serde_json::from_str(row.get("ips").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
token_context: match serde_json::from_str(row.get("token_context").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
metadata: match serde_json::from_str(row.get("metadata").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
badges: match serde_json::from_str(row.get("badges").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
group: row.get("gid").unwrap().parse::<i32>().unwrap_or(0),
joined: row.get("joined").unwrap().parse::<u128>().unwrap(),
tier: row.get("tier").unwrap().parse::<i32>().unwrap_or(0),
labels: Database::collect_split_without_stupid_reference(
row.get("labels").unwrap().split(","),
),
};
self.base
.cachedb
.set(
format!("rbeam.auth.profile:{}", id),
serde_json::to_string::<Profile>(&user).unwrap(),
)
.await;
Ok(Box::new(user))
}
pub fn validate_username(username: String) -> Result<()> {
let banned_usernames = &[
"admin",
"account",
"anonymous",
"login",
"sign_up",
"settings",
"api",
"intents",
"circles",
"chats",
"sites",
"responses",
"questions",
"comments",
"pages",
"inbox",
"system",
];
let regex = regex::RegexBuilder::new(r"[^\w_\-\.!]+")
.multi_line(true)
.build()
.unwrap();
if regex.captures(&username).is_some() {
return Err(DatabaseError::ValueError);
}
if (username.len() < 2) | (username.len() > 500) {
return Err(DatabaseError::ValueError);
}
if banned_usernames.contains(&username.as_str()) {
return Err(DatabaseError::ValueError);
}
Ok(())
}
pub async fn create_profile(&self, props: ProfileCreate, user_ip: String) -> Result<String> {
if self.config.registration_enabled == false {
return Err(DatabaseError::NotAllowed);
}
let username = props.username.trim().to_string();
let password = props.password.trim().to_string();
if let Err(_) = props
.valid_response(&self.config.captcha.secret, None)
.await
{
return Err(DatabaseError::NotAllowed);
}
if let Ok(_) = &self.get_profile_by_username(username.clone()).await {
return Err(DatabaseError::MustBeUnique);
};
if let Err(e) = Database::validate_username(username.clone()) {
return Err(e);
}
let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"INSERT INTO \"xprofiles\" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
} else {
"INSERT INTO \"xprofiles\" VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)"
};
let user_token_unhashed: String = databeam::utility::uuid();
let user_token_hashed: String = databeam::utility::hash(user_token_unhashed.clone());
let salt: String = rainbeam_shared::hash::salt();
let timestamp = utility::unix_epoch_timestamp().to_string();
let c = &self.base.db.client;
match sqlquery(query)
.bind::<&String>(&databeam::utility::uuid())
.bind::<&String>(&username.to_lowercase())
.bind::<&String>(&rainbeam_shared::hash::hash_salted(password, salt.clone()))
.bind::<&String>(
&serde_json::to_string::<Vec<String>>(&vec![user_token_hashed]).unwrap(),
)
.bind::<&String>(
&serde_json::to_string::<ProfileMetadata>(&ProfileMetadata::default()).unwrap(),
)
.bind::<&String>(×tamp)
.bind::<i32>(0)
.bind::<&String>(&salt)
.bind::<&String>(&serde_json::to_string::<Vec<String>>(&vec![user_ip]).unwrap())
.bind::<&str>("[]")
.bind::<i32>(0)
.bind::<&str>("[]")
.bind::<&str>("")
.execute(c)
.await
{
Ok(_) => Ok(user_token_unhashed),
Err(_) => Err(DatabaseError::Other),
}
}
pub fn allowed_custom_keys(&self) -> Vec<&'static str> {
vec![
"sparkler:display_name",
"sparkler:status_note",
"sparkler:status_emoji",
"sparkler:limited_friend_requests",
"sparkler:limited_chats",
"sparkler:private_profile",
"sparkler:allow_drawings",
"sparkler:biography",
"sparkler:sidebar",
"sparkler:avatar_url",
"sparkler:banner_url",
"sparkler:banner_fit",
"sparkler:website_theme",
"sparkler:allow_profile_themes",
"sparkler:motivational_header",
"sparkler:warning",
"sparkler:anonymous_username",
"sparkler:anonymous_avatar",
"sparkler:pinned",
"sparkler:profile_theme",
"sparkler:desktop_tl_logo",
"sparkler:layout",
"sparkler:nav_layout",
"sparkler:custom_css",
"sparkler:color_surface",
"sparkler:color_lowered",
"sparkler:color_super_lowered",
"sparkler:color_raised",
"sparkler:color_super_raised",
"sparkler:color_text",
"sparkler:color_text_raised",
"sparkler:color_text_lowered",
"sparkler:color_link",
"sparkler:color_primary",
"sparkler:color_primary_lowered",
"sparkler:color_primary_raised",
"sparkler:color_text_primary",
"sparkler:color_shadow",
"sparkler:lock_profile",
"sparkler:disallow_anonymous",
"sparkler:disallow_anonymous_comments",
"sparkler:require_account",
"sparkler:private_social",
"sparkler:disable_mailbox",
"sparkler:private_comments",
"sparkler:filter",
"sparkler:mail_signature",
"rainbeam:verify_url",
"rainbeam:verify_code",
]
}
pub async fn update_profile_metadata(
&self,
id: String,
mut metadata: ProfileMetadata,
) -> Result<()> {
let profile = match self.get_profile(id.clone()).await {
Ok(ua) => ua,
Err(e) => return Err(e),
};
let allowed_custom_keys = self.allowed_custom_keys();
for kv in metadata.kv.clone() {
if !allowed_custom_keys.contains(&kv.0.as_str()) {
metadata.kv.remove(&kv.0);
}
}
if !metadata.check() {
return Err(DatabaseError::TooLong);
}
let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"UPDATE \"xprofiles\" SET \"metadata\" = ? WHERE \"id\" = ?"
} else {
"UPDATE \"xprofiles\" SET (\"metadata\") = ($1) WHERE \"id\" = $2"
};
let c = &self.base.db.client;
let meta = &serde_json::to_string(&metadata).unwrap();
match sqlquery(query)
.bind::<&String>(meta)
.bind::<&String>(&id)
.execute(c)
.await
{
Ok(_) => {
self.base
.cachedb
.remove(format!("rbeam.auth.profile:{}", profile.username))
.await;
self.base
.cachedb
.remove(format!("rbeam.auth.profile:{}", profile.id))
.await;
Ok(())
}
Err(_) => Err(DatabaseError::Other),
}
}
pub async fn update_profile_tokens(
&self,
id: String,
tokens: Vec<String>,
ips: Vec<String>,
token_context: Vec<TokenContext>,
) -> Result<()> {
let ua = match self.get_profile(id.clone()).await {
Ok(ua) => ua,
Err(e) => return Err(e),
};
let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"UPDATE \"xprofiles\" SET \"tokens\" = ?, \"ips\" = ?, \"token_context\" = ? WHERE \"id\" = ?"
} else {
"UPDATE \"xprofiles\" SET (\"tokens\", \"ips\") = ($1, $2, $3) WHERE \"id\" = $4"
};
let c = &self.base.db.client;
let tokens = &serde_json::to_string(&tokens).unwrap();
let ips = &serde_json::to_string(&ips).unwrap();
let token_context = &serde_json::to_string(&token_context).unwrap();
match sqlquery(query)
.bind::<&String>(tokens)
.bind::<&String>(ips)
.bind::<&String>(token_context)
.bind::<&String>(&ua.id)
.execute(c)
.await
{
Ok(_) => {
self.base
.cachedb
.remove(format!("rbeam.auth.profile:{}", ua.username))
.await;
self.base
.cachedb
.remove(format!("rbeam.auth.profile:{}", ua.id))
.await;
Ok(())
}
Err(_) => Err(DatabaseError::Other),
}
}
pub async fn update_profile_badges(
&self,
id: String,
badges: Vec<(String, String, String)>,
) -> Result<()> {
let ua = match self.get_profile(id.clone()).await {
Ok(ua) => ua,
Err(e) => return Err(e),
};
let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"UPDATE \"xprofiles\" SET \"badges\" = ? WHERE \"id\" = ?"
} else {
"UPDATE \"xprofiles\" SET (\"badges\") = ($1) WHERE \"id\" = $2"
};
let c = &self.base.db.client;
let badges = &serde_json::to_string(&badges).unwrap();
match sqlquery(query)
.bind::<&String>(badges)
.bind::<&String>(&id)
.execute(c)
.await
{
Ok(_) => {
self.base
.cachedb
.remove(format!("rbeam.auth.profile:{}", ua.username))
.await;
self.base
.cachedb
.remove(format!("rbeam.auth.profile:{}", ua.id))
.await;
Ok(())
}
Err(_) => Err(DatabaseError::Other),
}
}
pub async fn update_profile_labels(&self, id: String, labels: Vec<String>) -> Result<()> {
let ua = match self.get_profile(id.clone()).await {
Ok(ua) => ua,
Err(e) => return Err(e),
};
let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"UPDATE \"xprofiles\" SET \"labels\" = ? WHERE \"id\" = ?"
} else {
"UPDATE \"xprofiles\" SET (\"labels\") = ($1) WHERE \"id\" = $2"
};
let c = &self.base.db.client;
let labels = &serde_json::to_string(&labels).unwrap();
match sqlquery(query)
.bind::<&String>(labels)
.bind::<&String>(&id)
.execute(c)
.await
{
Ok(_) => {
self.base
.cachedb
.remove(format!("rbeam.auth.profile:{}", ua.username))
.await;
self.base
.cachedb
.remove(format!("rbeam.auth.profile:{}", ua.id))
.await;
Ok(())
}
Err(_) => Err(DatabaseError::Other),
}
}
pub async fn update_profile_tier(&self, id: String, tier: i32) -> Result<()> {
let ua = match self.get_profile(id.clone()).await {
Ok(ua) => ua,
Err(e) => return Err(e),
};
let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"UPDATE \"xprofiles\" SET \"tier\" = ? WHERE \"id\" = ?"
} else {
"UPDATE \"xprofiles\" SET (\"tier\") = ($1) WHERE \"id\" = $2"
};
let c = &self.base.db.client;
match sqlquery(query)
.bind::<&String>(&tier.to_string())
.bind::<&String>(&id)
.execute(c)
.await
{
Ok(_) => {
self.base
.cachedb
.remove(format!("rbeam.auth.profile:{}", ua.username))
.await;
self.base
.cachedb
.remove(format!("rbeam.auth.profile:{}", ua.id))
.await;
Ok(())
}
Err(_) => Err(DatabaseError::Other),
}
}
pub async fn update_profile_group(&self, id: String, group: i32) -> Result<()> {
let ua = match self.get_profile(id.clone()).await {
Ok(ua) => ua,
Err(e) => return Err(e),
};
let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"UPDATE \"xprofiles\" SET \"gid\" = ? WHERE \"id\" = ?"
} else {
"UPDATE \"xprofiles\" SET (\"gid\") = ($1) WHERE \"id\" = $2"
};
let c = &self.base.db.client;
match sqlquery(query)
.bind::<&i32>(&group)
.bind::<&String>(&id)
.execute(c)
.await
{
Ok(_) => {
self.base
.cachedb
.remove(format!("rbeam.auth.profile:{}", ua.username))
.await;
self.base
.cachedb
.remove(format!("rbeam.auth.profile:{}", ua.id))
.await;
Ok(())
}
Err(_) => Err(DatabaseError::Other),
}
}
pub async fn update_profile_password(
&self,
id: String,
password: String,
new_password: String,
do_password_check: bool,
) -> Result<()> {
let ua = match self.get_profile(id.clone()).await {
Ok(ua) => ua,
Err(e) => return Err(e),
};
if do_password_check {
let password_hashed = rainbeam_shared::hash::hash_salted(password, ua.salt);
if password_hashed != ua.password {
return Err(DatabaseError::NotAllowed);
}
}
let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"UPDATE \"xprofiles\" SET \"password\" = ?, \"salt\" = ? WHERE \"id\" = ?"
} else {
"UPDATE \"xprofiles\" SET (\"password\", \"salt\") = ($1, $2) WHERE \"id\" = $3"
};
let new_salt = rainbeam_shared::hash::salt();
let c = &self.base.db.client;
match sqlquery(query)
.bind::<&String>(&rainbeam_shared::hash::hash_salted(
new_password,
new_salt.clone(),
))
.bind::<&String>(&new_salt)
.bind::<&String>(&id)
.execute(c)
.await
{
Ok(_) => {
self.base
.cachedb
.remove(format!("rbeam.auth.profile:{}", ua.username))
.await;
self.base
.cachedb
.remove(format!("rbeam.auth.profile:{}", ua.id))
.await;
Ok(())
}
Err(_) => Err(DatabaseError::Other),
}
}
pub async fn update_profile_username(
&self,
id: String,
password: String,
mut new_name: String,
) -> Result<()> {
new_name = new_name.to_lowercase();
let ua = match self.get_profile(id.clone()).await {
Ok(ua) => ua,
Err(e) => return Err(e),
};
if let Ok(_) = self.get_profile_by_username(new_name.clone()).await {
return Err(DatabaseError::MustBeUnique);
}
if let Err(e) = Database::validate_username(new_name.clone()) {
return Err(e);
}
let password_hashed = rainbeam_shared::hash::hash_salted(password, ua.salt);
if password_hashed != ua.password {
return Err(DatabaseError::NotAllowed);
}
let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"UPDATE \"xprofiles\" SET \"username\" = ? WHERE \"id\" = ?"
} else {
"UPDATE \"xprofiles\" SET (\"username\") = ($1) WHERE \"id\" = $2"
};
let c = &self.base.db.client;
match sqlquery(query)
.bind::<&String>(&new_name)
.bind::<&String>(&id)
.execute(c)
.await
{
Ok(_) => {
self.base
.cachedb
.remove(format!("rbeam.auth.profile:{}", ua.username))
.await;
self.base
.cachedb
.remove(format!("rbeam.auth.profile:{}", ua.id))
.await;
Ok(())
}
Err(_) => Err(DatabaseError::Other),
}
}
async fn delete_profile(&self, id: String) -> Result<()> {
let user = self.get_profile_by_id(id.clone()).await.unwrap();
let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"DELETE FROM \"xprofiles\" WHERE \"id\" = ?"
} else {
"DELETE FROM \"xprofiles\" WHERE \"id\" = $1"
};
let c = &self.base.db.client;
match sqlquery(query).bind::<&String>(&id).execute(c).await {
Ok(_) => {
let query: &str =
if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"DELETE FROM \"xnotifications\" WHERE \"recipient\" = ?"
} else {
"DELETE FROM \"xnotifications\" WHERE \"recipient\" = $1"
};
if let Err(_) = sqlquery(query).bind::<&String>(&id).execute(c).await {
return Err(DatabaseError::Other);
};
let query: &str =
if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"DELETE FROM \"xwarnings\" WHERE \"recipient\" = ?"
} else {
"DELETE FROM \"xwarnings\" WHERE \"recipient\" = $1"
};
if let Err(_) = sqlquery(query).bind::<&String>(&id).execute(c).await {
return Err(DatabaseError::Other);
};
let query: &str =
if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"DELETE FROM \"xfollows\" WHERE \"user\" = ? OR \"following\" = ?"
} else {
"DELETE FROM \"xfollows\" WHERE \"user\" = $1 OR \"following\" = $2"
};
if let Err(_) = sqlquery(query)
.bind::<&String>(&id)
.bind::<&String>(&id)
.execute(c)
.await
{
return Err(DatabaseError::Other);
};
let query: &str =
if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"DELETE FROM \"xquestions\" WHERE \"recipient\" = ?"
} else {
"DELETE FROM \"xquestions\" WHERE \"recipient\" = $1"
};
if let Err(_) = sqlquery(query).bind::<&String>(&id).execute(c).await {
return Err(DatabaseError::Other);
};
let query: &str =
if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"DELETE FROM \"xquestions\" WHERE \"author\" = ?"
} else {
"DELETE FROM \"xquestions\" WHERE \"author\" = $1"
};
if let Err(_) = sqlquery(query).bind::<&String>(&id).execute(c).await {
return Err(DatabaseError::Other);
};
let query: &str =
if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"DELETE FROM \"xresponses\" WHERE \"author\" = ?"
} else {
"DELETE FROM \"xresponses\" WHERE \"author\" = $1"
};
if let Err(_) = sqlquery(query).bind::<&String>(&id).execute(c).await {
return Err(DatabaseError::Other);
};
let query: &str =
if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"DELETE FROM \"xresponses\" WHERE \"question\" LIKE ?"
} else {
"DELETE FROM \"xresponses\" WHERE \"question\" LIKE $1"
};
if let Err(_) = sqlquery(query)
.bind::<&String>(&format!("%\"author\":\"{id}\"%"))
.execute(c)
.await
{
return Err(DatabaseError::Other);
};
self.base
.cachedb
.remove(format!("rbeam.app.response_count:{}", id))
.await;
self.base
.cachedb
.remove(format!("rbeam.app.global_question_count:{}", id))
.await;
let query: &str =
if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"DELETE FROM \"xcircles\" WHERE \"owner\" = ?"
} else {
"DELETE FROM \"xcircles\" WHERE \"owner\" = $1"
};
if let Err(_) = sqlquery(query).bind::<&String>(&id).execute(c).await {
return Err(DatabaseError::Other);
};
let query: &str =
if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"DELETE FROM \"xcircle_memberships\" WHERE \"user\" = ?"
} else {
"DELETE FROM \"xcircle_memberships\" WHERE \"user\" = $1"
};
if let Err(_) = sqlquery(query).bind::<&String>(&id).execute(c).await {
return Err(DatabaseError::Other);
};
let query: &str =
if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"DELETE FROM \"xrelationships\" WHERE \"one\" = ? OR \"two\" = ?"
} else {
"DELETE FROM \"xrelationships\" WHERE \"one\" = $1 OR \"two\" = $2"
};
if let Err(_) = sqlquery(query)
.bind::<&String>(&id)
.bind::<&String>(&id)
.execute(c)
.await
{
return Err(DatabaseError::Other);
};
self.base
.cachedb
.remove(format!("rbeam.app.friends_count:{}", id))
.await;
let query: &str =
if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"DELETE FROM \"xipblocks\" WHERE \"user\" = ?"
} else {
"DELETE FROM \"xipblocks\" WHERE \"user\" = $1"
};
if let Err(_) = sqlquery(query).bind::<&String>(&id).execute(c).await {
return Err(DatabaseError::Other);
};
self.base
.cachedb
.remove(format!("rbeam.auth.profile:{}", id))
.await;
self.base
.cachedb
.remove(format!("rbeam.auth.profile:{}", user.username))
.await;
self.base
.cachedb
.remove(format!("rbeam.auth.followers_count:{}", id))
.await;
self.base
.cachedb
.remove(format!("rbeam.auth.following_count:{}", id))
.await;
self.base
.cachedb
.remove(format!("rbeam.auth.notification_count:{}", id))
.await;
Ok(())
}
Err(_) => Err(DatabaseError::Other),
}
}
pub async fn delete_profile_by_id(&self, id: String) -> Result<()> {
let user = match self.get_profile_by_id(id.clone()).await {
Ok(ua) => ua,
Err(e) => return Err(e),
};
let group = match self.get_group_by_id(user.group).await {
Ok(g) => g,
Err(_) => return Err(DatabaseError::Other),
};
if group.permissions.contains(&Permission::Manager) {
return Err(DatabaseError::NotAllowed);
}
self.delete_profile(id).await
}
pub async fn get_group_by_id(&self, id: i32) -> Result<Group> {
let cached = self
.base
.cachedb
.get(format!("rbeam.auth.gid:{}", id))
.await;
if cached.is_some() {
return Ok(serde_json::from_str::<Group>(cached.unwrap().as_str()).unwrap());
}
let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"SELECT * FROM \"xgroups\" WHERE \"id\" = ?"
} else {
"SELECT * FROM \"xgroups\" WHERE \"id\" = $1"
};
let c = &self.base.db.client;
let row = match sqlquery(query).bind::<&i32>(&id).fetch_one(c).await {
Ok(r) => self.base.textify_row(r, Vec::new()).0,
Err(_) => return Ok(Group::default()),
};
let group = Group {
name: row.get("name").unwrap().to_string(),
id: row.get("id").unwrap().parse::<i32>().unwrap(),
permissions: match serde_json::from_str(row.get("permissions").unwrap()) {
Ok(m) => m,
Err(_) => return Err(DatabaseError::ValueError),
},
};
self.base
.cachedb
.set(
format!("rbeam.auth.gid:{}", id),
serde_json::to_string::<Group>(&group).unwrap(),
)
.await;
Ok(group)
}
pub async fn get_follow(&self, user: String, following: String) -> Result<UserFollow> {
let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"SELECT * FROM \"xfollows\" WHERE \"user\" = ? AND \"following\" = ?"
} else {
"SELECT * FROM \"xfollows\" WHERE \"user\" = $1 AND \"following\" = $2"
};
let c = &self.base.db.client;
let row = match sqlquery(query)
.bind::<&String>(&user)
.bind::<&String>(&following)
.fetch_one(c)
.await
{
Ok(u) => self.base.textify_row(u, Vec::new()).0,
Err(_) => return Err(DatabaseError::Other),
};
Ok(UserFollow {
user: row.get("user").unwrap().to_string(),
following: row.get("following").unwrap().to_string(),
})
}
pub async fn get_followers(
&self,
user: String,
) -> Result<Vec<(UserFollow, Box<Profile>, Box<Profile>)>> {
let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"SELECT * FROM \"xfollows\" WHERE \"following\" = ?"
} else {
"SELECT * FROM \"xfollows\" WHERE \"following\" = $1"
};
let c = &self.base.db.client;
let res = match sqlquery(query).bind::<&String>(&user).fetch_all(c).await {
Ok(u) => {
let mut out = Vec::new();
for row in u {
let row = self.base.textify_row(row, Vec::new()).0;
let user = row.get("user").unwrap().to_string();
let following = row.get("following").unwrap().to_string();
out.push((
UserFollow {
user: user.clone(),
following: following.clone(),
},
match self.get_profile_by_id(user).await {
Ok(ua) => ua,
Err(e) => return Err(e),
},
match self.get_profile_by_id(following).await {
Ok(ua) => ua,
Err(e) => return Err(e),
},
))
}
out
}
Err(_) => return Err(DatabaseError::Other),
};
Ok(res)
}
pub async fn get_followers_paginated(
&self,
user: String,
page: i32,
) -> Result<Vec<(UserFollow, Box<Profile>, Box<Profile>)>> {
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
format!(
"SELECT * FROM \"xfollows\" WHERE \"following\" = ? LIMIT 50 OFFSET {}",
page * 50
)
} else {
format!(
"SELECT * FROM \"xfollows\" WHERE \"following\" = $1 LIMIT 50 OFFSET {}",
page * 50
)
};
let c = &self.base.db.client;
let res = match sqlquery(&query).bind::<&String>(&user).fetch_all(c).await {
Ok(u) => {
let mut out = Vec::new();
for row in u {
let row = self.base.textify_row(row, Vec::new()).0;
let user = row.get("user").unwrap().to_string();
let following = row.get("following").unwrap().to_string();
out.push((
UserFollow {
user: user.clone(),
following: following.clone(),
},
match self.get_profile_by_id(user.clone()).await {
Ok(ua) => ua,
Err(e) => {
println!("({}) UID 'user' {}", e.to_string(), user);
continue;
}
},
match self.get_profile_by_id(following.clone()).await {
Ok(ua) => ua,
Err(e) => {
println!("({}) UID 'following' {}", e.to_string(), following);
continue;
}
},
))
}
out
}
Err(_) => return Err(DatabaseError::Other),
};
Ok(res)
}
pub async fn get_followers_count(&self, user: String) -> usize {
if let Some(count) = self
.base
.cachedb
.get(format!("rbeam.auth.followers_count:{}", user))
.await
{
return count.parse::<usize>().unwrap_or(0);
};
let count = self
.get_followers(user.clone())
.await
.unwrap_or(Vec::new())
.len();
self.base
.cachedb
.set(
format!("rbeam.auth.followers_count:{}", user),
count.to_string(),
)
.await;
count
}
pub async fn get_following(
&self,
user: String,
) -> Result<Vec<(UserFollow, Box<Profile>, Box<Profile>)>> {
let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"SELECT * FROM \"xfollows\" WHERE \"user\" = ?"
} else {
"SELECT * FROM \"xfollows\" WHERE \"user\" = $1"
};
let c = &self.base.db.client;
let res = match sqlquery(query).bind::<&String>(&user).fetch_all(c).await {
Ok(u) => {
let mut out = Vec::new();
for row in u {
let row = self.base.textify_row(row, Vec::new()).0;
let user = row.get("user").unwrap().to_string();
let following = row.get("following").unwrap().to_string();
out.push((
UserFollow {
user: user.clone(),
following: following.clone(),
},
match self.get_profile_by_id(user.clone()).await {
Ok(ua) => ua,
Err(e) => {
println!("({}) UID 'user' {}", e.to_string(), user);
continue;
}
},
match self.get_profile_by_id(following.clone()).await {
Ok(ua) => ua,
Err(e) => {
println!("({}) UID 'following' {}", e.to_string(), following);
continue;
}
},
))
}
out
}
Err(_) => return Err(DatabaseError::Other),
};
Ok(res)
}
pub async fn get_following_paginated(
&self,
user: String,
page: i32,
) -> Result<Vec<(UserFollow, Box<Profile>, Box<Profile>)>> {
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
format!(
"SELECT * FROM \"xfollows\" WHERE \"user\" = ? LIMIT 50 OFFSET {}",
page * 50
)
} else {
format!(
"SELECT * FROM \"xfollows\" WHERE \"user\" = $1 LIMIT 50 OFFSET {}",
page * 50
)
};
let c = &self.base.db.client;
let res = match sqlquery(&query).bind::<&String>(&user).fetch_all(c).await {
Ok(u) => {
let mut out = Vec::new();
for row in u {
let row = self.base.textify_row(row, Vec::new()).0;
let user = row.get("user").unwrap().to_string();
let following = row.get("following").unwrap().to_string();
out.push((
UserFollow {
user: user.clone(),
following: following.clone(),
},
match self.get_profile_by_id(user.clone()).await {
Ok(ua) => ua,
Err(e) => {
println!("({}) UID 'user' {}", e.to_string(), user);
continue;
}
},
match self.get_profile_by_id(following.clone()).await {
Ok(ua) => ua,
Err(e) => {
println!("({}) UID 'following' {}", e.to_string(), following);
continue;
}
},
))
}
out
}
Err(_) => return Err(DatabaseError::Other),
};
Ok(res)
}
pub async fn get_following_count(&self, user: String) -> usize {
if let Some(count) = self
.base
.cachedb
.get(format!("rbeam.auth.following_count:{}", user))
.await
{
return count.parse::<usize>().unwrap_or(0);
};
let count = self
.get_following(user.clone())
.await
.unwrap_or(Vec::new())
.len();
self.base
.cachedb
.set(
format!("rbeam.auth.following_count:{}", user),
count.to_string(),
)
.await;
count
}
pub async fn toggle_user_follow(&self, props: &mut UserFollow) -> Result<()> {
if props.user == props.following {
return Err(DatabaseError::Other);
}
let user_1 = match self.get_profile_by_username(props.user.to_owned()).await {
Ok(ua) => ua,
Err(e) => return Err(e),
};
if let Err(e) = self
.get_profile_by_username(props.following.to_owned())
.await
{
return Err(e);
};
if let Ok(_) = self
.get_follow(props.user.to_owned(), props.following.to_owned())
.await
{
let query: String =
if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"DELETE FROM \"xfollows\" WHERE \"user\" = ? AND \"following\" = ?"
} else {
"DELETE FROM \"xfollows\" WHERE \"user\" = $1 AND \"following\" = $2"
}
.to_string();
let c = &self.base.db.client;
match sqlquery(&query)
.bind::<&String>(&props.user)
.bind::<&String>(&props.following)
.execute(c)
.await
{
Ok(_) => {
self.base
.cachedb
.decr(format!("rbeam.auth.following_count:{}", props.user))
.await;
self.base
.cachedb
.decr(format!("rbeam.auth.followers_count:{}", props.following))
.await;
return Ok(());
}
Err(_) => return Err(DatabaseError::Other),
};
}
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"INSERT INTO \"xfollows\" VALUES (?, ?)"
} else {
"INSERT INTO \"xfollows\" VALEUS ($1, $2)"
}
.to_string();
let c = &self.base.db.client;
match sqlquery(&query)
.bind::<&String>(&props.user)
.bind::<&String>(&props.following)
.execute(c)
.await
{
Ok(_) => {
self.base
.cachedb
.incr(format!("rbeam.auth.following_count:{}", props.user))
.await;
self.base
.cachedb
.incr(format!("rbeam.auth.followers_count:{}", props.following))
.await;
if let Err(e) = self
.create_notification(
NotificationCreate {
title: format!(
"[@{}](/+u/{}) followed you!",
user_1.username, user_1.id
),
content: String::new(),
address: format!("/+u/{}", user_1.id),
recipient: props.following.clone(),
},
None,
)
.await
{
return Err(e);
};
Ok(())
}
Err(_) => Err(DatabaseError::Other),
}
}
pub async fn force_remove_user_follow(&self, props: &mut UserFollow) -> Result<()> {
if props.user == props.following {
return Err(DatabaseError::Other);
}
if let Ok(_) = self
.get_follow(props.user.to_owned(), props.following.to_owned())
.await
{
let query: String =
if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"DELETE FROM \"xfollows\" WHERE \"user\" = ? AND \"following\" = ?"
} else {
"DELETE FROM \"xfollows\" WHERE \"user\" = $1 AND \"following\" = $2"
}
.to_string();
let c = &self.base.db.client;
match sqlquery(&query)
.bind::<&String>(&props.user)
.bind::<&String>(&props.following)
.execute(c)
.await
{
Ok(_) => {
self.base
.cachedb
.decr(format!("rbeam.auth.following_count:{}", props.user))
.await;
self.base
.cachedb
.decr(format!("rbeam.auth.followers_count:{}", props.following))
.await;
return Ok(());
}
Err(_) => return Err(DatabaseError::Other),
};
}
Ok(())
}
pub async fn get_notification(&self, id: String) -> Result<Notification> {
match self
.base
.cachedb
.get(format!("rbeam.auth.notification:{}", id))
.await
{
Some(c) => return Ok(serde_json::from_str::<Notification>(c.as_str()).unwrap()),
None => (),
};
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"SELECT * FROM \"xnotifications\" WHERE \"id\" = ?"
} else {
"SELECT * FROM \"xnotifications\" WHERE \"id\" = $1"
}
.to_string();
let c = &self.base.db.client;
let res = match sqlquery(&query).bind::<&String>(&id).fetch_one(c).await {
Ok(p) => self.base.textify_row(p, Vec::new()).0,
Err(_) => return Err(DatabaseError::NotFound),
};
let notification = Notification {
title: res.get("title").unwrap().to_string(),
content: res.get("content").unwrap().to_string(),
address: res.get("address").unwrap().to_string(),
timestamp: res.get("timestamp").unwrap().parse::<u128>().unwrap(),
id: res.get("id").unwrap().to_string(),
recipient: res.get("recipient").unwrap().to_string(),
};
self.base
.cachedb
.set(
format!("rbeam.auth.notification:{}", id),
serde_json::to_string::<Notification>(¬ification).unwrap(),
)
.await;
Ok(notification)
}
pub async fn get_notifications_by_recipient(
&self,
recipient: String,
) -> Result<Vec<Notification>> {
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"SELECT * FROM \"xnotifications\" WHERE \"recipient\" = ? ORDER BY \"timestamp\" DESC"
} else {
"SELECT * FROM \"xnotifications\" WHERE \"recipient\" = $1 ORDER BY \"timestamp\" DESC"
}
.to_string();
let c = &self.base.db.client;
let res = match sqlquery(&query)
.bind::<&String>(&recipient.to_lowercase())
.fetch_all(c)
.await
{
Ok(p) => {
let mut out: Vec<Notification> = Vec::new();
for row in p {
let res = self.base.textify_row(row, Vec::new()).0;
out.push(Notification {
title: res.get("title").unwrap().to_string(),
content: res.get("content").unwrap().to_string(),
address: res.get("address").unwrap().to_string(),
timestamp: res.get("timestamp").unwrap().parse::<u128>().unwrap(),
id: res.get("id").unwrap().to_string(),
recipient: res.get("recipient").unwrap().to_string(),
});
}
out
}
Err(_) => return Err(DatabaseError::NotFound),
};
Ok(res)
}
pub async fn get_notification_count_by_recipient(&self, recipient: String) -> usize {
if let Some(count) = self
.base
.cachedb
.get(format!("rbeam.auth.notification_count:{}", recipient))
.await
{
return count.parse::<usize>().unwrap_or(0);
};
let count = self
.get_notifications_by_recipient(recipient.clone())
.await
.unwrap_or(Vec::new())
.len();
self.base
.cachedb
.set(
format!("rbeam.auth.notification_count:{}", recipient),
count.to_string(),
)
.await;
count
}
pub async fn get_notifications_by_recipient_paginated(
&self,
recipient: String,
page: i32,
) -> Result<Vec<Notification>> {
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
format!("SELECT * FROM \"xnotifications\" WHERE \"recipient\" = ? ORDER BY \"timestamp\" DESC LIMIT 50 OFFSET {}", page * 50)
} else {
format!("SELECT * FROM \"xnotifications\" WHERE \"recipient\" = $1 ORDER BY \"timestamp\" DESC LIMIT 50 OFFSET {}", page * 50)
};
let c = &self.base.db.client;
let res = match sqlquery(&query)
.bind::<&String>(&recipient.to_lowercase())
.fetch_all(c)
.await
{
Ok(p) => {
let mut out: Vec<Notification> = Vec::new();
for row in p {
let res = self.base.textify_row(row, Vec::new()).0;
out.push(Notification {
title: res.get("title").unwrap().to_string(),
content: res.get("content").unwrap().to_string(),
address: res.get("address").unwrap().to_string(),
timestamp: res.get("timestamp").unwrap().parse::<u128>().unwrap(),
id: res.get("id").unwrap().to_string(),
recipient: res.get("recipient").unwrap().to_string(),
});
}
out
}
Err(_) => return Err(DatabaseError::NotFound),
};
Ok(res)
}
pub async fn create_notification(
&self,
props: NotificationCreate,
id: Option<String>,
) -> Result<()> {
let notification = Notification {
title: props.title,
content: props.content,
address: props.address,
timestamp: utility::unix_epoch_timestamp(),
id: if let Some(id) = id {
id
} else {
utility::random_id()
},
recipient: props.recipient,
};
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"INSERT INTO \"xnotifications\" VALUES (?, ?, ?, ?, ?, ?)"
} else {
"INSERT INTO \"xnotifications\" VALEUS ($1, $2, $3, $4, $5, $6)"
}
.to_string();
let c = &self.base.db.client;
match sqlquery(&query)
.bind::<&String>(¬ification.title)
.bind::<&String>(¬ification.content)
.bind::<&String>(¬ification.address)
.bind::<&String>(¬ification.timestamp.to_string())
.bind::<&String>(¬ification.id)
.bind::<&String>(¬ification.recipient)
.execute(c)
.await
{
Ok(_) => {
self.base
.cachedb
.incr(format!(
"rbeam.auth.notification_count:{}",
notification.recipient
))
.await;
return Ok(());
}
Err(_) => return Err(DatabaseError::Other),
};
}
pub async fn delete_notification(&self, id: String, user: Box<Profile>) -> Result<()> {
let notification = match self.get_notification(id.clone()).await {
Ok(n) => n,
Err(e) => return Err(e),
};
if user.id != notification.recipient {
let group = match self.get_group_by_id(user.group).await {
Ok(g) => g,
Err(_) => return Err(DatabaseError::Other),
};
if !group.permissions.contains(&Permission::Helper) {
return Err(DatabaseError::NotAllowed);
}
}
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"DELETE FROM \"xnotifications\" WHERE \"id\" = ?"
} else {
"DELETE FROM \"xnotifications\" WHERE \"id\" = $1"
}
.to_string();
let c = &self.base.db.client;
match sqlquery(&query).bind::<&String>(&id).execute(c).await {
Ok(_) => {
self.base
.cachedb
.decr(format!(
"rbeam.auth.notification_count:{}",
notification.recipient
))
.await;
self.base
.cachedb
.remove(format!("rbeam.auth.notification:{}", id))
.await;
return Ok(());
}
Err(_) => return Err(DatabaseError::Other),
};
}
pub async fn delete_notifications_by_recipient(
&self,
recipient: String,
user: Box<Profile>,
) -> Result<()> {
let notifications = match self.get_notifications_by_recipient(recipient.clone()).await {
Ok(n) => n,
Err(e) => return Err(e),
};
if user.id != recipient {
let group = match self.get_group_by_id(user.group).await {
Ok(g) => g,
Err(_) => return Err(DatabaseError::Other),
};
if !group.permissions.contains(&Permission::Helper) {
return Err(DatabaseError::NotAllowed);
}
}
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"DELETE FROM \"xnotifications\" WHERE \"recipient\" = ?"
} else {
"DELETE FROM \"xnotifications\" WHERE \"recipient\" = $1"
}
.to_string();
let c = &self.base.db.client;
match sqlquery(&query)
.bind::<&String>(&recipient)
.execute(c)
.await
{
Ok(_) => {
self.base
.cachedb
.remove(format!("rbeam.auth.notification_count:{}", recipient))
.await;
for notification in notifications {
self.base
.cachedb
.remove(format!("rbeam.auth.notification:{}", notification.id))
.await;
}
return Ok(());
}
Err(_) => return Err(DatabaseError::Other),
};
}
pub async fn get_warning(&self, id: String) -> Result<Warning> {
match self
.base
.cachedb
.get(format!("rbeam.auth.warning:{}", id))
.await
{
Some(c) => return Ok(serde_json::from_str::<Warning>(c.as_str()).unwrap()),
None => (),
};
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"SELECT * FROM \"xwarnings\" WHERE \"id\" = ?"
} else {
"SELECT * FROM \"xwarnings\" WHERE \"id\" = $1"
}
.to_string();
let c = &self.base.db.client;
let res = match sqlquery(&query).bind::<&String>(&id).fetch_one(c).await {
Ok(p) => self.base.textify_row(p, Vec::new()).0,
Err(_) => return Err(DatabaseError::NotFound),
};
let warning = Warning {
id: res.get("id").unwrap().to_string(),
content: res.get("content").unwrap().to_string(),
timestamp: res.get("timestamp").unwrap().parse::<u128>().unwrap(),
recipient: res.get("recipient").unwrap().to_string(),
moderator: match self
.get_profile_by_id(res.get("moderator").unwrap().to_string())
.await
{
Ok(ua) => ua,
Err(e) => return Err(e),
},
};
self.base
.cachedb
.set(
format!("rbeam.auth.warning:{}", id),
serde_json::to_string::<Warning>(&warning).unwrap(),
)
.await;
Ok(warning)
}
pub async fn get_warnings_by_recipient(
&self,
recipient: String,
user: Box<Profile>,
) -> Result<Vec<Warning>> {
let group = match self.get_group_by_id(user.group).await {
Ok(g) => g,
Err(_) => return Err(DatabaseError::Other),
};
if !group.permissions.contains(&Permission::Helper) {
return Err(DatabaseError::NotAllowed);
}
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"SELECT * FROM \"xwarnings\" WHERE \"recipient\" = ? ORDER BY \"timestamp\" DESC"
} else {
"SELECT * FROM \"xwarnings\" WHERE \"recipient\" = $1 ORDER BY \"timestamp\" DESC"
}
.to_string();
let c = &self.base.db.client;
let res = match sqlquery(&query)
.bind::<&String>(&recipient.to_lowercase())
.fetch_all(c)
.await
{
Ok(p) => {
let mut out: Vec<Warning> = Vec::new();
for row in p {
let res = self.base.textify_row(row, Vec::new()).0;
out.push(Warning {
id: res.get("id").unwrap().to_string(),
content: res.get("content").unwrap().to_string(),
timestamp: res.get("timestamp").unwrap().parse::<u128>().unwrap(),
recipient: res.get("recipient").unwrap().to_string(),
moderator: match self
.get_profile_by_id(res.get("moderator").unwrap().to_string())
.await
{
Ok(ua) => ua,
Err(_) => continue,
},
});
}
out
}
Err(_) => return Err(DatabaseError::NotFound),
};
Ok(res)
}
pub async fn create_warning(&self, props: WarningCreate, user: Box<Profile>) -> Result<()> {
let group = match self.get_group_by_id(user.group).await {
Ok(g) => g,
Err(_) => return Err(DatabaseError::Other),
};
if !group.permissions.contains(&Permission::Helper) {
return Err(DatabaseError::NotAllowed);
}
let warning = Warning {
id: utility::random_id(),
content: props.content,
timestamp: utility::unix_epoch_timestamp(),
recipient: props.recipient,
moderator: user,
};
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"INSERT INTO \"xwarnings\" VALUES (?, ?, ?, ?, ?)"
} else {
"INSERT INTO \"xwarnings\" VALEUS ($1, $2, $3, $4, $5)"
}
.to_string();
let c = &self.base.db.client;
match sqlquery(&query)
.bind::<&String>(&warning.id)
.bind::<&String>(&warning.content)
.bind::<&String>(&warning.timestamp.to_string())
.bind::<&String>(&warning.recipient)
.bind::<&String>(&warning.moderator.id)
.execute(c)
.await
{
Ok(_) => {
if let Err(e) = self
.create_notification(
NotificationCreate {
title: "You have received an account warning!".to_string(),
content: warning.content,
address: String::new(),
recipient: warning.recipient,
},
None,
)
.await
{
return Err(e);
};
return Ok(());
}
Err(_) => return Err(DatabaseError::Other),
};
}
pub async fn delete_warning(&self, id: String, user: Box<Profile>) -> Result<()> {
let warning = match self.get_warning(id.clone()).await {
Ok(n) => n,
Err(e) => return Err(e),
};
if user.id != warning.moderator.id {
let group = match self.get_group_by_id(user.group).await {
Ok(g) => g,
Err(_) => return Err(DatabaseError::Other),
};
if !group.permissions.contains(&Permission::Manager) {
return Err(DatabaseError::NotAllowed);
}
}
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"DELETE FROM \"xwarnings\" WHERE \"id\" = ?"
} else {
"DELETE FROM \"xwarnings\" WHERE \"id\" = $1"
}
.to_string();
let c = &self.base.db.client;
match sqlquery(&query).bind::<&String>(&id).execute(c).await {
Ok(_) => {
self.base
.cachedb
.remove(format!("rbeam.auth.warning:{}", id))
.await;
return Ok(());
}
Err(_) => return Err(DatabaseError::Other),
};
}
pub async fn get_ipban(&self, id: String) -> Result<IpBan> {
match self
.base
.cachedb
.get(format!("rbeam.auth.ipban:{}", id))
.await
{
Some(c) => return Ok(serde_json::from_str::<IpBan>(c.as_str()).unwrap()),
None => (),
};
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"SELECT * FROM \"xbans\" WHERE \"id\" = ?"
} else {
"SELECT * FROM \"xbans\" WHERE \"id\" = $1"
}
.to_string();
let c = &self.base.db.client;
let res = match sqlquery(&query).bind::<&String>(&id).fetch_one(c).await {
Ok(p) => self.base.textify_row(p, Vec::new()).0,
Err(_) => return Err(DatabaseError::NotFound),
};
let ban = IpBan {
id: res.get("id").unwrap().to_string(),
ip: res.get("ip").unwrap().to_string(),
reason: res.get("reason").unwrap().to_string(),
moderator: match self
.get_profile_by_id(res.get("moderator").unwrap().to_string())
.await
{
Ok(ua) => ua,
Err(e) => return Err(e),
},
timestamp: res.get("timestamp").unwrap().parse::<u128>().unwrap(),
};
self.base
.cachedb
.set(
format!("rbeam.auth.ipban:{}", id),
serde_json::to_string::<IpBan>(&ban).unwrap(),
)
.await;
Ok(ban)
}
pub async fn get_ipban_by_ip(&self, ip: String) -> Result<IpBan> {
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"SELECT * FROM \"xbans\" WHERE \"ip\" = ?"
} else {
"SELECT * FROM \"xbans\" WHERE \"ip\" = $1"
}
.to_string();
let c = &self.base.db.client;
let res = match sqlquery(&query).bind::<&String>(&ip).fetch_one(c).await {
Ok(p) => self.base.textify_row(p, Vec::new()).0,
Err(_) => return Err(DatabaseError::NotFound),
};
let ban = IpBan {
id: res.get("id").unwrap().to_string(),
ip: res.get("ip").unwrap().to_string(),
reason: res.get("reason").unwrap().to_string(),
moderator: match self
.get_profile_by_id(res.get("moderator").unwrap().to_string())
.await
{
Ok(ua) => ua,
Err(e) => return Err(e),
},
timestamp: res.get("timestamp").unwrap().parse::<u128>().unwrap(),
};
Ok(ban)
}
pub async fn get_ipbans(&self, user: Box<Profile>) -> Result<Vec<IpBan>> {
let group = match self.get_group_by_id(user.group).await {
Ok(g) => g,
Err(_) => return Err(DatabaseError::Other),
};
if !group.permissions.contains(&Permission::Helper) {
return Err(DatabaseError::NotAllowed);
}
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"SELECT * FROM \"xbans\" ORDER BY \"timestamp\" DESC"
} else {
"SELECT * FROM \"xbans\" ORDER BY \"timestamp\" DESC"
}
.to_string();
let c = &self.base.db.client;
let res = match sqlquery(&query).fetch_all(c).await {
Ok(p) => {
let mut out: Vec<IpBan> = Vec::new();
for row in p {
let res = self.base.textify_row(row, Vec::new()).0;
out.push(IpBan {
id: res.get("id").unwrap().to_string(),
ip: res.get("ip").unwrap().to_string(),
reason: res.get("reason").unwrap().to_string(),
moderator: match self
.get_profile_by_id(res.get("moderator").unwrap().to_string())
.await
{
Ok(ua) => ua,
Err(_) => continue,
},
timestamp: res.get("timestamp").unwrap().parse::<u128>().unwrap(),
});
}
out
}
Err(_) => return Err(DatabaseError::NotFound),
};
Ok(res)
}
pub async fn create_ipban(&self, props: IpBanCreate, user: Box<Profile>) -> Result<()> {
let group = match self.get_group_by_id(user.group).await {
Ok(g) => g,
Err(_) => return Err(DatabaseError::Other),
};
if !group.permissions.contains(&Permission::Helper) {
return Err(DatabaseError::NotAllowed);
} else {
let actor_id = user.id.clone();
if let Err(e) = self
.create_notification(
NotificationCreate {
title: format!("[{actor_id}](/+u/{actor_id})"),
content: format!("Banned an IP: {}", props.ip),
address: format!("/+u/{actor_id}"),
recipient: "*(audit)".to_string(), },
None,
)
.await
{
return Err(e);
}
}
if self.get_ipban_by_ip(props.ip.clone()).await.is_ok() {
return Err(DatabaseError::MustBeUnique);
}
let ban = IpBan {
id: utility::random_id(),
ip: props.ip,
reason: props.reason,
moderator: user,
timestamp: utility::unix_epoch_timestamp(),
};
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"INSERT INTO \"xbans\" VALUES (?, ?, ?, ?, ?)"
} else {
"INSERT INTO \"xbans\" VALEUS ($1, $2, $3, $4, $5)"
}
.to_string();
let c = &self.base.db.client;
match sqlquery(&query)
.bind::<&String>(&ban.id)
.bind::<&String>(&ban.ip)
.bind::<&String>(&ban.reason)
.bind::<&String>(&ban.moderator.id)
.bind::<&String>(&ban.timestamp.to_string())
.execute(c)
.await
{
Ok(_) => return Ok(()),
Err(_) => return Err(DatabaseError::Other),
};
}
pub async fn delete_ipban(&self, id: String, user: Box<Profile>) -> Result<()> {
let ipban = match self.get_ipban(id.clone()).await {
Ok(n) => n,
Err(e) => return Err(e),
};
if user.id != ipban.moderator.id {
let group = match self.get_group_by_id(user.group).await {
Ok(g) => g,
Err(_) => return Err(DatabaseError::Other),
};
if !group.permissions.contains(&Permission::Manager) {
return Err(DatabaseError::NotAllowed);
} else {
let actor_id = user.id.clone();
if let Err(e) = self
.create_notification(
NotificationCreate {
title: format!("[{actor_id}](/+u/{actor_id})"),
content: format!("Unbanned an IP: {}", ipban.ip),
address: format!("/+u/{actor_id}"),
recipient: "*(audit)".to_string(), },
None,
)
.await
{
return Err(e);
}
}
}
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"DELETE FROM \"xbans\" WHERE \"id\" = ?"
} else {
"DELETE FROM \"xbans\" WHERE \"id\" = $1"
}
.to_string();
let c = &self.base.db.client;
match sqlquery(&query).bind::<&String>(&id).execute(c).await {
Ok(_) => {
self.base
.cachedb
.remove(format!("rbeam.auth.ipban:{}", id))
.await;
return Ok(());
}
Err(_) => return Err(DatabaseError::Other),
};
}
pub async fn get_user_relationship(
&self,
user: String,
other: String,
) -> (RelationshipStatus, String, String) {
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"SELECT * FROM \"xrelationships\" WHERE (\"one\" = ? AND \"two\" = ?) OR (\"one\" = ? AND \"two\" = ?)"
} else {
"SELECT * FROM \"xrelationships\" WHERE (\"one\" = $1 AND \"two\" = $2) OR (\"one\" = $3 AND \"two\" = $4)"
}
.to_string();
let c = &self.base.db.client;
let res = match sqlquery(&query)
.bind::<&String>(&user)
.bind::<&String>(&other)
.bind::<&String>(&other)
.bind::<&String>(&user)
.fetch_one(c)
.await
{
Ok(p) => self.base.textify_row(p, Vec::new()).0,
Err(_) => return (RelationshipStatus::default(), user, other),
};
(
serde_json::from_str(&res.get("status").unwrap()).unwrap(),
res.get("one").unwrap().to_owned(),
res.get("two").unwrap().to_owned(),
)
}
pub async fn set_user_relationship_status(
&self,
one: String,
two: String,
status: RelationshipStatus,
disable_notifications: bool,
) -> Result<()> {
let mut relationship = self.get_user_relationship(one.clone(), two.clone()).await;
if relationship.0 == status {
return Ok(());
}
let mut uone = match self.get_profile(relationship.1).await {
Ok(ua) => ua,
Err(e) => return Err(e),
};
let mut utwo = match self.get_profile(relationship.2).await {
Ok(ua) => ua,
Err(e) => return Err(e),
};
match status {
RelationshipStatus::Blocked => {
if relationship.0 != RelationshipStatus::Unknown && uone.id != one {
let query: String =
if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"DELETE FROM \"xrelationships\" WHERE \"one\" = ? AND \"two\" = ?"
} else {
"DELETE FROM \"xrelationships\" WHERE \"one\" = ? AND \"two\" = ?"
}
.to_string();
let c = &self.base.db.client;
if let Err(_) = sqlquery(&query)
.bind::<&String>(&uone.id)
.bind::<&String>(&utwo.id)
.execute(c)
.await
{
return Err(DatabaseError::Other);
};
relationship.0 = RelationshipStatus::Unknown; uone.id = one;
utwo.id = two;
}
if relationship.0 != RelationshipStatus::Unknown {
if relationship.0 == RelationshipStatus::Friends {
self.base
.cachedb
.decr(format!("rbeam.app.friends_count:{}", uone.id))
.await;
self.base
.cachedb
.decr(format!("rbeam.app.friends_count:{}", utwo.id))
.await;
}
let query: String =
if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"UPDATE \"xrelationships\" SET \"status\" = ? WHERE \"one\" = ? AND \"two\" = ?"
} else {
"UPDATE \"xrelationships\" SET (\"status\") = (?) WHERE \"one\" = ? AND \"two\" = ?"
}
.to_string();
let c = &self.base.db.client;
if let Err(_) = sqlquery(&query)
.bind::<&String>(&serde_json::to_string(&status).unwrap())
.bind::<&String>(&uone.id)
.bind::<&String>(&utwo.id)
.execute(c)
.await
{
return Err(DatabaseError::Other);
};
} else {
let query: String =
if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"INSERT INTO \"xrelationships\" VALUES (?, ?, ?, ?)"
} else {
"INSERT INTO \"xrelationships\" VALEUS ($1, $2, $3, $4)"
}
.to_string();
let c = &self.base.db.client;
if let Err(_) = sqlquery(&query)
.bind::<&String>(&uone.id)
.bind::<&String>(&utwo.id)
.bind::<&String>(&serde_json::to_string(&status).unwrap())
.bind::<&String>(&rainbeam_shared::unix_epoch_timestamp().to_string())
.execute(c)
.await
{
return Err(DatabaseError::Other);
};
}
}
RelationshipStatus::Pending => {
if utwo.metadata.is_true("sparkler:limited_friend_requests") {
if let Err(_) = self.get_follow(utwo.id.clone(), uone.id.clone()).await {
return Err(DatabaseError::NotAllowed);
}
}
let query: String =
if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"INSERT INTO \"xrelationships\" VALUES (?, ?, ?, ?)"
} else {
"INSERT INTO \"xrelationships\" VALEUS ($1, $2, $3, $4)"
}
.to_string();
let c = &self.base.db.client;
if let Err(_) = sqlquery(&query)
.bind::<&String>(&uone.id)
.bind::<&String>(&utwo.id)
.bind::<&String>(&serde_json::to_string(&status).unwrap())
.bind::<&String>(&rainbeam_shared::unix_epoch_timestamp().to_string())
.execute(c)
.await
{
return Err(DatabaseError::Other);
};
if !disable_notifications {
if let Err(_) = self
.create_notification(
NotificationCreate {
title: format!(
"[@{}](/+u/{}) has sent you a friend request!",
uone.username, uone.id
),
content: format!("{} wants to be your friend.", uone.username),
address: format!("/@{}/relationship/friend_accept", uone.id),
recipient: utwo.id,
},
None,
)
.await
{
return Err(DatabaseError::Other);
};
};
}
RelationshipStatus::Friends => {
let query: String =
if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"UPDATE \"xrelationships\" SET \"status\" = ? WHERE \"one\" = ? AND \"two\" = ?"
} else {
"UPDATE \"xrelationships\" SET (\"status\") = (?) WHERE \"one\" = ? AND \"two\" = ?"
}
.to_string();
let c = &self.base.db.client;
if let Err(_) = sqlquery(&query)
.bind::<&String>(&serde_json::to_string(&status).unwrap())
.bind::<&String>(&uone.id)
.bind::<&String>(&utwo.id)
.execute(c)
.await
{
return Err(DatabaseError::Other);
};
self.base
.cachedb
.incr(format!("rbeam.app.friends_count:{}", uone.id))
.await;
self.base
.cachedb
.incr(format!("rbeam.app.friends_count:{}", utwo.id))
.await;
if !disable_notifications {
if let Err(_) = self
.create_notification(
NotificationCreate {
title: "Your friend request has been accepted!".to_string(),
content: format!(
"[@{}](/@{}) has accepted your friend request.",
utwo.username, utwo.username
),
address: String::new(),
recipient: uone.id,
},
None,
)
.await
{
return Err(DatabaseError::Other);
};
};
}
RelationshipStatus::Unknown => {
let query: String =
if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") {
"DELETE FROM \"xrelationships\" WHERE \"one\" = ? AND \"two\" = ?"
} else {
"DELETE FROM \"xrelationships\" WHERE \"one\" = ? AND \"two\" = ?"
}
.to_string();
let c = &self.base.db.client;
if let Err(_) = sqlquery(&query)
.bind::<&String>(&uone.id)
.bind::<&String>(&utwo.id)
.execute(c)
.await
{
return Err(DatabaseError::Other);
};
if relationship.0 == RelationshipStatus::Friends {
self.base
.cachedb
.decr(format!("rbeam.app.friends_count:{}", uone.id))
.await;
self.base
.cachedb
.decr(format!("rbeam.app.friends_count:{}", utwo.id))
.await;
}
}
}
Ok(())
}
pub async fn get_user_relationships(
&self,
user: String,
) -> Result<Vec<(Box<Profile>, RelationshipStatus)>> {
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"SELECT * FROM \"xrelationships\" WHERE \"one\" = ?"
} else {
"SELECT * FROM \"xrelationships\" WHERE \"one\" = $1"
}
.to_string();
let c = &self.base.db.client;
match sqlquery(&query).bind::<&String>(&user).fetch_all(c).await {
Ok(p) => {
let mut out = Vec::new();
for row in p {
let res = self.base.textify_row(row, Vec::new()).0;
let profile = match self.get_profile(res.get("two").unwrap().to_string()).await
{
Ok(c) => c,
Err(e) => return Err(e),
};
out.push((
profile,
serde_json::from_str(&res.get("status").unwrap()).unwrap(),
));
}
Ok(out)
}
Err(_) => return Err(DatabaseError::Other),
}
}
pub async fn get_user_relationships_of_status(
&self,
user: String,
status: RelationshipStatus,
) -> Result<Vec<(Box<Profile>, RelationshipStatus)>> {
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"SELECT * FROM \"xrelationships\" WHERE \"one\" = ? AND \"status\" = ?"
} else {
"SELECT * FROM \"xrelationships\" WHERE \"one\" = $1 AND \"status\" = $2"
}
.to_string();
let c = &self.base.db.client;
match sqlquery(&query)
.bind::<&String>(&user)
.bind::<&String>(&serde_json::to_string(&status).unwrap())
.fetch_all(c)
.await
{
Ok(p) => {
let mut out = Vec::new();
for row in p {
let res = self.base.textify_row(row, Vec::new()).0;
let profile = match self.get_profile(res.get("two").unwrap().to_string()).await
{
Ok(c) => c,
Err(_) => continue,
};
out.push((
profile,
serde_json::from_str(&res.get("status").unwrap()).unwrap(),
));
}
Ok(out)
}
Err(_) => return Err(DatabaseError::Other),
}
}
pub async fn get_user_participating_relationships_of_status(
&self,
user: String,
status: RelationshipStatus,
) -> Result<Vec<(Box<Profile>, Box<Profile>)>> {
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"SELECT * FROM \"xrelationships\" WHERE (\"one\" = ? OR \"two\" = ?) AND \"status\" = ?"
} else {
"SELECT * FROM \"xrelationships\" WHERE (\"one\" = $1 OR \"two\" = $2) AND \"status\" = $3"
}.to_string();
let c = &self.base.db.client;
match sqlquery(&query)
.bind::<&String>(&user)
.bind::<&String>(&user)
.bind::<&String>(&serde_json::to_string(&status).unwrap())
.fetch_all(c)
.await
{
Ok(p) => {
let mut out = Vec::new();
for row in p {
let res = self.base.textify_row(row, Vec::new()).0;
let profile = match self.get_profile(res.get("one").unwrap().to_string()).await
{
Ok(c) => c,
Err(_) => continue,
};
let profile_2 =
match self.get_profile(res.get("two").unwrap().to_string()).await {
Ok(c) => c,
Err(_) => continue,
};
out.push((profile, profile_2));
}
Ok(out)
}
Err(_) => return Err(DatabaseError::Other),
}
}
pub async fn get_user_participating_relationships_of_status_paginated(
&self,
user: String,
status: RelationshipStatus,
page: i32,
) -> Result<Vec<(Box<Profile>, Box<Profile>)>> {
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
format!("SELECT * FROM \"xrelationships\" WHERE (\"one\" = ? OR \"two\" = ?) AND \"status\" = ? ORDER BY \"timestamp\" DESC LIMIT 50 OFFSET {}", page * 50)
} else {
format!("SELECT * FROM \"xrelationships\" WHERE (\"one\" = $1 OR \"two\" = $2) AND \"status\" = $3 ORDER BY \"timestamp\" DESC LIMIT 50 OFFSET {}", page * 50)
};
let c = &self.base.db.client;
match sqlquery(&query)
.bind::<&String>(&user)
.bind::<&String>(&user)
.bind::<&String>(&serde_json::to_string(&status).unwrap())
.fetch_all(c)
.await
{
Ok(p) => {
let mut out = Vec::new();
for row in p {
let res = self.base.textify_row(row, Vec::new()).0;
let profile = match self.get_profile(res.get("one").unwrap().to_string()).await
{
Ok(c) => c,
Err(e) => return Err(e),
};
let profile_2 =
match self.get_profile(res.get("two").unwrap().to_string()).await {
Ok(c) => c,
Err(e) => return Err(e),
};
out.push((profile, profile_2));
}
Ok(out)
}
Err(_) => return Err(DatabaseError::Other),
}
}
pub async fn get_friendship_count_by_user(&self, id: String) -> usize {
if let Some(count) = self
.base
.cachedb
.get(format!("rbeam.app.friends_count:{}", id))
.await
{
return count.parse::<usize>().unwrap_or(0);
};
let count = self
.get_user_participating_relationships_of_status(id.clone(), RelationshipStatus::Friends)
.await
.unwrap_or(Vec::new())
.len();
self.base
.cachedb
.set(format!("rbeam.app.friends_count:{}", id), count.to_string())
.await;
count
}
pub async fn get_ipblock(&self, id: String) -> Result<IpBlock> {
match self
.base
.cachedb
.get(format!("rbeam.auth.ipblock:{}", id))
.await
{
Some(c) => return Ok(serde_json::from_str::<IpBlock>(c.as_str()).unwrap()),
None => (),
};
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"SELECT * FROM \"xipblocks\" WHERE \"id\" = ?"
} else {
"SELECT * FROM \"xipblocks\" WHERE \"id\" = $1"
}
.to_string();
let c = &self.base.db.client;
let res = match sqlquery(&query).bind::<&String>(&id).fetch_one(c).await {
Ok(p) => self.base.textify_row(p, Vec::new()).0,
Err(_) => return Err(DatabaseError::NotFound),
};
let block = IpBlock {
id: res.get("id").unwrap().to_string(),
ip: res.get("ip").unwrap().to_string(),
user: res.get("user").unwrap().to_string(),
context: res.get("context").unwrap().to_string(),
timestamp: res.get("timestamp").unwrap().parse::<u128>().unwrap(),
};
self.base
.cachedb
.set(
format!("rbeam.auth.ipblock:{}", id),
serde_json::to_string::<IpBlock>(&block).unwrap(),
)
.await;
Ok(block)
}
pub async fn get_ipblock_by_ip(&self, ip: String, user: String) -> Result<IpBlock> {
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"SELECT * FROM \"xipblocks\" WHERE \"ip\" = ? AND \"user\" = ?"
} else {
"SELECT * FROM \"xipblocks\" WHERE \"ip\" = $1 AND \"user\" = $2"
}
.to_string();
let c = &self.base.db.client;
let res = match sqlquery(&query)
.bind::<&String>(&ip)
.bind::<&String>(&user)
.fetch_one(c)
.await
{
Ok(p) => self.base.textify_row(p, Vec::new()).0,
Err(_) => return Err(DatabaseError::NotFound),
};
let block = IpBlock {
id: res.get("id").unwrap().to_string(),
ip: res.get("ip").unwrap().to_string(),
user: res.get("user").unwrap().to_string(),
context: res.get("context").unwrap().to_string(),
timestamp: res.get("timestamp").unwrap().parse::<u128>().unwrap(),
};
Ok(block)
}
pub async fn get_ipblocks(&self, query_user: String) -> Result<Vec<IpBlock>> {
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"SELECT * FROM \"xipblocks\" WHERE \"user\" = ? ORDER BY \"timestamp\" DESC"
} else {
"SELECT * FROM \"xipblocks\" WHERE \"user\" = $1 ORDER BY \"timestamp\" DESC"
}
.to_string();
let c = &self.base.db.client;
let res = match sqlquery(&query)
.bind::<&String>(&query_user)
.fetch_all(c)
.await
{
Ok(p) => {
let mut out: Vec<IpBlock> = Vec::new();
for row in p {
let res = self.base.textify_row(row, Vec::new()).0;
out.push(IpBlock {
id: res.get("id").unwrap().to_string(),
ip: res.get("ip").unwrap().to_string(),
user: res.get("user").unwrap().to_string(),
context: res.get("context").unwrap().to_string(),
timestamp: res.get("timestamp").unwrap().parse::<u128>().unwrap(),
});
}
out
}
Err(_) => return Err(DatabaseError::NotFound),
};
Ok(res)
}
pub async fn create_ipblock(&self, props: IpBlockCreate, user: Box<Profile>) -> Result<()> {
if self
.get_ipblock_by_ip(props.ip.clone(), user.id.clone())
.await
.is_ok()
{
return Err(DatabaseError::MustBeUnique);
}
let block = IpBlock {
id: utility::random_id(),
ip: props.ip,
user: user.id,
context: props.context,
timestamp: utility::unix_epoch_timestamp(),
};
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"INSERT INTO \"xipblocks\" VALUES (?, ?, ?, ?, ?)"
} else {
"INSERT INTO \"xipblocks\" VALEUS ($1, $2, $3, $4, $5)"
}
.to_string();
let c = &self.base.db.client;
match sqlquery(&query)
.bind::<&String>(&block.id)
.bind::<&String>(&block.ip)
.bind::<&String>(&block.user)
.bind::<&String>(&block.context)
.bind::<&String>(&block.timestamp.to_string())
.execute(c)
.await
{
Ok(_) => return Ok(()),
Err(_) => return Err(DatabaseError::Other),
};
}
pub async fn delete_ipblock(&self, id: String, user: Box<Profile>) -> Result<()> {
let block = match self.get_ipblock(id.clone()).await {
Ok(n) => n,
Err(e) => return Err(e),
};
if user.id != block.user {
let group = match self.get_group_by_id(user.group).await {
Ok(g) => g,
Err(_) => return Err(DatabaseError::Other),
};
if !group.permissions.contains(&Permission::Manager) {
return Err(DatabaseError::NotAllowed);
} else {
let actor_id = user.id.clone();
if let Err(e) = self
.create_notification(
NotificationCreate {
title: format!("[{actor_id}](/+u/{actor_id})"),
content: format!("Unblocked an IP: {}", block.ip),
address: format!("/+u/{actor_id}"),
recipient: "*(audit)".to_string(), },
None,
)
.await
{
return Err(e);
}
}
}
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"DELETE FROM \"xipblocks\" WHERE \"id\" = ?"
} else {
"DELETE FROM \"xipblocks\" WHERE \"id\" = $1"
}
.to_string();
let c = &self.base.db.client;
match sqlquery(&query).bind::<&String>(&id).execute(c).await {
Ok(_) => {
self.base
.cachedb
.remove(format!("rbeam.auth.ipblock:{}", id))
.await;
return Ok(());
}
Err(_) => return Err(DatabaseError::Other),
};
}
pub async fn get_mail(&self, id: String) -> Result<Mail> {
match self
.base
.cachedb
.get(format!("rbeam.auth.mail:{}", id))
.await
{
Some(c) => match serde_json::from_str::<Mail>(c.as_str()) {
Ok(c) => return Ok(c),
Err(_) => {
self.base
.cachedb
.remove(format!("rbeam.auth.mail:{}", id))
.await;
}
},
None => (),
};
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"SELECT * FROM \"xmail\" WHERE \"id\" = ?"
} else {
"SELECT * FROM \"xmail\" WHERE \"id\" = $1"
}
.to_string();
let c = &self.base.db.client;
let res = match sqlquery(&query).bind::<&String>(&id).fetch_one(c).await {
Ok(p) => self.base.textify_row(p, Vec::new()).0,
Err(_) => return Err(DatabaseError::NotFound),
};
let recipient = res.get("recipient").unwrap();
let mail = Mail {
title: res.get("title").unwrap().to_string(),
content: res.get("content").unwrap().to_string(),
timestamp: res.get("timestamp").unwrap().parse::<u128>().unwrap(),
id: res.get("id").unwrap().to_string(),
state: serde_json::from_str(res.get("state").unwrap()).unwrap(),
author: res.get("author").unwrap().to_string(),
recipient: serde_json::from_str(recipient).unwrap_or(vec![recipient.to_string()]),
};
self.base
.cachedb
.set(
format!("rbeam.auth.mail:{}", id),
serde_json::to_string::<Mail>(&mail).unwrap(),
)
.await;
Ok(mail)
}
pub async fn get_mail_by_recipient_paginated(
&self,
recipient: String,
page: i32,
) -> Result<Vec<(Mail, Box<Profile>)>> {
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
format!("SELECT * FROM \"xmail\" WHERE \"recipient\" LIKE ? OR \"recipient\" = ? ORDER BY \"timestamp\" DESC LIMIT 50 OFFSET {}", page * 50)
} else {
format!("SELECT * FROM \"xmail\" WHERE \"recipient\" LIKE $1 OR \"recipient\" = $2 ORDER BY \"timestamp\" DESC LIMIT 50 OFFSET {}", page * 50)
};
let c = &self.base.db.client;
let res = match sqlquery(&query)
.bind::<&String>(&format!("%\"{}\"%", recipient.to_lowercase()))
.bind::<&String>(&recipient.to_lowercase())
.fetch_all(c)
.await
{
Ok(p) => {
let mut out: Vec<(Mail, Box<Profile>)> = Vec::new();
for row in p {
let res = self.base.textify_row(row, Vec::new()).0;
let author = res.get("author").unwrap();
let recipient = res.get("recipient").unwrap();
out.push((
Mail {
title: res.get("title").unwrap().to_string(),
content: res.get("content").unwrap().to_string(),
timestamp: res.get("timestamp").unwrap().parse::<u128>().unwrap(),
id: res.get("id").unwrap().to_string(),
state: serde_json::from_str(res.get("state").unwrap()).unwrap(),
author: author.to_string(),
recipient: serde_json::from_str(recipient)
.unwrap_or(vec![recipient.to_string()]),
},
match self.get_profile(author.to_string()).await {
Ok(ua) => ua,
Err(_) => continue,
},
));
}
out
}
Err(_) => return Err(DatabaseError::Other),
};
Ok(res)
}
pub async fn get_mail_by_recipient_sent_paginated(
&self,
recipient: String,
page: i32,
) -> Result<Vec<(Mail, Box<Profile>)>> {
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
format!("SELECT * FROM \"xmail\" WHERE \"author\" LIKE ? OR \"author\" = ? ORDER BY \"timestamp\" DESC LIMIT 50 OFFSET {}", page * 50)
} else {
format!("SELECT * FROM \"xmail\" WHERE \"author\" LIKE $1 OR \"author\" = $2 ORDER BY \"timestamp\" DESC LIMIT 50 OFFSET {}", page * 50)
};
let c = &self.base.db.client;
let res = match sqlquery(&query)
.bind::<&String>(&format!("%\"{}\"%", recipient.to_lowercase()))
.bind::<&String>(&recipient.to_lowercase())
.fetch_all(c)
.await
{
Ok(p) => {
let mut out: Vec<(Mail, Box<Profile>)> = Vec::new();
for row in p {
let res = self.base.textify_row(row, Vec::new()).0;
let author = res.get("author").unwrap();
let recipient = res.get("recipient").unwrap();
out.push((
Mail {
title: res.get("title").unwrap().to_string(),
content: res.get("content").unwrap().to_string(),
timestamp: res.get("timestamp").unwrap().parse::<u128>().unwrap(),
id: res.get("id").unwrap().to_string(),
state: serde_json::from_str(res.get("state").unwrap()).unwrap(),
author: author.to_string(),
recipient: serde_json::from_str(recipient)
.unwrap_or(vec![recipient.to_string()]),
},
match self.get_profile(author.to_string()).await {
Ok(ua) => ua,
Err(_) => continue,
},
));
}
out
}
Err(_) => return Err(DatabaseError::Other),
};
Ok(res)
}
pub async fn create_mail(&self, props: MailCreate, author: String) -> Result<Mail> {
if props.title.len() > (64 * 4) {
return Err(DatabaseError::Other);
}
if props.title.len() < 2 {
return Err(DatabaseError::Other);
}
if props.content.len() > (64 * 8) {
return Err(DatabaseError::Other);
}
if props.content.len() < 2 {
return Err(DatabaseError::Other);
}
let markdown = rainbeam_shared::ui::render_markdown(&props.content);
if markdown.trim().len() == 0 {
return Err(DatabaseError::Other);
}
let author = match self.get_profile(author.clone()).await {
Ok(ua) => ua,
Err(e) => return Err(e),
};
let mut recipients = Vec::new();
for recipient in props.recipient {
if recipient.is_empty() {
continue;
}
match self.get_profile(recipient.clone()).await {
Ok(ua) => {
if ua.metadata.is_true("sparkler:disable_mailbox") {
continue;
}
let relationship = self
.get_user_relationship(ua.id.clone(), author.id.clone())
.await
.0;
if relationship == RelationshipStatus::Blocked {
continue;
}
recipients.push(ua.id)
}
Err(e) => return Err(e),
}
}
if recipients.len() == 0 {
return Err(DatabaseError::ValueError);
};
let mut seen_severs: Vec<String> = Vec::new();
for recipient in recipients.clone() {
let cid = CitrusID(recipient.clone()).fields();
if cid.0 != self.config.citrus_id && !cid.0.is_empty() {
if seen_severs.contains(&cid.0) {
continue;
}
seen_severs.push(cid.0.clone());
let server = match self.citrus.server(cid.0.to_string()).await {
Ok(s) => s,
Err(_) => return Err(DatabaseError::Other),
};
if server.get_schema("net.rainbeam.structs.Mail").is_none() {
return Err(DatabaseError::Other);
}
if let Err(_) = self
.citrus
.post::<MailCreate, DefaultReturn<Option<Profile>>>(
server,
"net.rainbeam.structs.Mail",
"/api/v0/auth/mail",
MailCreate {
title: props.title.clone(),
content: props.content.clone(),
recipient: vec![recipient.clone()],
},
)
.await
{
return Err(DatabaseError::Other);
};
}
}
let mail = Mail {
title: props.title,
content: props.content,
timestamp: utility::unix_epoch_timestamp(),
id: utility::random_id(),
state: MailState::Unread,
author: author.id.clone(),
recipient: recipients.clone(),
};
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"INSERT INTO \"xmail\" VALUES (?, ?, ?, ?, ?, ?, ?)"
} else {
"INSERT INTO \"xmail\" VALEUS ($1, $2, $3, $4, $5, $6, $7)"
}
.to_string();
let c = &self.base.db.client;
match sqlquery(&query)
.bind::<&String>(&mail.title)
.bind::<&String>(&mail.content)
.bind::<&String>(&mail.timestamp.to_string())
.bind::<&String>(&mail.id)
.bind::<&String>(&serde_json::to_string(&mail.state).unwrap())
.bind::<&String>(&mail.author)
.bind::<&String>(&serde_json::to_string(&mail.recipient).unwrap())
.execute(c)
.await
{
Ok(_) => {
for recipient in recipients {
if let Err(e) = self
.create_notification(
NotificationCreate {
title: format!(
"[@{}](/+u/{}) sent you new mail!",
author.username, author.id
),
content: String::new(),
address: format!("/inbox/mail/letter/{}", mail.id),
recipient: recipient.clone(),
},
None,
)
.await
{
return Err(e);
};
}
return Ok(mail);
}
Err(_) => return Err(DatabaseError::Other),
};
}
pub async fn delete_mail(&self, id: String, user: Box<Profile>) -> Result<()> {
let mail = match self.get_mail(id.clone()).await {
Ok(n) => n,
Err(e) => return Err(e),
};
if !mail.recipient.contains(&user.id) && (user.id != mail.author) {
let group = match self.get_group_by_id(user.group).await {
Ok(g) => g,
Err(_) => return Err(DatabaseError::Other),
};
if !group.permissions.contains(&Permission::Helper) {
return Err(DatabaseError::NotAllowed);
}
}
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"DELETE FROM \"xmail\" WHERE \"id\" = ?"
} else {
"DELETE FROM \"xmail\" WHERE \"id\" = $1"
}
.to_string();
let c = &self.base.db.client;
match sqlquery(&query).bind::<&String>(&id).execute(c).await {
Ok(_) => {
self.base
.cachedb
.remove(format!("rbeam.auth.mail:{}", id))
.await;
return Ok(());
}
Err(_) => return Err(DatabaseError::Other),
};
}
pub async fn update_mail_state(
&self,
id: String,
state: MailState,
user: Box<Profile>,
) -> Result<()> {
let mail = match self.get_mail(id.clone()).await {
Ok(n) => n,
Err(e) => return Err(e),
};
if user.id != mail.author {
let group = match self.get_group_by_id(user.group).await {
Ok(g) => g,
Err(_) => return Err(DatabaseError::Other),
};
if !group.permissions.contains(&Permission::Helper) {
return Err(DatabaseError::NotAllowed);
}
}
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"UPDATE \"xmail\" SET \"state\" = ? WHERE \"id\" = ?"
} else {
"UPDATE \"xmail\" SET (\"state\") = ($1) WHERE \"id\" = $2"
}
.to_string();
let c = &self.base.db.client;
match sqlquery(&query)
.bind::<&String>(&serde_json::to_string(&state).unwrap())
.bind::<&String>(&id)
.execute(c)
.await
{
Ok(_) => {
self.base
.cachedb
.remove(format!("rbeam.auth.mail:{}", id))
.await;
return Ok(());
}
Err(_) => return Err(DatabaseError::Other),
};
}
pub async fn gimme_label(&self, res: HashMap<String, String>) -> Result<UserLabel> {
Ok(UserLabel {
id: res.get("id").unwrap().to_string(),
name: res.get("name").unwrap().to_string(),
timestamp: res.get("timestamp").unwrap().parse::<u128>().unwrap(),
creator: res.get("author").unwrap().to_string(),
})
}
pub async fn get_label(&self, id: String) -> Result<UserLabel> {
match self
.base
.cachedb
.get(format!("rbeam.auth.label:{}", id))
.await
{
Some(c) => match serde_json::from_str::<UserLabel>(c.as_str()) {
Ok(c) => return Ok(c),
Err(_) => {
self.base
.cachedb
.remove(format!("rbeam.auth.label:{}", id))
.await;
}
},
None => (),
};
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"SELECT * FROM \"xlabels\" WHERE \"id\" = ?"
} else {
"SELECT * FROM \"xlabels\" WHERE \"id\" = $1"
}
.to_string();
let c = &self.base.db.client;
let res = match sqlquery(&query).bind::<&String>(&id).fetch_one(c).await {
Ok(p) => self.base.textify_row(p, Vec::new()).0,
Err(_) => return Err(DatabaseError::NotFound),
};
let label = match self.gimme_label(res).await {
Ok(l) => l,
Err(e) => return Err(e),
};
self.base
.cachedb
.set(
format!("rbeam.auth.label:{}", id),
serde_json::to_string::<UserLabel>(&label).unwrap(),
)
.await;
Ok(label)
}
pub async fn create_label(&self, name: String, author: String) -> Result<UserLabel> {
let author = match self.get_profile(author.clone()).await {
Ok(ua) => ua,
Err(e) => return Err(e),
};
let group = match self.get_group_by_id(author.group).await {
Ok(g) => g,
Err(e) => return Err(e),
};
if !group.permissions.contains(&Permission::Helper) {
return Err(DatabaseError::NotAllowed);
}
if name.len() < 2 {
return Err(DatabaseError::Other);
}
if name.len() > 32 {
return Err(DatabaseError::Other);
}
let label = UserLabel {
id: utility::random_id(),
name,
timestamp: utility::unix_epoch_timestamp(),
creator: author.id,
};
let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql")
{
"INSERT INTO \"xlabels\" VALUES (?, ?, ?, ?)"
} else {
"INSERT INTO \"xlabels\" VALEUS ($1, $2, $3, $4)"
}
.to_string();
let c = &self.base.db.client;
match sqlquery(&query)
.bind::<&String>(&label.id)
.bind::<&String>(&label.name)
.bind::<&String>(&label.timestamp.to_string())
.bind::<&String>(&label.creator)
.execute(c)
.await
{
Ok(_) => Ok(label),
Err(_) => Err(DatabaseError::Other),
}
}
}