use serde::{Deserialize, Serialize};
pub mod event_kind {
pub const CHAT_MESSAGE: u16 = 9;
pub const PRIVATE_DIRECT_MESSAGE: u16 = 14;
pub const FILE_ATTACHMENT: u16 = 15;
pub const MESSAGE_EDIT: u16 = 16;
pub const REACTION: u16 = 7;
pub const APPLICATION_SPECIFIC: u16 = 30078;
pub const COMMUNITY_MESSAGE: u16 = 3300;
pub const COMMUNITY_REACTION: u16 = 3301;
pub const COMMUNITY_EDIT: u16 = 3302;
pub const COMMUNITY_REKEY: u16 = 3303;
pub const COMMUNITY_INVITE_BUNDLE: u16 = 3304;
pub const COMMUNITY_DELETE: u16 = 3305;
pub const COMMUNITY_PRESENCE: u16 = 3306;
pub const COMMUNITY_KICK: u16 = 3309;
pub const COMMUNITY_CONTROL: u16 = 3308;
pub const COMMUNITY_WEBXDC: u16 = 3310;
pub const COMMUNITY_TYPING: u16 = 3311;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[repr(u8)]
pub enum SystemEventType {
MemberLeft = 0,
MemberJoined = 1,
MemberRemoved = 2,
WallpaperChanged = 3,
WallpaperRemoved = 4,
}
impl SystemEventType {
pub fn display_message(&self, display_name: &str) -> String {
match self {
SystemEventType::MemberLeft => format!("{} has left", display_name),
SystemEventType::MemberJoined => format!("{} has joined", display_name),
SystemEventType::MemberRemoved => format!("{} was removed", display_name),
SystemEventType::WallpaperChanged => format!("{} changed the wallpaper", display_name),
SystemEventType::WallpaperRemoved => format!("{} removed the wallpaper", display_name),
}
}
pub fn as_u8(&self) -> u8 {
*self as u8
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredEvent {
pub id: String,
pub kind: u16,
pub chat_id: i64,
pub user_id: Option<i64>,
pub content: String,
pub tags: Vec<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reference_id: Option<String>,
pub created_at: u64,
pub received_at: u64,
pub mine: bool,
#[serde(default)]
pub pending: bool,
#[serde(default)]
pub failed: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub wrapper_event_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub npub: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preview_metadata: Option<String>,
}
impl StoredEvent {
pub fn new(id: String, kind: u16, chat_id: i64, content: String, created_at: u64) -> Self {
Self {
id,
kind,
chat_id,
user_id: None,
content,
tags: Vec::new(),
reference_id: None,
created_at,
received_at: current_timestamp_ms(),
mine: false,
pending: false,
failed: false,
wrapper_event_id: None,
npub: None,
preview_metadata: None,
}
}
pub fn is_message(&self) -> bool {
self.kind == event_kind::PRIVATE_DIRECT_MESSAGE
|| self.kind == event_kind::FILE_ATTACHMENT
}
pub fn is_reaction(&self) -> bool {
self.kind == event_kind::REACTION
}
pub fn is_known_kind(&self) -> bool {
matches!(
self.kind,
event_kind::PRIVATE_DIRECT_MESSAGE
| event_kind::FILE_ATTACHMENT
| event_kind::REACTION
| event_kind::APPLICATION_SPECIFIC
)
}
pub fn get_tag(&self, key: &str) -> Option<&str> {
self.tags
.iter()
.find(|tag| tag.first().map(|s| s.as_str()) == Some(key))
.and_then(|tag| tag.get(1))
.map(|s| s.as_str())
}
pub fn get_tags(&self, key: &str) -> Vec<&str> {
self.tags
.iter()
.filter(|tag| tag.first().map(|s| s.as_str()) == Some(key))
.filter_map(|tag| tag.get(1))
.map(|s| s.as_str())
.collect()
}
pub fn get_reply_reference(&self) -> Option<&str> {
self.tags
.iter()
.find(|tag| {
tag.first().map(|s| s.as_str()) == Some("e")
&& tag.get(3).map(|s| s.as_str()) == Some("reply")
})
.and_then(|tag| tag.get(1))
.map(|s| s.as_str())
}
pub fn timestamp_ms(&self) -> u64 {
if let Some(ms_str) = self.get_tag("ms") {
if let Ok(ms) = ms_str.parse::<u64>() {
if ms <= 999 {
return self.created_at * 1000 + ms;
}
}
}
self.created_at * 1000
}
}
fn current_timestamp_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
#[derive(Debug, Default)]
pub struct StoredEventBuilder {
id: String,
kind: u16,
chat_id: i64,
user_id: Option<i64>,
content: String,
tags: Vec<Vec<String>>,
reference_id: Option<String>,
created_at: u64,
mine: bool,
pending: bool,
failed: bool,
wrapper_event_id: Option<String>,
npub: Option<String>,
}
impl StoredEventBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = id.into();
self
}
pub fn kind(mut self, kind: u16) -> Self {
self.kind = kind;
self
}
pub fn chat_id(mut self, chat_id: i64) -> Self {
self.chat_id = chat_id;
self
}
pub fn user_id(mut self, user_id: Option<i64>) -> Self {
self.user_id = user_id;
self
}
pub fn content(mut self, content: impl Into<String>) -> Self {
self.content = content.into();
self
}
pub fn tags(mut self, tags: Vec<Vec<String>>) -> Self {
self.tags = tags;
self
}
pub fn reference_id(mut self, reference_id: Option<String>) -> Self {
self.reference_id = reference_id;
self
}
pub fn created_at(mut self, created_at: u64) -> Self {
self.created_at = created_at;
self
}
pub fn mine(mut self, mine: bool) -> Self {
self.mine = mine;
self
}
pub fn pending(mut self, pending: bool) -> Self {
self.pending = pending;
self
}
pub fn failed(mut self, failed: bool) -> Self {
self.failed = failed;
self
}
pub fn wrapper_event_id(mut self, wrapper_event_id: Option<String>) -> Self {
self.wrapper_event_id = wrapper_event_id;
self
}
pub fn npub(mut self, npub: Option<String>) -> Self {
self.npub = npub;
self
}
pub fn build(self) -> StoredEvent {
StoredEvent {
id: self.id,
kind: self.kind,
chat_id: self.chat_id,
user_id: self.user_id,
content: self.content,
tags: self.tags,
reference_id: self.reference_id,
created_at: self.created_at,
received_at: current_timestamp_ms(),
mine: self.mine,
pending: self.pending,
failed: self.failed,
wrapper_event_id: self.wrapper_event_id,
npub: self.npub,
preview_metadata: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stored_event_new() {
let event = StoredEvent::new(
"abc123".to_string(),
event_kind::PRIVATE_DIRECT_MESSAGE,
1,
"Hello world".to_string(),
1234567890,
);
assert_eq!(event.id, "abc123");
assert_eq!(event.kind, 14);
assert!(event.is_message());
assert!(!event.is_reaction());
assert!(event.is_known_kind());
}
#[test]
fn test_get_tag() {
let mut event = StoredEvent::new(
"abc123".to_string(),
event_kind::PRIVATE_DIRECT_MESSAGE,
1,
"Hello".to_string(),
1234567890,
);
event.tags = vec![
vec!["e".to_string(), "ref123".to_string(), "".to_string(), "reply".to_string()],
vec!["ms".to_string(), "500".to_string()],
];
assert_eq!(event.get_tag("ms"), Some("500"));
assert_eq!(event.get_reply_reference(), Some("ref123"));
}
#[test]
fn test_timestamp_ms() {
let mut event = StoredEvent::new(
"abc123".to_string(),
event_kind::PRIVATE_DIRECT_MESSAGE,
1,
"Hello".to_string(),
1234567890,
);
assert_eq!(event.timestamp_ms(), 1234567890000);
event.tags = vec![vec!["ms".to_string(), "456".to_string()]];
assert_eq!(event.timestamp_ms(), 1234567890456);
}
#[test]
fn test_unknown_kind() {
let event = StoredEvent::new(
"abc123".to_string(),
65535, 1,
"Unknown content".to_string(),
1234567890,
);
assert!(!event.is_message());
assert!(!event.is_reaction());
assert!(!event.is_known_kind());
}
#[test]
fn test_builder() {
let event = StoredEventBuilder::new()
.id("abc123")
.kind(event_kind::REACTION)
.chat_id(1)
.content("👍")
.reference_id(Some("msg456".to_string()))
.mine(true)
.build();
assert_eq!(event.id, "abc123");
assert!(event.is_reaction());
assert_eq!(event.reference_id, Some("msg456".to_string()));
assert!(event.mine);
}
}