499 lines
14 KiB
Rust
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
|
|
}
|