入門
NIP-40 過期時間戳
Nostr 事件過期機制,用於設定事件的有效期限。
5 分鐘
概述
NIP-40 定義了事件過期機制,讓發布者可以指定事件的有效期限。 過期後,中繼器應刪除該事件,客戶端也不應顯示。這對於臨時公告、 限時優惠或隱私敏感內容非常有用。
Expiration 標籤
使用 expiration 標籤指定事件的過期時間,值為 Unix 時間戳(秒):
{`["expiration", ""]`} 事件範例
{`{
"id": "...",
"pubkey": "...",
"created_at": 1700000000,
"kind": 1,
"tags": [
["expiration", "1700086400"]
],
"content": "這則訊息將在 24 小時後過期",
"sig": "..."
}`} 實作
建立過期事件
{`import { finalizeEvent, getPublicKey } from 'nostr-tools';
// 建立帶有過期時間的事件
function createExpiringEvent(
privateKey: Uint8Array,
content: string,
expiresInSeconds: number
) {
const now = Math.floor(Date.now() / 1000);
const expiration = now + expiresInSeconds;
const event = {
kind: 1,
created_at: now,
tags: [
['expiration', expiration.toString()],
],
content,
};
return finalizeEvent(event, privateKey);
}
// 建立 1 小時後過期的事件
const hourEvent = createExpiringEvent(
privateKey,
'這則訊息 1 小時後過期',
3600
);
// 建立 24 小時後過期的事件
const dayEvent = createExpiringEvent(
privateKey,
'這則訊息 24 小時後過期',
86400
);
// 建立特定日期過期的事件
function createEventExpiringAt(
privateKey: Uint8Array,
content: string,
expirationDate: Date
) {
const expiration = Math.floor(expirationDate.getTime() / 1000);
const event = {
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [
['expiration', expiration.toString()],
],
content,
};
return finalizeEvent(event, privateKey);
}
// 建立在 2024 年底過期的事件
const newYearEvent = createEventExpiringAt(
privateKey,
'2024 年特別公告',
new Date('2024-12-31T23:59:59Z')
);`} 檢查事件是否過期
{`// 檢查事件是否已過期
function isEventExpired(event: Event): boolean {
const expirationTag = event.tags.find(t => t[0] === 'expiration');
if (!expirationTag) {
return false; // 沒有過期標籤,永不過期
}
const expiration = parseInt(expirationTag[1], 10);
const now = Math.floor(Date.now() / 1000);
return now > expiration;
}
// 取得過期時間
function getExpiration(event: Event): Date | null {
const expirationTag = event.tags.find(t => t[0] === 'expiration');
if (!expirationTag) {
return null;
}
const expiration = parseInt(expirationTag[1], 10);
return new Date(expiration * 1000);
}
// 取得剩餘時間
function getTimeRemaining(event: Event): number | null {
const expirationTag = event.tags.find(t => t[0] === 'expiration');
if (!expirationTag) {
return null;
}
const expiration = parseInt(expirationTag[1], 10);
const now = Math.floor(Date.now() / 1000);
const remaining = expiration - now;
return remaining > 0 ? remaining : 0;
}
// 格式化剩餘時間
function formatTimeRemaining(seconds: number): string {
if (seconds <= 0) return '已過期';
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (days > 0) return \`\${days} 天 \${hours} 小時\`;
if (hours > 0) return \`\${hours} 小時 \${minutes} 分鐘\`;
return \`\${minutes} 分鐘\`;
}`} 過濾過期事件
{`import { SimplePool } from 'nostr-tools';
const pool = new SimplePool();
const relays = ['wss://relay.damus.io', 'wss://nos.lol'];
// 查詢事件並過濾過期的
async function getActiveEvents(pubkey: string) {
const events = await pool.querySync(relays, {
kinds: [1],
authors: [pubkey],
limit: 50,
});
// 過濾掉已過期的事件
return events.filter(event => !isEventExpired(event));
}
// 訂閱事件時自動過濾過期
function subscribeToActiveEvents(
pubkey: string,
onEvent: (event: Event) => void
) {
return pool.subscribeMany(
relays,
[{ kinds: [1], authors: [pubkey] }],
{
onevent: (event) => {
if (!isEventExpired(event)) {
onEvent(event);
}
},
}
);
}`} 中繼器行為
符合 NIP-40 的中繼器應該:
- 拒絕已過期事件:不接受 expiration 時間已過的事件
- 定期清理:刪除已過期的事件以節省儲存空間
- 不返回過期事件:查詢時不返回已過期的事件
注意
並非所有中繼器都支援 NIP-40。某些中繼器可能會忽略過期標籤,繼續儲存和提供過期事件。 客戶端應該自行過濾過期事件,不要完全依賴中繼器的行為。
應用場景
限時公告
{`// 活動公告,活動結束後過期
const eventAnnouncement = createEventExpiringAt(
privateKey,
'🎉 線上聚會今晚 8 點開始!加入連結:...',
new Date('2024-03-15T22:00:00Z')
);`} 閃購優惠
{`// 限時優惠,24 小時後過期
const flashSale = createExpiringEvent(
privateKey,
'⚡ 限時優惠!使用代碼 NOSTR24 享 50% 折扣',
86400
);`} 臨時訊息
{`// 臨時位置分享,1 小時後過期
const locationShare = createExpiringEvent(
privateKey,
'📍 我現在在台北 101,想見面的朋友來找我',
3600
);`} 投票與問卷
{`// 投票,截止時間後過期
const poll = {
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [
['expiration', (Math.floor(Date.now() / 1000) + 604800).toString()], // 1 週
['poll_option', '0', '選項 A'],
['poll_option', '1', '選項 B'],
],
content: '你比較喜歡哪個?投票將在一週後截止。',
};`} UI 顯示建議
{`// React 組件範例
function ExpiringNote({ event }: { event: Event }) {
const [remaining, setRemaining] = useState(getTimeRemaining(event));
useEffect(() => {
const interval = setInterval(() => {
const newRemaining = getTimeRemaining(event);
setRemaining(newRemaining);
if (newRemaining === 0) {
clearInterval(interval);
}
}, 60000); // 每分鐘更新
return () => clearInterval(interval);
}, [event]);
if (remaining === null) {
// 沒有過期時間
return ;
}
if (remaining === 0) {
// 已過期,不顯示
return null;
}
return (
⏱️ {formatTimeRemaining(remaining)}
);
}`} 注意事項
- 不可撤銷:一旦設定過期時間,無法延長(除非發布新事件)
- 非強制性:過期只是建議,無法保證所有副本都被刪除
- 時區問題:使用 Unix 時間戳避免時區混淆
- 快取問題:客戶端快取可能仍包含過期事件
相關 NIP
參考資源
已複製連結