Initial commit

This commit is contained in:
unknown
2026-05-22 02:52:15 +02:00
commit 125321c418
55 changed files with 9231 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
[package]
name = "cgcx-core"
version.workspace = true
edition.workspace = true
[dependencies]
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
chrono = { version = "0.4", features = ["serde"] }
rand = "0.8"

View File

@@ -0,0 +1,75 @@
use rand::Rng;
use serde::{Deserialize, Serialize};
use std::fmt;
const CXID_ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const CXID_LENGTH: usize = 12;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
pub struct ContentId(String);
impl ContentId {
pub fn generate() -> Self {
let mut rng = rand::thread_rng();
let alphabet_len = CXID_ALPHABET.len() as u32;
let mut s = String::with_capacity(CXID_LENGTH);
while s.len() < CXID_LENGTH {
let val: u32 = rng.gen();
// Rejection sampling: only use values that map uniformly
let max = u32::MAX - (u32::MAX % alphabet_len);
if val < max {
s.push(CXID_ALPHABET[(val % alphabet_len) as usize] as char);
}
}
Self(s)
}
pub fn is_valid(s: &str) -> bool {
s.len() == CXID_LENGTH
&& s.bytes().all(|b| CXID_ALPHABET.contains(&b))
}
pub fn as_str(&self) -> &str {
&self.0
}
/// Construct from a string without validation.
/// Only use when the source is trusted (e.g., DB row with FK constraint).
pub fn new_unchecked(s: String) -> Self {
Self(s)
}
}
impl fmt::Display for ContentId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for ContentId {
fn as_ref(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for ContentId {
type Error = crate::CgcxError;
fn try_from(value: String) -> crate::Result<Self> {
if Self::is_valid(&value) {
Ok(Self(value))
} else {
Err(crate::CgcxError::InvalidContentId(value))
}
}
}
impl TryFrom<&str> for ContentId {
type Error = crate::CgcxError;
fn try_from(value: &str) -> crate::Result<Self> {
if Self::is_valid(value) {
Ok(Self(value.to_string()))
} else {
Err(crate::CgcxError::InvalidContentId(value.to_string()))
}
}
}

View File

@@ -0,0 +1,37 @@
pub mod id;
pub mod models;
pub use id::ContentId;
pub use models::*;
#[derive(thiserror::Error, Debug)]
pub enum CgcxError {
#[error("invalid content id: {0}")]
InvalidContentId(String),
#[error("crypto error: {0}")]
Crypto(String),
#[error("database error: {0}")]
Database(String),
#[error("storage error: {0}")]
Storage(String),
#[error("config error: {0}")]
Config(String),
#[error("moderation error: {0}")]
Moderation(String),
#[error("not found")]
NotFound,
#[error("unauthorized")]
Unauthorized,
#[error("forbidden")]
Forbidden,
#[error("rate limited")]
RateLimited,
#[error("bad request: {0}")]
BadRequest(String),
#[error("insufficient storage")]
InsufficientStorage,
#[error("io error: {0}")]
Io(#[from] std::io::Error),
}
pub type Result<T> = std::result::Result<T, CgcxError>;

View File

@@ -0,0 +1,86 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::id::ContentId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum UserRole {
User,
Admin,
Banned,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum ContentStatus {
Staged,
Active,
Deleted,
Blacklisted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum ReportStatus {
Open,
Dismissed,
Actioned,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct User {
pub id: i64,
pub telegram_username: Option<String>,
pub first_name: String,
pub role: UserRole,
pub accepted_terms_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Content {
pub id: ContentId,
pub user_id: i64,
pub status: ContentStatus,
pub view_count: u64,
pub max_views: Option<u64>,
pub allow_download: bool,
pub password_hash: Option<String>,
pub created_at: DateTime<Utc>,
pub deleted_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ContentFile {
pub content_id: ContentId,
pub file_index: u32,
pub original_name: String,
pub stored_path: std::path::PathBuf,
pub mime_type: String,
pub size_bytes: u64,
pub ciphertext_size_bytes: u64,
pub encrypted_key_wrapped: Vec<u8>,
pub encrypted_hash: Vec<u8>,
pub render_flags: u32,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Report {
pub id: i64,
pub content_id: ContentId,
pub reporter_user_id: i64,
pub reason: String,
pub status: ReportStatus,
pub created_at: DateTime<Utc>,
pub resolved_at: Option<DateTime<Utc>>,
pub resolver_id: Option<i64>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AdminAction {
pub id: i64,
pub admin_user_id: i64,
pub target_type: String,
pub target_id: String,
pub action: String,
pub created_at: DateTime<Utc>,
}