use models::room::room_members; use models::space::space_groups; use models::space::space_members::{self, MemberStatus}; use models::users::user; use sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter}; use uuid::Uuid; use crate::errors::ChatError; use crate::services::service::ChatService; use crate::share::lookup::RoomContext; #[derive(Debug, Default)] pub struct ParsedMentions { pub users: Vec, pub groups: Vec, pub everyone: bool, } pub async fn parse_mentions( service: &ChatService, ctx: &RoomContext, content: &str, exclude: i64, ) -> Result { if !content.contains('@') { return Ok(ParsedMentions::default()); } let max = service.config().chat.max_mentions_per_message; let mut parsed = ParsedMentions::default(); for token in content.split_whitespace() { let Some(rest) = token.strip_prefix('@') else { continue; }; let clean = rest.trim_matches(|c: char| !c.is_alphanumeric() && c != '-' && c != '_'); if clean.is_empty() { continue; } let lower = clean.to_lowercase(); if lower == "channel" || lower == "here" || lower == "everyone" { parsed.everyone = true; continue; } if let Ok(uid) = clean.parse::() { if uid != exclude && !parsed.users.contains(&uid) && parsed.users.len() < max { parsed.users.push(uid); } continue; } if parsed.users.len() + parsed.groups.len() >= max { continue; } match resolve_name(service, ctx, &lower).await? { Resolved::User(uid) if uid != exclude && !parsed.users.contains(&uid) => { parsed.users.push(uid) } Resolved::Group(gid) if !parsed.groups.contains(&gid) => parsed.groups.push(gid), _ => {} } } Ok(parsed) } enum Resolved { User(i64), Group(Uuid), None, } async fn resolve_name( service: &ChatService, ctx: &RoomContext, lower: &str, ) -> Result { // Targeted lookup by username/name, then verify the resolved user is // actually visible in this room's context (avoids full member scans). let candidates = user::Entity::find() .filter( Condition::any() .add(user::Column::LowerName.eq(lower)) .add(user::Column::Username.eq(lower)), ) .all(service.db().read()) .await?; if let Some(space) = &ctx.space { for candidate in candidates { if space.owner_id == candidate.uid { return Ok(Resolved::User(candidate.uid)); } let member = space_members::Entity::find_by_id((space.id, candidate.uid)) .one(service.db().read()) .await?; if member.is_some_and(|m| m.status == MemberStatus::Active) { return Ok(Resolved::User(candidate.uid)); } } let group = space_groups::Entity::find() .filter(space_groups::Column::SpaceId.eq(space.id)) .filter(space_groups::Column::Name.eq(lower)) .one(service.db().read()) .await?; if let Some(group) = group { return Ok(Resolved::Group(group.id)); } return Ok(Resolved::None); } for candidate in candidates { let member = room_members::Entity::find_by_id((ctx.room.id, candidate.uid)) .one(service.db().read()) .await?; if member.is_some_and(|m| m.left_at.is_none()) { return Ok(Resolved::User(candidate.uid)); } } Ok(Resolved::None) }