Fix: Protocol implementation error
Build and Push Docker Image / build_docker_image (push) Has been cancelled
Build and Push Docker Image / build_docker_image (push) Has been cancelled
This commit is contained in:
@@ -22,3 +22,4 @@ target/
|
||||
.idea/
|
||||
.vscode/
|
||||
.zed/
|
||||
.env
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT * FROM deleted_events WHERE event_id = ? AND pubkey = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "event_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "pubkey",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "kind",
|
||||
"ordinal": 2,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "d_tag",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "deleted_at",
|
||||
"ordinal": 4,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "20cd6a8251512848aaf6b0a5ea721da5d37b0d946f7a0e083afaa9e8fb296e3f"
|
||||
}
|
||||
+13
-7
@@ -6,15 +6,18 @@ pub const DEFAULT_BIND_ADDR: &str = "0.0.0.0:8080";
|
||||
pub const DEFAULT_DB_PATH: &str = "nostr.db";
|
||||
pub const MAX_EVENT_TAGS: u32 = 5000;
|
||||
pub const MAX_LIMIT: u64 = 500;
|
||||
pub const DEFAULT_LIMIT: u64 = 10;
|
||||
pub const DEFAULT_LIMIT: u64 = 50;
|
||||
pub const MAX_FILTERS_PER_REQ: usize = 100;
|
||||
pub const BROADCAST_CHANNEL_SIZE: usize = 100;
|
||||
pub const CLIENT_CHANNEL_SIZE: usize = 32;
|
||||
pub const MAX_SUBSCRIPTIONS: usize = 20;
|
||||
pub static RELAY_URL: Lazy<String> =
|
||||
Lazy::new(|| env::var("RELAY_URL").unwrap_or_else(|_| "".to_string()));
|
||||
|
||||
pub static SERVER_INFO: Lazy<ServerInfo> = Lazy::new(|| ServerInfo {
|
||||
contact: "https://www.moec.top/",
|
||||
description: "Powered by laoXong.",
|
||||
contact: env::var("RELAY_CONTACT").unwrap_or_else(|_| "https://www.moec.top/".to_string()),
|
||||
description: env::var("RELAY_DESCRIPTION")
|
||||
.unwrap_or_else(|_| "Powered by laoXong.".to_string()),
|
||||
limitation: Limitation {
|
||||
max_event_tags: MAX_EVENT_TAGS,
|
||||
max_event_time_newer_than_now: 900,
|
||||
@@ -26,11 +29,14 @@ pub static SERVER_INFO: Lazy<ServerInfo> = Lazy::new(|| ServerInfo {
|
||||
max_subscriptions: MAX_SUBSCRIPTIONS as u32, // usize to u32 cast
|
||||
min_prefix: 10,
|
||||
},
|
||||
name: "A rust nostr relay by laoXong",
|
||||
pubkey: "63abd4f817e39cca4e6abb6e6cf3e133bb718cf8ec28b38c1645e84d7a6190c6",
|
||||
software: "https://git.moe.gift/laoxong/nostr-relay",
|
||||
name: env::var("RELAY_NAME").unwrap_or_else(|_| "A rust nostr relay by laoXong".to_string()),
|
||||
pubkey: env::var("RELAY_PUBKEY").unwrap_or_else(|_| {
|
||||
"63abd4f817e39cca4e6abb6e6cf3e133bb718cf8ec28b38c1645e84d7a6190c6".to_string()
|
||||
}),
|
||||
software: env::var("RELAY_SOFTWARE")
|
||||
.unwrap_or_else(|_| "https://git.moe.gift/laoxong/nostr-relay".to_string()),
|
||||
supported_nips: vec![1, 2, 5, 9, 11, 42, 50, 65],
|
||||
version: env!("CARGO_PKG_VERSION"), // 从 Cargo.toml 获取版本
|
||||
version: env!("CARGO_PKG_VERSION").to_string(), // 从 Cargo.toml 获取版本
|
||||
auth_required: env::var("AUTH_REQUIRED")
|
||||
.unwrap_or_else(|_| "false".to_string())
|
||||
.to_lowercase()
|
||||
|
||||
+21
-1
@@ -15,19 +15,39 @@ pub async fn init_database(pool: &SqlitePool) -> Result<()> {
|
||||
tags TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
sig TEXT NOT NULL,
|
||||
d_tag TEXT,
|
||||
indexed_at INTEGER DEFAULT (unixepoch())
|
||||
);
|
||||
"#;
|
||||
|
||||
let create_deleted_events_table = r#"
|
||||
CREATE TABLE IF NOT EXISTS deleted_events (
|
||||
event_id TEXT,
|
||||
pubkey TEXT NOT NULL,
|
||||
kind INTEGER,
|
||||
d_tag TEXT,
|
||||
deleted_at INTEGER DEFAULT (unixepoch())
|
||||
);
|
||||
"#;
|
||||
|
||||
let create_indexes = vec![
|
||||
"CREATE INDEX IF NOT EXISTS idx_events_pubkey ON events(pubkey);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_events_kind ON events(kind);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_events_pubkey_kind ON events(pubkey, kind);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_events_kind_created_at_desc ON events(kind, created_at DESC);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_events_pubkey_kind_d_tag ON events(pubkey, kind, d_tag);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_deleted_events_pubkey ON deleted_events(pubkey, event_id);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_deleted_events_kind_d_tag ON deleted_events(kind, pubkey, d_tag);",
|
||||
];
|
||||
|
||||
query(create_events_table).execute(pool).await?;
|
||||
// 尝试添加 d_tag 列,如果已存在则忽略错误
|
||||
let _ = query("ALTER TABLE events ADD COLUMN d_tag TEXT DEFAULT ''")
|
||||
.execute(pool)
|
||||
.await;
|
||||
|
||||
query(create_deleted_events_table).execute(pool).await?;
|
||||
|
||||
for index_sql in create_indexes {
|
||||
query(index_sql).execute(pool).await?;
|
||||
@@ -39,7 +59,7 @@ pub async fn init_database(pool: &SqlitePool) -> Result<()> {
|
||||
|
||||
// 获取信任账户列表
|
||||
pub async fn get_trust_accounts(pool: &SqlitePool) -> Vec<String> {
|
||||
let pubkey = SERVER_INFO.pubkey; // 获取服务器公钥
|
||||
let pubkey = &SERVER_INFO.pubkey; // 获取服务器公钥
|
||||
// 查询最新的 kind 3 事件(联系人列表),获取其中的 p 标签作为信任账户
|
||||
let sql =
|
||||
"SELECT tags FROM events WHERE kind = 3 AND pubkey = ? ORDER BY created_at DESC LIMIT 1";
|
||||
|
||||
+6
-6
@@ -7,14 +7,14 @@ use uuid::Uuid;
|
||||
// 服务器信息结构体(用于 NIP-11)
|
||||
#[derive(Serialize)]
|
||||
pub struct ServerInfo {
|
||||
pub contact: &'static str,
|
||||
pub description: &'static str,
|
||||
pub contact: String,
|
||||
pub description: String,
|
||||
pub limitation: Limitation,
|
||||
pub name: &'static str,
|
||||
pub pubkey: &'static str,
|
||||
pub software: &'static str,
|
||||
pub name: String,
|
||||
pub pubkey: String,
|
||||
pub software: String,
|
||||
pub supported_nips: Vec<u32>,
|
||||
pub version: &'static str,
|
||||
pub version: String,
|
||||
pub auth_required: bool,
|
||||
}
|
||||
|
||||
|
||||
+225
-35
@@ -7,12 +7,13 @@ use serde_json::json;
|
||||
use sha2::{Digest, Sha256};
|
||||
use sqlx::SqlitePool;
|
||||
use std::{
|
||||
result,
|
||||
sync::Arc,
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tokio::sync::{RwLock, mpsc};
|
||||
|
||||
use crate::constants::SERVER_INFO;
|
||||
use crate::constants::{RELAY_URL, SERVER_INFO};
|
||||
use crate::models::ClientConnection;
|
||||
use crate::nostr::NostrEvent;
|
||||
use crate::nostr::messages::RelayMessage;
|
||||
@@ -20,7 +21,7 @@ use crate::nostr::messages::RelayMessage;
|
||||
pub trait NostrEventExt {
|
||||
fn serialize_for_id(&self) -> String; // 用于计算事件 ID 的序列化
|
||||
fn verify(&self) -> bool; // 验证 ID、时间戳、标签数量和签名
|
||||
async fn save(&self, pool: &SqlitePool) -> Result<(), sqlx::Error>; // 保存事件到数据库
|
||||
async fn save(&self, pool: &SqlitePool) -> Result<(), anyhow::Error>; // 保存事件到数据库
|
||||
async fn handle_auth_event(
|
||||
&self,
|
||||
client_conn: &Arc<RwLock<ClientConnection>>,
|
||||
@@ -89,6 +90,11 @@ impl NostrEventExt for NostrEvent {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// 验证只有一个e标签
|
||||
if self.tags.iter().filter(|tag| tag[0] == "e").count() > 1 {
|
||||
debug!("Event has more than one e tag");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 解析公钥:从十六进制字符串解析 XOnlyPublicKey
|
||||
let pubkey_bytes: Vec<u8> = match hex::decode(&self.pubkey) {
|
||||
@@ -174,9 +180,17 @@ impl NostrEventExt for NostrEvent {
|
||||
}
|
||||
|
||||
// 保存事件到数据库,并处理可替换事件和删除事件
|
||||
async fn save(&self, pool: &SqlitePool) -> Result<(), sqlx::Error> {
|
||||
async fn save(&self, pool: &SqlitePool) -> Result<(), anyhow::Error> {
|
||||
let tags_json = serde_json::to_string(&self.tags).unwrap();
|
||||
|
||||
// 提取 d_tag
|
||||
let d_tag = self
|
||||
.tags
|
||||
.iter()
|
||||
.find(|tag| tag.len() >= 2 && tag[0] == "d")
|
||||
.map(|tag| tag[1].clone())
|
||||
.unwrap_or_default(); // 默认为空字符串
|
||||
|
||||
match self.kind {
|
||||
// NIP-09 事件删除 (Event Deletion)
|
||||
5 => {
|
||||
@@ -188,65 +202,228 @@ impl NostrEventExt for NostrEvent {
|
||||
"Attempting to delete event with ID: {} by request of pubkey {}",
|
||||
event_id_to_delete, self.pubkey
|
||||
);
|
||||
let sql = "DELETE FROM events WHERE id = ? AND pubkey = ?";
|
||||
let result = sqlx::query(sql)
|
||||
.bind(event_id_to_delete)
|
||||
.bind(&self.pubkey)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
let result =
|
||||
sqlx::query("DELETE FROM events WHERE id = ? AND pubkey = ?")
|
||||
.bind(event_id_to_delete)
|
||||
.bind(&self.pubkey)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
if result.rows_affected() > 0 {
|
||||
info!(
|
||||
"Deleted event {} by pubkey (kind 5): {}",
|
||||
event_id_to_delete, self.pubkey
|
||||
);
|
||||
let result = sqlx::query(
|
||||
"INSERT INTO deleted_events (event_id, pubkey, deleted_at) VALUES (?, ?, ?)",
|
||||
)
|
||||
.bind(event_id_to_delete)
|
||||
.bind(&self.pubkey)
|
||||
.bind(self.created_at as i64)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
if result.rows_affected() > 0 {
|
||||
info!(
|
||||
"Deleted event {} by pubkey (kind 5): {}",
|
||||
event_id_to_delete, self.pubkey
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
"Could not delete event {} for pubkey {}.",
|
||||
event_id_to_delete, self.pubkey
|
||||
);
|
||||
}
|
||||
} else {
|
||||
debug!(
|
||||
"Could not delete event {} for pubkey {}. It might not exist or unauthorized.",
|
||||
"Could not delete event {} for pubkey {}.",
|
||||
event_id_to_delete, self.pubkey
|
||||
);
|
||||
}
|
||||
} else if tag.get(0).map(|s| s == "a").unwrap_or(false) {
|
||||
let d_tag_value = &tag[1];
|
||||
let tag_d_vector = d_tag_value.splitn(3, ':').collect::<Vec<&str>>();
|
||||
if tag_d_vector.len() != 3 {
|
||||
debug!("Invalid a-tag format: {}", d_tag_value);
|
||||
continue;
|
||||
}
|
||||
let result = sqlx::query(
|
||||
"DELETE FROM events WHERE kind = ? AND pubkey = ? AND d_tag = ? AND created_at <= ?;",
|
||||
)
|
||||
.bind(tag_d_vector[0].parse::<i64>().unwrap())
|
||||
.bind(&self.pubkey)
|
||||
.bind(tag_d_vector[2])
|
||||
.bind(self.created_at as i64)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
if result.rows_affected() > 0 {
|
||||
info!(
|
||||
"Deleted event {:?} by pubkey (kind 5): {}",
|
||||
tag_d_vector, self.pubkey
|
||||
);
|
||||
let result = sqlx::query(
|
||||
"INSERT INTO deleted_events (kind, pubkey, d_tag, deleted_at) VALUES (?, ?, ?, ?)",
|
||||
)
|
||||
.bind(tag_d_vector[0])
|
||||
.bind(&self.pubkey)
|
||||
.bind(tag_d_vector[2])
|
||||
.bind(self.created_at as i64)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
if result.rows_affected() > 0 {
|
||||
info!(
|
||||
"Deleted event {:?} by pubkey (kind 5): {}",
|
||||
tag_d_vector, self.pubkey
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
"Could not delete event {:?} for pubkey {}.",
|
||||
tag_d_vector, self.pubkey
|
||||
);
|
||||
}
|
||||
} else {
|
||||
debug!(
|
||||
"Could not delete event {:?} for pubkey {}.",
|
||||
tag_d_vector, self.pubkey
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// NIP-01 可替换事件 (Replaceable Events): kind 0 (Metadata), kind 3 (Contact List)
|
||||
// NIP-16 可替换事件 (Replaceable Events): kinds 10000 to 19999
|
||||
// NIP-33 参数化可替换事件 (Parameterized Replaceable Events): kinds 30000 to 39999
|
||||
// 对于所有这些可替换事件,新事件会替换掉旧事件。
|
||||
// 这里的简化处理是,对于所有可替换事件,都基于 (pubkey, kind) 来删除旧事件。
|
||||
// 对于 NIP-33 事件,严格来说还需要匹配 'd' tag。
|
||||
// 但考虑到数据库操作的复杂性以及原始代码的实现,这里仍采用 (pubkey, kind) 的方式。
|
||||
// 一个更健壮的 NIP-33 实现可能需要单独的字段来存储 'd' tag 或更复杂的 SQL。
|
||||
0 | 3 | _
|
||||
if (self.kind >= 10000 && self.kind < 20000)
|
||||
|| (self.kind >= 30000 && self.kind < 40000) =>
|
||||
{
|
||||
// 对于这些可替换事件,新事件会替换掉旧事件。
|
||||
0 | 3 | _ if (self.kind >= 10000 && self.kind < 20000) => {
|
||||
debug!(
|
||||
"Attempting to delete previous replaceable event for pubkey: {}, kind: {}",
|
||||
self.pubkey, self.kind
|
||||
);
|
||||
let sql = "DELETE FROM events WHERE pubkey = ? AND kind = ?";
|
||||
sqlx::query(sql)
|
||||
let is_deleted = sqlx::query(
|
||||
"SELECT 1 FROM deleted_events WHERE pubkey = ? AND kind = ? AND deleted_at > ?",
|
||||
)
|
||||
.bind(&self.pubkey)
|
||||
.bind(self.kind)
|
||||
.bind(self.created_at as i64)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
if is_deleted.is_some() {
|
||||
return Err(anyhow::anyhow!("Event already deleted"));
|
||||
}
|
||||
let sql = "SELECT created_at FROM events WHERE pubkey = ? AND kind = ? ORDER BY created_at DESC LIMIT 1";
|
||||
let created_at: Option<u64> = sqlx::query_scalar(sql)
|
||||
.bind(&self.pubkey)
|
||||
.bind(self.kind)
|
||||
.execute(pool)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
if let Some(prev_created_at) = created_at {
|
||||
if prev_created_at <= self.created_at {
|
||||
let sql = "DELETE FROM events WHERE pubkey = ? AND kind = ?";
|
||||
sqlx::query(sql)
|
||||
.bind(&self.pubkey)
|
||||
.bind(self.kind)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
debug!(
|
||||
"Deleted previous replaceable event for pubkey: {}, kind: {}",
|
||||
self.pubkey, self.kind
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
"Previous replaceable event for pubkey: {}, kind: {} is not older than current event",
|
||||
self.pubkey, self.kind
|
||||
);
|
||||
// 如果新事件不比旧事件新,则报错
|
||||
anyhow::bail!("Event is not newer than existing replaceable event");
|
||||
}
|
||||
}
|
||||
}
|
||||
// NIP-33 参数化可替换事件 (Parameterized Replaceable Events): kinds 30000 to 39999
|
||||
_ if (self.kind >= 30000 && self.kind < 40000) => {
|
||||
debug!(
|
||||
"Attempting to delete previous parameterized replaceable event for pubkey: {}, kind: {}, d_tag: {}",
|
||||
self.pubkey, self.kind, d_tag
|
||||
);
|
||||
let result = sqlx::query(
|
||||
"SELECT * FROM deleted_events WHERE pubkey = ? AND kind = ? AND d_tag = ? AND deleted_at > ?",
|
||||
)
|
||||
.bind(&self.pubkey)
|
||||
.bind(self.kind)
|
||||
.bind(&d_tag)
|
||||
.bind(self.created_at as i64)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
if let Some(_) = result {
|
||||
debug!(
|
||||
"Event {} already deleted by pubkey {}",
|
||||
self.id, self.pubkey
|
||||
);
|
||||
return Err(anyhow::anyhow!("Event already deleted"));
|
||||
}
|
||||
let sql = "SELECT created_at FROM events WHERE pubkey = ? AND kind = ? AND d_tag = ? ORDER BY created_at DESC LIMIT 1";
|
||||
let created_at: Option<u64> = sqlx::query_scalar(sql)
|
||||
.bind(&self.pubkey)
|
||||
.bind(self.kind)
|
||||
.bind(&d_tag)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
if let Some(prev_created_at) = created_at {
|
||||
if prev_created_at < self.created_at {
|
||||
let sql = "DELETE FROM events WHERE pubkey = ? AND kind = ? AND d_tag = ?";
|
||||
sqlx::query(sql)
|
||||
.bind(&self.pubkey)
|
||||
.bind(self.kind)
|
||||
.bind(&d_tag)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
debug!(
|
||||
"Deleted previous parameterized replaceable event for pubkey: {}, kind: {}, d_tag: {}",
|
||||
self.pubkey, self.kind, d_tag
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
"Previous parameterized replaceable event for pubkey: {}, kind: {}, d_tag: {} is not older than current event",
|
||||
self.pubkey, self.kind, d_tag
|
||||
);
|
||||
anyhow::bail!(
|
||||
"Event is not newer than existing parameterized replaceable event"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => { /* 非可替换事件不需要在插入前删除 */ }
|
||||
}
|
||||
|
||||
// 插入新事件
|
||||
let sql = "INSERT OR IGNORE INTO events (id, pubkey, created_at, kind, tags, content, sig) VALUES (?, ?, ?, ?, ?, ?, ?);";
|
||||
sqlx::query(sql)
|
||||
.bind(&self.id)
|
||||
.bind(&self.pubkey)
|
||||
.bind(self.created_at as i64)
|
||||
.bind(self.kind)
|
||||
.bind(tags_json)
|
||||
.bind(&self.content)
|
||||
.bind(&self.sig)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
if !(self.kind >= 20000 && self.kind < 30000) {
|
||||
let result =
|
||||
sqlx::query("SELECT * FROM deleted_events WHERE event_id = ? AND pubkey = ?")
|
||||
.bind(&self.id)
|
||||
.bind(&self.pubkey)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
if let Some(_) = result {
|
||||
debug!(
|
||||
"Event {} already deleted by pubkey {}",
|
||||
self.id, self.pubkey
|
||||
);
|
||||
return Err(anyhow::anyhow!("Event already deleted"));
|
||||
}
|
||||
let sql = "INSERT OR IGNORE INTO events (id, pubkey, created_at, kind, tags, content, sig, d_tag) VALUES (?, ?, ?, ?, ?, ?, ?, ?);";
|
||||
sqlx::query(sql)
|
||||
.bind(&self.id)
|
||||
.bind(&self.pubkey)
|
||||
.bind(self.created_at as i64)
|
||||
.bind(self.kind)
|
||||
.bind(tags_json)
|
||||
.bind(&self.content)
|
||||
.bind(&self.sig)
|
||||
.bind(&d_tag)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -304,10 +481,23 @@ impl NostrEventExt for NostrEvent {
|
||||
}
|
||||
};
|
||||
|
||||
let relay_url = match relay_url {
|
||||
Some(r) => r,
|
||||
None => {
|
||||
let _ = RelayMessage::send_notice(
|
||||
"AUTH event missing relay tag".to_string(),
|
||||
to_client_msg_tx,
|
||||
)
|
||||
.await;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
// 4. 验证 challenge 是否匹配客户端连接 ID,并且未过期
|
||||
let is_valid_challenge = {
|
||||
let conn = client_conn.read().await;
|
||||
if conn.id.to_string() == *challenge {
|
||||
if relay_url.as_str() != RELAY_URL.as_str() {
|
||||
false
|
||||
} else if conn.id.to_string() == *challenge {
|
||||
// 检查 challenge 是否在 15 分钟内有效 (可配置)
|
||||
let connected_at_duration = UNIX_EPOCH + Duration::from_secs(conn.connected_at);
|
||||
match SystemTime::now().duration_since(connected_at_duration) {
|
||||
|
||||
Reference in New Issue
Block a user