feat: pure vibes

feat: implement API
This commit is contained in:
2025-06-06 12:07:17 +01:00
parent 41773cc3a0
commit b295d7e7be
12 changed files with 1258 additions and 150 deletions

View File

@ -1,17 +1,33 @@
-- Add migration script here
create table user
(
id integer unsigned not null auto_increment primary key,
pubkey binary(32) not null,
created timestamp default current_timestamp,
balance bigint not null default 0,
tos_accepted timestamp,
stream_key text not null default uuid(),
is_admin bool not null default false,
is_blocked bool not null default false,
recording bool not null default false
id integer unsigned not null auto_increment primary key,
pubkey binary(32) not null,
created timestamp not null default current_timestamp,
balance bigint not null default 0,
tos_accepted timestamp,
stream_key text not null default uuid(),
is_admin bool not null default false,
is_blocked bool not null default false,
recording bool not null default false,
title text,
summary text,
image text,
tags text,
content_warning text,
goal text
);
create unique index ix_user_pubkey on user (pubkey);
-- Add ingest endpoints table for pipeline configuration (must come before user_stream)
create table ingest_endpoint
(
id integer unsigned not null auto_increment primary key,
name varchar(255) not null,
cost bigint unsigned not null default 10000,
capabilities text
);
create table user_stream
(
id varchar(50) not null primary key,
@ -35,7 +51,56 @@ create table user_stream
fee integer unsigned,
-- current nostr event json
event text,
-- endpoint id if using specific endpoint
endpoint_id integer unsigned,
-- timestamp of last segment
last_segment timestamp,
constraint fk_user_stream_user
foreign key (user_id) references user (id),
constraint fk_user_stream_endpoint
foreign key (endpoint_id) references ingest_endpoint (id)
);
-- Add forwards table for payment forwarding
create table user_stream_forward
(
id integer unsigned not null auto_increment primary key,
user_id integer unsigned not null,
name text not null,
target text not null,
constraint fk_user_stream_forward_user
foreign key (user_id) references user (id)
);
);
-- Add keys table for stream keys
create table user_stream_key
(
id integer unsigned not null auto_increment primary key,
user_id integer unsigned not null,
`key` text not null,
created timestamp not null default current_timestamp,
expires timestamp,
stream_id varchar(50) not null,
constraint fk_user_stream_key_user
foreign key (user_id) references user (id),
constraint fk_user_stream_key_stream
foreign key (stream_id) references user_stream (id)
);
-- Add payments table for payment logging
create table payment
(
payment_hash binary(32) not null primary key,
user_id integer unsigned not null,
invoice text,
is_paid bool not null default false,
amount bigint unsigned not null,
created timestamp not null default current_timestamp,
nostr text,
payment_type tinyint unsigned not null,
fee bigint unsigned not null default 0,
constraint fk_payment_user
foreign key (user_id) references user (id)
);

View File

@ -1,6 +1,8 @@
use crate::{User, UserStream};
use crate::{
IngestEndpoint, Payment, PaymentType, User, UserStream, UserStreamForward, UserStreamKey,
};
use anyhow::Result;
use sqlx::{Executor, MySqlPool, Row};
use sqlx::{MySqlPool, Row};
use uuid::Uuid;
#[derive(Clone)]
@ -53,6 +55,15 @@ impl ZapStreamDb {
Ok(())
}
/// Mark TOS as accepted for a user
pub async fn accept_tos(&self, uid: u64) -> Result<()> {
sqlx::query("update user set tos_accepted = NOW() where id = ?")
.bind(uid)
.execute(&self.db)
.await?;
Ok(())
}
pub async fn upsert_user(&self, pubkey: &[u8; 32]) -> Result<u64> {
let res = sqlx::query("insert ignore into user(pubkey) values(?) returning id")
.bind(pubkey.as_slice())
@ -153,4 +164,220 @@ impl ZapStreamDb {
Ok(balance)
}
/// Create a new forward
pub async fn create_forward(&self, user_id: u64, name: &str, target: &str) -> Result<u64> {
let result =
sqlx::query("insert into user_stream_forward (user_id, name, target) values (?, ?, ?)")
.bind(user_id)
.bind(name)
.bind(target)
.execute(&self.db)
.await?;
Ok(result.last_insert_id())
}
/// Get all forwards for a user
pub async fn get_user_forwards(&self, user_id: u64) -> Result<Vec<UserStreamForward>> {
Ok(
sqlx::query_as("select * from user_stream_forward where user_id = ?")
.bind(user_id)
.fetch_all(&self.db)
.await?,
)
}
/// Delete a forward
pub async fn delete_forward(&self, user_id: u64, forward_id: u64) -> Result<()> {
sqlx::query("delete from user_stream_forward where id = ? and user_id = ?")
.bind(forward_id)
.bind(user_id)
.execute(&self.db)
.await?;
Ok(())
}
/// Create a new stream key
pub async fn create_stream_key(
&self,
user_id: u64,
key: &str,
expires: Option<chrono::DateTime<chrono::Utc>>,
stream_id: &str,
) -> Result<u64> {
let result = sqlx::query(
"insert into user_stream_key (user_id, key, expires, stream_id) values (?, ?, ?, ?)",
)
.bind(user_id)
.bind(key)
.bind(expires)
.bind(stream_id)
.execute(&self.db)
.await?;
Ok(result.last_insert_id())
}
/// Get all stream keys for a user
pub async fn get_user_stream_keys(&self, user_id: u64) -> Result<Vec<UserStreamKey>> {
Ok(
sqlx::query_as("select * from user_stream_key where user_id = ?")
.bind(user_id)
.fetch_all(&self.db)
.await?,
)
}
/// Delete a stream key
pub async fn delete_stream_key(&self, user_id: u64, key_id: u64) -> Result<()> {
sqlx::query("delete from user_stream_key where id = ? and user_id = ?")
.bind(key_id)
.bind(user_id)
.execute(&self.db)
.await?;
Ok(())
}
/// Find user by stream key (including temporary keys)
pub async fn find_user_by_any_stream_key(&self, key: &str) -> Result<Option<u64>> {
#[cfg(feature = "test-pattern")]
if key == "test" {
return Ok(Some(self.upsert_user(&[0; 32]).await?));
}
// First check primary stream key
if let Some(uid) = self.find_user_stream_key(key).await? {
return Ok(Some(uid));
}
// Then check temporary stream keys
Ok(sqlx::query("select user_id from user_stream_key where key = ? and (expires is null or expires > now())")
.bind(key)
.fetch_optional(&self.db)
.await?
.map(|r| r.try_get(0).unwrap()))
}
/// Create a payment record
pub async fn create_payment(
&self,
payment_hash: &[u8],
user_id: u64,
invoice: Option<&str>,
amount: u64,
payment_type: PaymentType,
fee: u64,
) -> Result<()> {
sqlx::query("insert into payment (payment_hash, user_id, invoice, amount, payment_type, fee) values (?, ?, ?, ?, ?, ?)")
.bind(payment_hash)
.bind(user_id)
.bind(invoice)
.bind(amount)
.bind(payment_type)
.bind(fee)
.execute(&self.db)
.await?;
Ok(())
}
/// Mark payment as paid
pub async fn mark_payment_paid(&self, payment_hash: &[u8]) -> Result<()> {
sqlx::query("update payment set is_paid = true where payment_hash = ?")
.bind(payment_hash)
.execute(&self.db)
.await?;
Ok(())
}
/// Update payment fee and mark as paid
pub async fn complete_payment(&self, payment_hash: &[u8], fee: u64) -> Result<()> {
sqlx::query("update payment set fee = ?, is_paid = true where payment_hash = ?")
.bind(fee)
.bind(payment_hash)
.execute(&self.db)
.await?;
Ok(())
}
/// Get payment by hash
pub async fn get_payment(&self, payment_hash: &[u8]) -> Result<Option<Payment>> {
Ok(
sqlx::query_as("select * from payment where payment_hash = ?")
.bind(payment_hash)
.fetch_optional(&self.db)
.await?,
)
}
/// Get payment history for user
pub async fn get_payment_history(
&self,
user_id: u64,
offset: u64,
limit: u64,
) -> Result<Vec<Payment>> {
Ok(sqlx::query_as(
"select * from payment where user_id = ? order by created desc limit ? offset ?",
)
.bind(user_id)
.bind(limit)
.bind(offset)
.fetch_all(&self.db)
.await?)
}
/// Update user default stream info
pub async fn update_user_defaults(
&self,
user_id: u64,
title: Option<&str>,
summary: Option<&str>,
image: Option<&str>,
tags: Option<&str>,
content_warning: Option<&str>,
goal: Option<&str>,
) -> Result<()> {
sqlx::query("update user set title = ?, summary = ?, image = ?, tags = ?, content_warning = ?, goal = ? where id = ?")
.bind(title)
.bind(summary)
.bind(image)
.bind(tags)
.bind(content_warning)
.bind(goal)
.bind(user_id)
.execute(&self.db)
.await?;
Ok(())
}
/// Get all ingest endpoints
pub async fn get_ingest_endpoints(&self) -> Result<Vec<IngestEndpoint>> {
Ok(sqlx::query_as("select * from ingest_endpoint")
.fetch_all(&self.db)
.await?)
}
/// Get ingest endpoint by id
pub async fn get_ingest_endpoint(&self, endpoint_id: u64) -> Result<Option<IngestEndpoint>> {
Ok(sqlx::query_as("select * from ingest_endpoint where id = ?")
.bind(endpoint_id)
.fetch_optional(&self.db)
.await?)
}
/// Create ingest endpoint
pub async fn create_ingest_endpoint(
&self,
name: &str,
cost: u64,
capabilities: Option<&str>,
) -> Result<u64> {
let result =
sqlx::query("insert into ingest_endpoint (name, cost, capabilities) values (?, ?, ?)")
.bind(name)
.bind(cost)
.bind(capabilities)
.execute(&self.db)
.await?;
Ok(result.last_insert_id())
}
}

View File

@ -22,6 +22,18 @@ pub struct User {
pub is_blocked: bool,
/// Streams are recorded
pub recording: bool,
/// Default stream title
pub title: Option<String>,
/// Default stream summary
pub summary: Option<String>,
/// Default stream image
pub image: Option<String>,
/// Default tags (comma separated)
pub tags: Option<String>,
/// Default content warning
pub content_warning: Option<String>,
/// Default stream goal
pub goal: Option<String>,
}
#[derive(Default, Debug, Clone, Type)]
@ -64,4 +76,56 @@ pub struct UserStream {
pub duration: f32,
pub fee: Option<u32>,
pub event: Option<String>,
pub endpoint_id: Option<u64>,
pub last_segment: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, FromRow)]
pub struct UserStreamForward {
pub id: u64,
pub user_id: u64,
pub name: String,
pub target: String,
}
#[derive(Debug, Clone, FromRow)]
pub struct UserStreamKey {
pub id: u64,
pub user_id: u64,
pub key: String,
pub created: DateTime<Utc>,
pub expires: Option<DateTime<Utc>>,
pub stream_id: String,
}
#[derive(Default, Debug, Clone, Type)]
#[repr(u8)]
pub enum PaymentType {
#[default]
TopUp = 0,
Zap = 1,
Credit = 2,
Withdrawal = 3,
AdmissionFee = 4,
}
#[derive(Debug, Clone, FromRow)]
pub struct Payment {
pub payment_hash: Vec<u8>,
pub user_id: u64,
pub invoice: Option<String>,
pub is_paid: bool,
pub amount: u64,
pub created: DateTime<Utc>,
pub nostr: Option<String>,
pub payment_type: PaymentType,
pub fee: u64,
}
#[derive(Debug, Clone, FromRow)]
pub struct IngestEndpoint {
pub id: u64,
pub name: String,
pub cost: u64,
pub capabilities: Option<String>, // JSON array stored as string
}