821537186e
- Reorganized import statements in adapter tests for better readability - Replaced or_insert_with(Vec::new) with or_default() in test closures - Updated Cargo.lock with new dependency versions and checksums - Added TLS features to tonic dependency configuration - Included sqlx, chrono, and uuid dependencies with specific features - Added jsonwebtoken and arc-swap as project dependencies - Reformatted assertion statements to comply with line length limits - Adjusted base64 import order in engine codec module - Updated protobuf include statement formatting
124 lines
3.4 KiB
Rust
124 lines
3.4 KiB
Rust
//! @mention tracking — maps to `message_mention` table.
|
|
//!
|
|
//! Parsed from message body on send. Used for notification and
|
|
//! "mentions" feed.
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
use uuid::Uuid;
|
|
|
|
/// An @mention of a user in a message.
|
|
///
|
|
/// Maps to the `message_mention` table. Parsed from message body on send
|
|
/// and used for notifications and the mentions feed.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
|
pub struct MessageMention {
|
|
pub id: Uuid,
|
|
pub message_id: Uuid,
|
|
pub channel_id: Uuid,
|
|
pub mentioned_user_id: Uuid,
|
|
pub mentioned_by: Uuid,
|
|
/// When the mentioned user read the notification.
|
|
pub read_at: Option<DateTime<Utc>>,
|
|
pub created_at: DateTime<Utc>,
|
|
}
|
|
|
|
/// Parse @username mentions from a message body.
|
|
/// Returns unique usernames (without the `@` prefix).
|
|
///
|
|
/// Matches `@word` where word is `[a-zA-Z0-9_-]+`, min 2 chars.
|
|
/// Ignores `@@` (escaped) and mentions inside code spans/blocks.
|
|
pub fn parse_mentions(body: &str) -> Vec<String> {
|
|
let mut seen = std::collections::HashSet::new();
|
|
let mut mentions = Vec::new();
|
|
|
|
// Simple state machine: skip content inside backtick spans.
|
|
let mut in_code = false;
|
|
let bytes = body.as_bytes();
|
|
let mut i = 0;
|
|
|
|
while i < bytes.len() {
|
|
if bytes[i] == b'`' {
|
|
in_code = !in_code;
|
|
i += 1;
|
|
continue;
|
|
}
|
|
|
|
if !in_code && bytes[i] == b'@' {
|
|
// Skip escaped @@
|
|
if i + 1 < bytes.len() && bytes[i + 1] == b'@' {
|
|
i += 2;
|
|
continue;
|
|
}
|
|
|
|
let start = i + 1;
|
|
let mut end = start;
|
|
while end < bytes.len()
|
|
&& (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_' || bytes[end] == b'-')
|
|
{
|
|
end += 1;
|
|
}
|
|
|
|
let len = end - start;
|
|
if len >= 2 {
|
|
let name = body[start..end].to_lowercase();
|
|
if seen.insert(name.clone()) {
|
|
mentions.push(name);
|
|
}
|
|
}
|
|
i = end;
|
|
} else {
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
mentions
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_parse_mentions_basic() {
|
|
let m = parse_mentions("hey @alice, cc @bob");
|
|
assert_eq!(m, vec!["alice".to_string(), "bob".to_string()]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_mentions_dedup() {
|
|
let m = parse_mentions("@alice said hi to @alice");
|
|
assert_eq!(m, vec!["alice".to_string()]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_mentions_case_insensitive() {
|
|
let m = parse_mentions("@Alice and @ALICE");
|
|
assert_eq!(m, vec!["alice".to_string()]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_mentions_skip_code_span() {
|
|
let m = parse_mentions("use `@here` to notify @alice");
|
|
assert_eq!(m, vec!["alice".to_string()]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_mentions_skip_escaped() {
|
|
let m = parse_mentions("@@alice is not a mention, but @bob is");
|
|
assert_eq!(m, vec!["bob".to_string()]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_mentions_short_name_ignored() {
|
|
let m = parse_mentions("@a is too short, @ab is ok");
|
|
assert_eq!(m, vec!["ab".to_string()]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_mentions_empty() {
|
|
assert!(parse_mentions("no mentions here").is_empty());
|
|
assert!(parse_mentions("").is_empty());
|
|
}
|
|
}
|