add: stripe integration

This commit is contained in:
trisua 2025-05-05 19:38:01 -04:00
parent 2fa5a4dc1f
commit 1d120555a0
31 changed files with 1137 additions and 122 deletions

View file

@ -45,6 +45,7 @@ impl DataManager {
post_count: get!(x->15(i32)) as usize,
request_count: get!(x->16(i32)) as usize,
connections: serde_json::from_str(&get!(x->17(String)).to_string()).unwrap(),
stripe_id: get!(x->18(String)),
}
}
@ -139,7 +140,7 @@ impl DataManager {
let res = execute!(
&conn,
"INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)",
"INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)",
params![
&(data.id as i64),
&(data.created as i64),
@ -159,6 +160,7 @@ impl DataManager {
&0_i32,
&0_i32,
&serde_json::to_string(&data.connections).unwrap(),
&""
]
);
@ -433,24 +435,27 @@ impl DataManager {
id: usize,
role: FinePermission,
user: User,
force: bool,
) -> Result<()> {
// check permission
if !user.permissions.check(FinePermission::MANAGE_USERS) {
return Err(Error::NotAllowed);
}
let other_user = self.get_user_by_id(id).await?;
if other_user.permissions.check_manager() && !user.permissions.check_admin() {
return Err(Error::MiscError(
"Cannot manage the role of other managers".to_string(),
));
}
if !force {
// check permission
if !user.permissions.check(FinePermission::MANAGE_USERS) {
return Err(Error::NotAllowed);
}
if other_user.permissions == user.permissions {
return Err(Error::MiscError(
"Cannot manage users of equal level to you".to_string(),
));
if other_user.permissions.check_manager() && !user.permissions.check_admin() {
return Err(Error::MiscError(
"Cannot manage the role of other managers".to_string(),
));
}
if other_user.permissions == user.permissions {
return Err(Error::MiscError(
"Cannot manage users of equal level to you".to_string(),
));
}
}
// ...
@ -645,6 +650,9 @@ impl DataManager {
auto_method!(update_user_connections(UserConnections)@get_user_by_id -> "UPDATE users SET connections = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user);
auto_method!(update_user_subscriptions(HashMap<usize, usize>)@get_user_by_id -> "UPDATE users SET subscriptions = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user);
auto_method!(get_user_by_stripe_id(&str)@get_user_from_row -> "SELECT * FROM users WHERE stripe_id = $1" --name="user" --returns=User);
auto_method!(update_user_stripe_id(&str)@get_user_by_id -> "UPDATE users SET stripe_id = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user);
auto_method!(incr_user_notifications()@get_user_by_id -> "UPDATE users SET notification_count = notification_count + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --incr);
auto_method!(decr_user_notifications()@get_user_by_id -> "UPDATE users SET notification_count = notification_count - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --decr);

View file

@ -30,6 +30,8 @@ impl DataManager {
execute!(&conn, common::CREATE_TABLE_IPBLOCKS).unwrap();
execute!(&conn, common::CREATE_TABLE_CHANNELS).unwrap();
execute!(&conn, common::CREATE_TABLE_MESSAGES).unwrap();
execute!(&conn, common::CREATE_TABLE_UPLOADS).unwrap();
execute!(&conn, common::CREATE_TABLE_EMOJIS).unwrap();
self.2
.set("atto.active_connections:users".to_string(), "0".to_string())

View file

@ -15,3 +15,5 @@ pub const CREATE_TABLE_QUESTIONS: &str = include_str!("./sql/create_questions.sq
pub const CREATE_TABLE_IPBLOCKS: &str = include_str!("./sql/create_ipblocks.sql");
pub const CREATE_TABLE_CHANNELS: &str = include_str!("./sql/create_channels.sql");
pub const CREATE_TABLE_MESSAGES: &str = include_str!("./sql/create_messages.sql");
pub const CREATE_TABLE_UPLOADS: &str = include_str!("./sql/create_uploads.sql");
pub const CREATE_TABLE_EMOJIS: &str = include_str!("./sql/create_emojis.sql");

View file

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS emojis (
id BIGINT NOT NULL PRIMARY KEY,
created BIGINT NOT NULL,
owner BIGINT NOT NULL,
community BIGINT NOT NULL,
upload_id BIGINT NOT NULL,
name TEXT NOT NULL
)

View file

@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS uploads (
id BIGINT NOT NULL PRIMARY KEY,
created BIGINT NOT NULL,
owner BIGINT NOT NULL,
what TEXT NOT NULL
)

View file

@ -0,0 +1,191 @@
use super::*;
use crate::cache::Cache;
use crate::model::{
Error, Result, auth::User, permissions::FinePermission,
communities_permissions::CommunityPermission, uploads::CustomEmoji,
};
use crate::{auto_method, execute, get, query_row, query_rows, params};
#[cfg(feature = "sqlite")]
use rusqlite::Row;
#[cfg(feature = "postgres")]
use tokio_postgres::Row;
impl DataManager {
/// Get a [`CustomEmoji`] from an SQL row.
pub(crate) fn get_emoji_from_row(
#[cfg(feature = "sqlite")] x: &Row<'_>,
#[cfg(feature = "postgres")] x: &Row,
) -> CustomEmoji {
CustomEmoji {
id: get!(x->0(i64)) as usize,
owner: get!(x->1(i64)) as usize,
created: get!(x->2(i64)) as usize,
community: get!(x->3(i64)) as usize,
upload_id: get!(x->4(i64)) as usize,
name: get!(x->5(String)),
}
}
auto_method!(get_emoji_by_id(usize as i64)@get_emoji_from_row -> "SELECT * FROM emojis WHERE id = $1" --name="emoji" --returns=CustomEmoji --cache-key-tmpl="atto.emoji:{}");
/// Get all emojis by community.
///
/// # Arguments
/// * `community` - the ID of the community to fetch emojis for
pub async fn get_emojis_by_community(&self, community: usize) -> Result<Vec<CustomEmoji>> {
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_rows!(
&conn,
"SELECT * FROM emojis WHERE community = $1 ORDER BY name ASC",
&[&(community as i64)],
|x| { Self::get_emoji_from_row(x) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("emoji".to_string()));
}
Ok(res.unwrap())
}
/// Get all emojis by user.
///
/// # Arguments
/// * `user` - the ID of the user to fetch emojis for
pub async fn get_emojis_by_user(&self, user: usize) -> Result<Vec<CustomEmoji>> {
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_rows!(
&conn,
"SELECT * FROM emojis WHERE (owner = $1 OR members LIKE $2) AND community = 0 ORDER BY created DESC",
params![&(user as i64), &format!("%{user}%")],
|x| { Self::get_emoji_from_row(x) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("emoji".to_string()));
}
Ok(res.unwrap())
}
/// Get a emoji given its `owner` and a member.
///
/// # Arguments
/// * `owner` - the ID of the owner
/// * `member` - the ID of the member
pub async fn get_emoji_by_owner_member(
&self,
owner: usize,
member: usize,
) -> Result<CustomEmoji> {
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_row!(
&conn,
"SELECT * FROM emojis WHERE owner = $1 AND members = $2 AND community = 0 ORDER BY created DESC",
params![&(owner as i64), &format!("[{member}]")],
|x| { Ok(Self::get_emoji_from_row(x)) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("emoji".to_string()));
}
Ok(res.unwrap())
}
/// Create a new emoji in the database.
///
/// # Arguments
/// * `data` - a mock [`CustomEmoji`] object to insert
pub async fn create_emoji(&self, data: CustomEmoji) -> Result<()> {
let user = self.get_user_by_id(data.owner).await?;
// check user permission in community
if data.community != 0 {
let membership = self
.get_membership_by_owner_community(user.id, data.community)
.await?;
if !membership.role.check(CommunityPermission::MANAGE_EMOJIS)
&& !user.permissions.check(FinePermission::MANAGE_EMOJIS)
{
return Err(Error::NotAllowed);
}
}
// ...
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = execute!(
&conn,
"INSERT INTO emojis VALUES ($1, $2, $3, $4, $5, $6)",
params![
&(data.id as i64),
&(data.owner as i64),
&(data.created as i64),
&(data.community as i64),
&(data.upload_id as i64),
&data.name
]
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
Ok(())
}
pub async fn delete_emoji(&self, id: usize, user: &User) -> Result<()> {
let emoji = self.get_emoji_by_id(id).await?;
// check user permission in community
if user.id != emoji.owner {
let membership = self
.get_membership_by_owner_community(user.id, emoji.community)
.await?;
if !membership.role.check(CommunityPermission::MANAGE_EMOJIS) {
return Err(Error::NotAllowed);
}
}
// ...
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = execute!(&conn, "DELETE FROM emojis WHERE id = $1", &[&(id as i64)]);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
// delete uploads
self.delete_upload(emoji.upload_id).await?;
// ...
self.2.remove(format!("atto.emoji:{}", id)).await;
Ok(())
}
auto_method!(update_emoji_name(&str)@get_emoji_by_id:MANAGE_EMOJIS -> "UPDATE emojis SET title = $1 WHERE id = $2" --cache-key-tmpl="atto.emoji:{}");
}

View file

@ -4,6 +4,7 @@ mod common;
mod communities;
pub mod connections;
mod drivers;
mod emojis;
mod ipbans;
mod ipblocks;
mod memberships;
@ -13,6 +14,7 @@ mod questions;
mod reactions;
mod reports;
mod requests;
mod uploads;
mod user_warnings;
mod userblocks;
mod userfollows;

View file

@ -0,0 +1,122 @@
use std::fs::{exists, remove_file};
use super::*;
use crate::cache::Cache;
use crate::model::{Error, Result, uploads::MediaUpload};
use crate::{auto_method, execute, get, query_row, query_rows, params};
use pathbufd::PathBufD;
#[cfg(feature = "sqlite")]
use rusqlite::Row;
#[cfg(feature = "postgres")]
use tokio_postgres::Row;
impl DataManager {
/// Get a [`MediaUpload`] from an SQL row.
pub(crate) fn get_upload_from_row(
#[cfg(feature = "sqlite")] x: &Row<'_>,
#[cfg(feature = "postgres")] x: &Row,
) -> MediaUpload {
MediaUpload {
id: get!(x->0(i64)) as usize,
created: get!(x->1(i64)) as usize,
owner: get!(x->2(i64)) as usize,
what: serde_json::from_str(&get!(x->3(String))).unwrap(),
}
}
auto_method!(get_upload_by_id(usize as i64)@get_upload_from_row -> "SELECT * FROM uploads WHERE id = $1" --name="upload" --returns=MediaUpload --cache-key-tmpl="atto.uploads:{}");
/// Get all uploads (paginated).
///
/// # Arguments
/// * `batch` - the limit of items in each page
/// * `page` - the page number
pub async fn get_uploads(&self, batch: usize, page: usize) -> Result<Vec<MediaUpload>> {
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_rows!(
&conn,
"SELECT * FROM uploads ORDER BY created DESC LIMIT $1 OFFSET $2",
&[&(batch as i64), &((page * batch) as i64)],
|x| { Self::get_upload_from_row(x) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("upload".to_string()));
}
Ok(res.unwrap())
}
/// Create a new upload in the database.
///
/// # Arguments
/// * `data` - a mock [`MediaUpload`] object to insert
pub async fn create_upload(&self, data: MediaUpload) -> Result<()> {
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = execute!(
&conn,
"INSERT INTO uploads VALUES ($1, $2, $3, $4)",
params![
&(data.id as i64),
&(data.created as i64),
&(data.owner as i64),
&serde_json::to_string(&data.what).unwrap().as_str(),
]
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
// return
Ok(())
}
pub async fn delete_upload(&self, id: usize) -> Result<()> {
// if !user.permissions.check(FinePermission::MANAGE_UPLOADS) {
// return Err(Error::NotAllowed);
// }
// delete file
// it's most important that the file gets off the file system first, even
// if there's an issue in the database
//
// the actual file takes up much more space than the database entry.
let path = PathBufD::current().extend(&[self.0.dirs.media.as_str(), "uploads"]);
if let Ok(exists) = exists(&path) {
if exists {
if let Err(e) = remove_file(&path) {
return Err(Error::MiscError(e.to_string()));
}
}
} else {
return Err(Error::GeneralNotFound("file".to_string()));
}
// delete from database
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = execute!(&conn, "DELETE FROM uploads WHERE id = $1", &[&(id as i64)]);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
self.2.remove(format!("atto.upload:{}", id)).await;
// return
Ok(())
}
}