add: mail base
This commit is contained in:
parent
a337e0c7c1
commit
29155ddb0c
11 changed files with 211 additions and 3 deletions
|
@ -406,6 +406,7 @@
|
||||||
MANAGE_SERVICES: 1 << 3,
|
MANAGE_SERVICES: 1 << 3,
|
||||||
MANAGE_PRODUCTS: 1 << 4,
|
MANAGE_PRODUCTS: 1 << 4,
|
||||||
DEVELOPER_PASS: 1 << 5,
|
DEVELOPER_PASS: 1 << 5,
|
||||||
|
MANAGE_LETTERS: 1 << 6,
|
||||||
},
|
},
|
||||||
\"secondary_role\",
|
\"secondary_role\",
|
||||||
\"add_permission_to_secondary_role\",
|
\"add_permission_to_secondary_role\",
|
||||||
|
|
|
@ -100,14 +100,29 @@ impl DataManager {
|
||||||
tokens: serde_json::from_str(&get!(x->6(String)).to_string()).unwrap(),
|
tokens: serde_json::from_str(&get!(x->6(String)).to_string()).unwrap(),
|
||||||
permissions: FinePermission::from_bits(get!(x->7(i32)) as u32).unwrap(),
|
permissions: FinePermission::from_bits(get!(x->7(i32)) as u32).unwrap(),
|
||||||
is_verified: get!(x->8(i32)) as i8 == 1,
|
is_verified: get!(x->8(i32)) as i8 == 1,
|
||||||
notification_count: get!(x->9(i32)) as usize,
|
notification_count: {
|
||||||
|
let x = get!(x->9(i32));
|
||||||
|
if x > (usize::MAX - 1000) as i32 {
|
||||||
|
// we're a little too close to the maximum count, clearly something's gone wrong
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
x as usize
|
||||||
|
}
|
||||||
|
},
|
||||||
follower_count: get!(x->10(i32)) as usize,
|
follower_count: get!(x->10(i32)) as usize,
|
||||||
following_count: get!(x->11(i32)) as usize,
|
following_count: get!(x->11(i32)) as usize,
|
||||||
last_seen: get!(x->12(i64)) as usize,
|
last_seen: get!(x->12(i64)) as usize,
|
||||||
totp: get!(x->13(String)),
|
totp: get!(x->13(String)),
|
||||||
recovery_codes: serde_json::from_str(&get!(x->14(String)).to_string()).unwrap(),
|
recovery_codes: serde_json::from_str(&get!(x->14(String)).to_string()).unwrap(),
|
||||||
post_count: get!(x->15(i32)) as usize,
|
post_count: get!(x->15(i32)) as usize,
|
||||||
request_count: get!(x->16(i32)) as usize,
|
request_count: {
|
||||||
|
let x = get!(x->16(i32));
|
||||||
|
if x > (usize::MAX - 1000) as i32 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
x as usize
|
||||||
|
}
|
||||||
|
},
|
||||||
connections: serde_json::from_str(&get!(x->17(String)).to_string()).unwrap(),
|
connections: serde_json::from_str(&get!(x->17(String)).to_string()).unwrap(),
|
||||||
stripe_id: get!(x->18(String)),
|
stripe_id: get!(x->18(String)),
|
||||||
grants: serde_json::from_str(&get!(x->19(String)).to_string()).unwrap(),
|
grants: serde_json::from_str(&get!(x->19(String)).to_string()).unwrap(),
|
||||||
|
|
|
@ -44,6 +44,7 @@ impl DataManager {
|
||||||
execute!(&conn, common::CREATE_TABLE_SERVICES).unwrap();
|
execute!(&conn, common::CREATE_TABLE_SERVICES).unwrap();
|
||||||
execute!(&conn, common::CREATE_TABLE_PRODUCTS).unwrap();
|
execute!(&conn, common::CREATE_TABLE_PRODUCTS).unwrap();
|
||||||
execute!(&conn, common::CREATE_TABLE_APP_DATA).unwrap();
|
execute!(&conn, common::CREATE_TABLE_APP_DATA).unwrap();
|
||||||
|
execute!(&conn, common::CREATE_TABLE_LETTERS).unwrap();
|
||||||
|
|
||||||
for x in common::VERSION_MIGRATIONS.split(";") {
|
for x in common::VERSION_MIGRATIONS.split(";") {
|
||||||
execute!(&conn, x).unwrap();
|
execute!(&conn, x).unwrap();
|
||||||
|
|
|
@ -32,3 +32,4 @@ pub const CREATE_TABLE_DOMAINS: &str = include_str!("./sql/create_domains.sql");
|
||||||
pub const CREATE_TABLE_SERVICES: &str = include_str!("./sql/create_services.sql");
|
pub const CREATE_TABLE_SERVICES: &str = include_str!("./sql/create_services.sql");
|
||||||
pub const CREATE_TABLE_PRODUCTS: &str = include_str!("./sql/create_products.sql");
|
pub const CREATE_TABLE_PRODUCTS: &str = include_str!("./sql/create_products.sql");
|
||||||
pub const CREATE_TABLE_APP_DATA: &str = include_str!("./sql/create_app_data.sql");
|
pub const CREATE_TABLE_APP_DATA: &str = include_str!("./sql/create_app_data.sql");
|
||||||
|
pub const CREATE_TABLE_LETTERS: &str = include_str!("./sql/create_letters.sql");
|
||||||
|
|
9
crates/core/src/database/drivers/sql/create_letters.sql
Normal file
9
crates/core/src/database/drivers/sql/create_letters.sql
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS letters (
|
||||||
|
id BIGINT NOT NULL PRIMARY KEY,
|
||||||
|
created BIGINT NOT NULL,
|
||||||
|
owner BIGINT NOT NULL,
|
||||||
|
receivers TEXT NOT NULL,
|
||||||
|
subject TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
read_by TEXT NOT NULL
|
||||||
|
)
|
144
crates/core/src/database/letters.rs
Normal file
144
crates/core/src/database/letters.rs
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
use crate::model::{auth::User, mail::Letter, permissions::SecondaryPermission, Error, Result};
|
||||||
|
use crate::{auto_method, DataManager};
|
||||||
|
use oiseau::{cache::Cache, execute, get, params, query_rows, PostgresRow};
|
||||||
|
|
||||||
|
impl DataManager {
|
||||||
|
/// Get a [`Letter`] from an SQL row.
|
||||||
|
pub(crate) fn get_letter_from_row(x: &PostgresRow) -> Letter {
|
||||||
|
Letter {
|
||||||
|
id: get!(x->0(i64)) as usize,
|
||||||
|
created: get!(x->1(i64)) as usize,
|
||||||
|
owner: get!(x->2(i64)) as usize,
|
||||||
|
receivers: serde_json::from_str(&get!(x->3(String))).unwrap(),
|
||||||
|
subject: get!(x->4(String)),
|
||||||
|
content: get!(x->5(String)),
|
||||||
|
read_by: serde_json::from_str(&get!(x->6(String))).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto_method!(get_letter_by_id(usize as i64)@get_letter_from_row -> "SELECT * FROM letters WHERE id = $1" --name="letter" --returns=Letter --cache-key-tmpl="atto.letter:{}");
|
||||||
|
|
||||||
|
/// Get all letters by user.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `id` - the ID of the user to fetch letters for
|
||||||
|
pub async fn get_letters_by_user(&self, id: usize) -> Result<Vec<Letter>> {
|
||||||
|
let conn = match self.0.connect().await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = query_rows!(
|
||||||
|
&conn,
|
||||||
|
"SELECT * FROM letters WHERE owner = $1 ORDER BY created DESC",
|
||||||
|
&[&(id as i64)],
|
||||||
|
|x| { Self::get_letter_from_row(x) }
|
||||||
|
);
|
||||||
|
|
||||||
|
if res.is_err() {
|
||||||
|
return Err(Error::GeneralNotFound("letter".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all letters by user (where user is a receiver).
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `id` - the ID of the user to fetch letters for
|
||||||
|
pub async fn get_received_letters_by_user(&self, id: usize) -> Result<Vec<Letter>> {
|
||||||
|
let conn = match self.0.connect().await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = query_rows!(
|
||||||
|
&conn,
|
||||||
|
"SELECT * FROM letters WHERE receivers LIKE $1 ORDER BY created DESC",
|
||||||
|
&[&format!("%\"{id}\"%")],
|
||||||
|
|x| { Self::get_letter_from_row(x) }
|
||||||
|
);
|
||||||
|
|
||||||
|
if res.is_err() {
|
||||||
|
return Err(Error::GeneralNotFound("letter".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new letter in the database.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `data` - a mock [`Letter`] object to insert
|
||||||
|
pub async fn create_letter(&self, data: Letter) -> Result<Letter> {
|
||||||
|
// check values
|
||||||
|
if data.subject.len() < 2 {
|
||||||
|
return Err(Error::DataTooShort("subject".to_string()));
|
||||||
|
} else if data.subject.len() > 256 {
|
||||||
|
return Err(Error::DataTooLong("subject".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.content.len() < 2 {
|
||||||
|
return Err(Error::DataTooShort("content".to_string()));
|
||||||
|
} else if data.content.len() > 16384 {
|
||||||
|
return Err(Error::DataTooLong("content".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
let conn = match self.0.connect().await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = execute!(
|
||||||
|
&conn,
|
||||||
|
"INSERT INTO letters VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
||||||
|
params![
|
||||||
|
&(data.id as i64),
|
||||||
|
&(data.created as i64),
|
||||||
|
&(data.owner as i64),
|
||||||
|
&serde_json::to_string(&data.receivers).unwrap(),
|
||||||
|
&data.subject,
|
||||||
|
&data.content,
|
||||||
|
&serde_json::to_string(&data.read_by).unwrap(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
return Err(Error::DatabaseError(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_letter(&self, id: usize, user: &User) -> Result<()> {
|
||||||
|
let letter = self.get_letter_by_id(id).await?;
|
||||||
|
|
||||||
|
// check user permission
|
||||||
|
if user.id != letter.owner
|
||||||
|
&& !user
|
||||||
|
.secondary_permissions
|
||||||
|
.check(SecondaryPermission::MANAGE_LETTERS)
|
||||||
|
{
|
||||||
|
return Err(Error::NotAllowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
let conn = match self.0.connect().await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = execute!(&conn, "DELETE FROM letters WHERE id = $1", &[&(id as i64)]);
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
return Err(Error::DatabaseError(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
self.0.1.remove(format!("atto.letter:{}", id)).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
auto_method!(update_letter_read_by(Vec<usize>) -> "UPDATE letters SET read_by = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.letter:{}");
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ mod invite_codes;
|
||||||
mod ipbans;
|
mod ipbans;
|
||||||
mod ipblocks;
|
mod ipblocks;
|
||||||
mod journals;
|
mod journals;
|
||||||
|
mod letters;
|
||||||
mod memberships;
|
mod memberships;
|
||||||
mod message_reactions;
|
mod message_reactions;
|
||||||
mod messages;
|
mod messages;
|
||||||
|
|
35
crates/core/src/model/mail.rs
Normal file
35
crates/core/src/model/mail.rs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
|
||||||
|
|
||||||
|
/// A letter is the most basic structure of the mail system. Letters are sent
|
||||||
|
/// and received by users.
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct Letter {
|
||||||
|
pub id: usize,
|
||||||
|
pub created: usize,
|
||||||
|
pub owner: usize,
|
||||||
|
pub receivers: Vec<usize>,
|
||||||
|
pub subject: String,
|
||||||
|
pub content: String,
|
||||||
|
/// The ID of every use who has read the letter. Can be checked in the UI
|
||||||
|
/// with `user.id in letter.read_by`.
|
||||||
|
///
|
||||||
|
/// This field can be updated by anyone in the letter's `receivers` field.
|
||||||
|
/// Other fields in the letter can only be updated by the letter's `owner`.
|
||||||
|
pub read_by: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Letter {
|
||||||
|
/// Create a new [`Letter`].
|
||||||
|
pub fn new(owner: usize, receivers: Vec<usize>, subject: String, content: String) -> Self {
|
||||||
|
Self {
|
||||||
|
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
|
||||||
|
created: unix_epoch_timestamp(),
|
||||||
|
owner,
|
||||||
|
receivers,
|
||||||
|
subject,
|
||||||
|
content,
|
||||||
|
read_by: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ pub mod communities;
|
||||||
pub mod communities_permissions;
|
pub mod communities_permissions;
|
||||||
pub mod journals;
|
pub mod journals;
|
||||||
pub mod littleweb;
|
pub mod littleweb;
|
||||||
|
pub mod mail;
|
||||||
pub mod moderation;
|
pub mod moderation;
|
||||||
pub mod oauth;
|
pub mod oauth;
|
||||||
pub mod permissions;
|
pub mod permissions;
|
||||||
|
|
|
@ -178,6 +178,7 @@ bitflags! {
|
||||||
const MANAGE_SERVICES = 1 << 3;
|
const MANAGE_SERVICES = 1 << 3;
|
||||||
const MANAGE_PRODUCTS = 1 << 4;
|
const MANAGE_PRODUCTS = 1 << 4;
|
||||||
const DEVELOPER_PASS = 1 << 5;
|
const DEVELOPER_PASS = 1 << 5;
|
||||||
|
const MANAGE_LETTERS = 1 << 6;
|
||||||
|
|
||||||
const _ = !0;
|
const _ = !0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@ pub fn render_markdown_dirty(input: &str) -> String {
|
||||||
options.insert(Options::ENABLE_TABLES);
|
options.insert(Options::ENABLE_TABLES);
|
||||||
options.insert(Options::ENABLE_HEADING_ATTRIBUTES);
|
options.insert(Options::ENABLE_HEADING_ATTRIBUTES);
|
||||||
options.insert(Options::ENABLE_SUBSCRIPT);
|
options.insert(Options::ENABLE_SUBSCRIPT);
|
||||||
options.insert(Options::ENABLE_SUPERSCRIPT);
|
|
||||||
|
|
||||||
let parser = Parser::new_ext(input, options);
|
let parser = Parser::new_ext(input, options);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue