Skip to content
Clone
acbf7ea
116 lines 3.6 KB Rust
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<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.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::<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 = export::users::find_by_username(&service.state, lower).await?;
if let Some(space) = &ctx.space {
if let Some(candidate) = &candidate {
if space.owner_id == candidate.user_id {
return Ok(Resolved::User(candidate.user_id));
}
let member = space::authority::member(
&service.state,
space.owner_id,
space.id,
candidate.user_id,
)
.await?;
if member.is_some_and(|m| m.status == MemberStatus::Active) {
return Ok(Resolved::User(candidate.user_id));
}
}
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);
}
if let Some(candidate) = candidate {
let member = room_members::Entity::find_by_id((ctx.room.id, candidate.user_id))
.one(service.db().read())
.await?;
if member.is_some_and(|m| m.left_at.is_none()) {
return Ok(Resolved::User(candidate.user_id));
}
}
Ok(Resolved::None)
}