main
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<i64>,
pub groups: Vec<Uuid>,
pub everyone: bool,
}
pub async fn parse_mentions(
service: &ChatService,
ctx: &RoomContext,
content: &str,
exclude: i64,
) -> Result<ParsedMentions, ChatError> {
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::<i64>() {
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<Resolved, ChatError> {
// 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)
}