Files
zep/spacetimedb/src/utils.rs
T
2026-04-21 22:29:20 -04:00

499 lines
14 KiB
Rust

use crate::tables::*;
use spacetimedb::{Identity, Local, LocalReadOnly, Table};
use std::collections::{HashMap, HashSet};
pub fn validate_name(name: &str) -> Result<(), String> {
let trimmed = name.trim();
if trimmed.is_empty() {
return Err("Name cannot be empty".to_string());
}
if trimmed.len() > 32 {
return Err("Name too long".to_string());
}
Ok(())
}
pub fn validate_message_length(db: &Local, text: &str) -> Result<(), String> {
let limit = db
.system_configuration()
.key()
.find("max_message_length".to_string())
.and_then(|c| c.value.parse::<usize>().ok())
.unwrap_or(2000);
if text.len() > limit {
return Err(format!("Message exceeds {} characters", limit));
}
Ok(())
}
pub fn get_recent_message_limit_read_only(db: &LocalReadOnly) -> u64 {
db.system_configuration()
.key()
.find("recent_message_limit".to_string())
.and_then(|c| c.value.parse::<u64>().ok())
.unwrap_or(50)
}
pub fn get_recent_message_limit(db: &Local) -> u64 {
db.system_configuration()
.key()
.find("recent_message_limit".to_string())
.and_then(|c| c.value.parse::<u64>().ok())
.unwrap_or(50)
}
pub fn get_next_seq_id(db: &Local, channel_id: u64) -> u64 {
if let Some(mut state) = db.channel_internal_state().channel_id().find(channel_id) {
state.last_seq_id += 1;
let new_id = state.last_seq_id;
db.channel_internal_state().channel_id().update(state);
new_id
} else {
db.channel_internal_state().insert(ChannelInternalState {
channel_id,
last_seq_id: 1,
});
1
}
}
/// Simplified: uses UserChannelAccess table directly
pub fn get_visible_message_ids(db: &Local, identity: Identity) -> HashSet<u64> {
db.user_channel_access()
.identity()
.filter(identity)
.map(|a| a.channel_id)
.collect()
}
pub fn get_visible_message_ids_read_only(db: &LocalReadOnly, identity: Identity) -> HashSet<u64> {
db.user_channel_access()
.identity()
.filter(identity)
.map(|a| a.channel_id)
.collect()
}
pub fn get_visible_image_ids(db: &Local, identity: Identity) -> HashSet<u64> {
let mut results = HashSet::new();
let accessible_channels = get_visible_message_ids(db, identity);
for channel_id in accessible_channels {
for msg in db.message().channel_id().filter(channel_id) {
for id in msg.image_ids {
results.insert(id);
}
}
}
// User's own avatar/banner
if let Some(user) = db.user().identity().find(identity) {
if let Some(id) = user.avatar_id {
results.insert(id);
}
if let Some(id) = user.banner_id {
results.insert(id);
}
}
// Server avatars for servers I am a member of or are public
let my_server_ids: HashSet<u64> = db
.server_member()
.identity()
.filter(identity)
.map(|m| m.server_id)
.collect();
for s in db.server().name().filter(""..) {
if s.public || my_server_ids.contains(&s.id) {
if let Some(id) = s.avatar_id {
results.insert(id);
}
}
}
// Avatars for members of servers I am in (and redundant check for server avatars stored in membership)
for server_id in my_server_ids {
for member in db.server_member().server_id().filter(server_id) {
if let Some(id) = member.avatar_id {
results.insert(id);
}
}
// Also check if any server I'm in has an avatar id that might not have been caught in the name filter
if let Some(s) = db.server().id().find(server_id) {
if let Some(id) = s.avatar_id {
results.insert(id);
}
}
}
// Avatars for DM participants
for dm in db.direct_message().sender().filter(identity) {
if let Some(u) = db.user().identity().find(dm.recipient) {
if let Some(id) = u.avatar_id {
results.insert(id);
}
}
}
for dm in db.direct_message().recipient().filter(identity) {
if let Some(u) = db.user().identity().find(dm.sender) {
if let Some(id) = u.avatar_id {
results.insert(id);
}
}
}
results
}
pub fn get_visible_image_ids_read_only(db: &LocalReadOnly, identity: Identity) -> HashSet<u64> {
let mut results = HashSet::new();
let accessible_channels = get_visible_message_ids_read_only(db, identity);
for channel_id in accessible_channels {
for msg in db.message().channel_id().filter(channel_id) {
for id in msg.image_ids {
results.insert(id);
}
}
}
// User's own avatar/banner
if let Some(user) = db.user().identity().find(identity) {
if let Some(id) = user.avatar_id {
results.insert(id);
}
if let Some(id) = user.banner_id {
results.insert(id);
}
}
// Server avatars for servers I am a member of or are public
let my_server_ids: HashSet<u64> = db
.server_member()
.identity()
.filter(identity)
.map(|m| m.server_id)
.collect();
for s in db.server().name().filter(""..) {
if s.public || my_server_ids.contains(&s.id) {
if let Some(id) = s.avatar_id {
results.insert(id);
}
}
}
// Avatars for members of servers I am in (and redundant check for server avatars stored in membership)
for server_id in my_server_ids {
for member in db.server_member().server_id().filter(server_id) {
if let Some(id) = member.avatar_id {
results.insert(id);
}
}
// Also check if any server I'm in has an avatar id that might not have been caught in the name filter
if let Some(s) = db.server().id().find(server_id) {
if let Some(id) = s.avatar_id {
results.insert(id);
}
}
}
// Avatars for DM participants
for dm in db.direct_message().sender().filter(identity) {
if let Some(u) = db.user().identity().find(dm.recipient) {
if let Some(id) = u.avatar_id {
results.insert(id);
}
}
}
for dm in db.direct_message().recipient().filter(identity) {
if let Some(u) = db.user().identity().find(dm.sender) {
if let Some(id) = u.avatar_id {
results.insert(id);
}
}
}
results
}
pub fn internal_open_direct_message(db: &Local, sender: Identity, recipient: Identity) -> u64 {
let existing = db
.direct_message()
.sender()
.filter(sender)
.find(|dm| dm.recipient == recipient)
.or_else(|| {
db.direct_message()
.sender()
.filter(recipient)
.find(|dm| dm.recipient == sender)
});
if let Some(mut dm) = existing {
if dm.sender == sender {
dm.is_open_sender = true;
} else {
dm.is_open_recipient = true;
}
db.direct_message().id().update(dm.clone());
dm.channel_id
} else {
let chan = db.channel().insert(Channel {
id: 0,
server_id: 0,
name: "dm".to_string(),
kind: ChannelKind::Text,
});
db.direct_message().insert(DirectMessage {
id: 0,
channel_id: chan.id,
sender,
recipient,
is_open_sender: true,
is_open_recipient: true,
});
grant_user_channel_access(db, sender, chan.id);
grant_user_channel_access(db, recipient, chan.id);
chan.id
}
}
pub fn internal_send_message(
db: &Local,
sender: Identity,
channel_id: u64,
text: String,
timestamp: spacetimedb::Timestamp,
thread_id: Option<u64>,
image_ids: Vec<u64>,
is_encrypted: bool,
) {
let seq_id = get_next_seq_id(db, channel_id);
let server_id = db
.channel()
.id()
.find(channel_id)
.map(|c| c.server_id)
.unwrap_or(0);
let msg = db.message().insert(Message {
id: 0,
sender,
sent: timestamp,
text,
channel_id,
server_id,
thread_id,
reactions: Vec::new(),
image_ids,
thread_name: None,
thread_reply_count: 0,
edited: false,
is_encrypted,
seq_id,
});
sync_recent_message(db, msg.clone());
// Plan D: Update parent message reply count
if let Some(tid) = thread_id {
if let Some(mut parent) = db.message().id().find(tid) {
parent.thread_reply_count += 1;
let parent = db.message().id().update(parent);
sync_recent_message(db, parent);
}
}
}
pub fn sync_recent_message(db: &Local, msg: Message) {
let recent = RecentMessage {
id: msg.id,
sender: msg.sender,
sent: msg.sent,
text: msg.text.clone(),
channel_id: msg.channel_id,
server_id: msg.server_id,
thread_id: msg.thread_id,
reactions: msg.reactions,
image_ids: msg.image_ids,
thread_name: msg.thread_name,
thread_reply_count: msg.thread_reply_count,
edited: msg.edited,
is_encrypted: msg.is_encrypted,
seq_id: msg.seq_id,
};
// 1. Upsert: Update if exists, otherwise insert
if db.recent_message().id().find(msg.id).is_some() {
db.recent_message().id().update(recent);
} else {
db.recent_message().insert(recent);
}
// 2. Prune to 50
let limit = get_recent_message_limit(db);
let count = db
.recent_message()
.channel_id()
.filter(msg.channel_id)
.count();
if count > limit as usize {
// Find all recent messages for this channel
let mut recent_msgs: Vec<_> = db
.recent_message()
.channel_id()
.filter(msg.channel_id)
.collect();
// Sort by seq_id ascending
recent_msgs.sort_by_key(|m| m.seq_id);
// Delete the oldest ones until we are back at the limit
let to_delete = count - limit as usize;
for i in 0..to_delete {
db.recent_message().id().delete(recent_msgs[i].id);
}
}
}
pub fn sync_server_member_info(db: &Local, identity: Identity) {
if let Some(user) = db.user().identity().find(identity) {
for mut member in db.server_member().identity().filter(identity) {
member.name = user.name.clone();
member.avatar_id = user.avatar_id;
member.online = user.online;
db.server_member().id().update(member);
}
}
}
pub fn grant_user_channel_access(db: &Local, identity: Identity, channel_id: u64) {
let exists = db
.user_channel_access()
.identity()
.filter(identity)
.any(|a| a.channel_id == channel_id);
if !exists {
db.user_channel_access().insert(UserChannelAccess {
id: 0,
identity,
channel_id,
});
}
}
pub fn revoke_user_channel_access(db: &Local, identity: Identity, channel_id: u64) {
let to_delete: Vec<_> = db
.user_channel_access()
.identity()
.filter(identity)
.filter(|a| a.channel_id == channel_id)
.map(|a| a.id)
.collect();
for id in to_delete {
db.user_channel_access().id().delete(id);
}
}
pub fn sync_server_access(db: &Local, identity: Identity, server_id: u64) {
for c in db.channel().server_id().filter(server_id) {
grant_user_channel_access(db, identity, c.id);
}
}
pub fn revoke_server_access(db: &Local, identity: Identity, server_id: u64) {
for c in db.channel().server_id().filter(server_id) {
revoke_user_channel_access(db, identity, c.id);
}
}
pub fn clear_user_presence(db: &Local, identity: Identity) {
if let Some(_) = db.user_state().identity().find(identity) {
db.user_state().identity().delete(identity);
}
clear_signaling_for_user(db, identity);
}
pub fn clear_signaling_for_user(db: &Local, identity: Identity) {
let signals: Vec<_> = db
.webrtc_signal()
.sender()
.filter(identity)
.map(|s| s.id)
.collect();
for id in signals {
db.webrtc_signal().id().delete(id);
}
}
pub fn report_error(
db: &Local,
identity: Identity,
reducer_name: &str,
error: &str,
timestamp: spacetimedb::Timestamp,
) {
db.reducer_status().identity().delete(identity);
db.reducer_status().insert(ReducerStatus {
identity,
reducer_name: reducer_name.to_string(),
status: "error".to_string(),
error: Some(error.to_string()),
last_update: timestamp,
});
}
pub fn report_success(
db: &Local,
identity: Identity,
reducer_name: &str,
timestamp: spacetimedb::Timestamp,
) {
db.reducer_status().identity().delete(identity);
db.reducer_status().insert(ReducerStatus {
identity,
reducer_name: reducer_name.to_string(),
status: "success".to_string(),
error: None,
last_update: timestamp,
});
}
pub fn report_success_with_payload(
db: &Local,
identity: Identity,
reducer_name: &str,
payload: &str,
timestamp: spacetimedb::Timestamp,
) {
db.reducer_status().identity().delete(identity);
db.reducer_status().insert(ReducerStatus {
identity,
reducer_name: reducer_name.to_string(),
status: "success".to_string(),
error: Some(payload.to_string()), // We reuse the error field as a general-purpose result payload for success
last_update: timestamp,
});
}
pub const PERM_MANAGE_SERVER: u128 = 0x01;
pub const PERM_MANAGE_ROLES: u128 = 0x02;
pub const PERM_MANAGE_CHANNELS: u128 = 0x04;
pub const PERM_CREATE_INVITES: u128 = 0x08;
pub const PERM_KICK_MEMBERS: u128 = 0x10;
pub const PERM_BAN_MEMBERS: u128 = 0x20;
pub const PERM_MODERATE_MESSAGES: u128 = 0x40;
pub const PERM_USE_VOICE: u128 = 0x80;
pub const PERM_SHARE_SCREEN: u128 = 0x100;
pub const PERM_USE_THREADS: u128 = 0x200;
pub const PERM_MANAGE_EMOJIS: u128 = 0x400;
pub const PERM_DELETE_SERVER: u128 = 0x800;
pub fn has_permission(db: &Local, identity: Identity, server_id: u64, bit: u128) -> bool {
if let Some(perm) = db.server_permission().server_id().filter(server_id)
.find(|p| p.identity == identity) {
return (perm.permissions & bit) != 0;
}
false
}