use space::MemberStatus; 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.settings.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 candidate = auth::users::find_by_username(&service.audience.accounts, lower).await?; if let Some(space) = &ctx.space { if let Some(candidate) = &candidate { if space.owner() == candidate.user_id { return Ok(Resolved::User(candidate.user_id)); } let member = service .participants .access .member(space.id(), candidate.user_id) .await?; if member.is_some_and(|m| m.status() == MemberStatus::Active) { return Ok(Resolved::User(candidate.user_id)); } } if let Some(group) = space.group_id_by_name(lower).await? { return Ok(Resolved::Group(group)); } return Ok(Resolved::None); } if let Some(candidate) = candidate && crate::share::lookup::is_room_member(service, ctx.room.id, candidate.user_id).await? { return Ok(Resolved::User(candidate.user_id)); } Ok(Resolved::None) }