diff --git a/scripts/delete_name b/scripts/delete_name new file mode 100755 index 0000000..5aaa568 --- /dev/null +++ b/scripts/delete_name @@ -0,0 +1,29 @@ +#!/bin/bash + +usage() { + echo "Usage: $0 NAME" + echo " NAME - The name to be used." + echo " Note: This script requires the 'pubhex' secret to be set in the NIP05_SEC environment variable." + echo "Dependencies:" + echo " nostrkeytool - A tool for NOSTR keys, installable via 'cargo install nostrkeytool' (https://crates.io/crates/nostrkeytool)." + echo " nak - A tool required for authentication, installable via 'go install github.com/fiatjaf/nak@latest' (https://github.com/fiatjaf/nak)." + echo "" + echo "Example:" + echo " export NIP05_SEC=nsec1j40appu959h3gedew92t9ty05mg32sc4l6e4qvrz80rryu4x7kuqxxx" + echo " $0 daniel" + exit 1 +} + +if [ "$#" -lt 1 ]; then + usage +fi + +NAME="$1" + +BASE64_DELETE_AUTH_EVENT=$(nak event --content='' --kind 27235 -t method='DELETE' -t u="https://nos.social/api/names/$NAME" --sec $NIP05_SEC | base64) + +HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE "https://nos.social/api/names/$NAME" \ + -H "Content-Type: application/json" \ + -H "Authorization: Nostr $BASE64_DELETE_AUTH_EVENT") + +echo "HTTP Status from delete: $HTTP_STATUS" diff --git a/src/nameRecord.js b/src/nameRecord.js index c6200d5..cc2e17f 100644 --- a/src/nameRecord.js +++ b/src/nameRecord.js @@ -9,6 +9,10 @@ export default class NameRecord { userAgent = "", updatedAt ) { + if (!pubkey) { + throw new AppError(422, "Pubkey is required."); + } + validateName(name); this.name = name; diff --git a/src/nameRecordRepository.js b/src/nameRecordRepository.js index e4ef53f..c35b235 100644 --- a/src/nameRecordRepository.js +++ b/src/nameRecordRepository.js @@ -66,9 +66,7 @@ export default class NameRecordRepository { } pipeline.set(`updated_at:${pubkey}`, updated_at); - pipeline.zadd(`name_record_updates`, timestamp, name); - // Keep the latest maxEntries records by removing older ones - pipeline.zremrangebyrank(`name_record_updates`, 0, -(MAX_ENTRIES + 1)); + pipeline.zadd(`pending_notifications`, timestamp, name); await pipeline.exec(); } @@ -83,31 +81,25 @@ export default class NameRecordRepository { pipeline.del(`ip:${pubkey}`); pipeline.del(`user_agent:${pubkey}`); pipeline.del(`updated_at:${pubkey}`); - pipeline.zrem(`name_record_updates`, name); + pipeline.zrem(`pending_notifications`, name); await pipeline.exec(); return true; } - async findLatest(limit = 10) { - const names = await this.redis.zrevrange( - "name_record_updates", - 0, - limit - 1 - ); - const records = await Promise.all( - names.map((name) => this.findByName(name)) - ); + async fetchAndClearPendingNotifications() { + const luaScript = ` + local entries = redis.call('ZRANGE', 'pending_notifications', 0, -1) + redis.call('DEL', 'pending_notifications') + return entries + `; - return records; - } + const names = await this.redis.eval(luaScript, 0); - async setLastSentEntryTimestamp(timestamp) { - await this.redis.set("lastSentEntryTimestamp", timestamp); - } + const records = ( + await Promise.all(names.map((name) => this.findByName(name))) + ).filter(Boolean); - async getLastSentEntryTimestamp() { - const timestamp = await this.redis.get("lastSentEntryTimestamp"); - return timestamp ? parseInt(timestamp, 10) : null; + return records; } } diff --git a/src/slackNotifier.js b/src/slackNotifier.js index 0485d87..6d8fe80 100644 --- a/src/slackNotifier.js +++ b/src/slackNotifier.js @@ -9,19 +9,9 @@ export default async function fetchAndSendLatestEntries(repo) { return; } - const latestEntries = await repo.findLatest(config.latestEntriesCount); + const pendingEntries = await repo.fetchAndClearPendingNotifications(); - const lastSentEntryTimestamp = await repo.getLastSentEntryTimestamp(); - - if ( - latestEntries.length === 0 || - new Date(latestEntries[0].updatedAt).getTime() <= lastSentEntryTimestamp - ) { - logger.info("No new changes to send to Slack."); - return; - } - - const message = latestEntries + const message = pendingEntries .map( (entry, index) => `${index + 1}. https://njump.me/${entry.name}@nos.social\n` + @@ -33,11 +23,12 @@ export default async function fetchAndSendLatestEntries(repo) { ) .join("\n"); - await sendSlackMessage(`Latest ${latestEntries.length} entries:\n${message}`); - logger.info("Sent latest entries to Slack."); - await repo.setLastSentEntryTimestamp( - new Date(latestEntries[0].updatedAt).getTime() - ); + if (pendingEntries.length > 0) { + await sendSlackMessage(`Latest entries:\n${message}`); + logger.info("Sent latest entries to Slack."); + } else { + logger.info("No new changes to send to Slack."); + } } async function sendSlackMessage(message) { diff --git a/test/app.test.js b/test/app.test.js index 95d245e..8dade2d 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -3,7 +3,8 @@ import getRedisClient from "../src/getRedisClient.js"; import app from "../src/app.js"; import config from "../config/index.js"; import { getNip98AuthToken, createUserPayload } from "./testUtils.js"; -import test from "../config/test.js"; +import NameRecord from "../src/nameRecord.js"; +import NameRecordRepository from "../src/nameRecordRepository.js"; const notSystemSecret = "73685b53bdf5ac16498f2dc6a9891d076039adbe7eebff88b7f7ac72963450e2"; @@ -379,4 +380,52 @@ describe("Nostr NIP 05 API tests", () => { .expect(401); }); }); + + it("should save notifications and then fetch and clear them correctly", async () => { + const testRecords = [ + new NameRecord( + "testName1", + "pubkey1", + ["wss://relay1.com"], + "clientIp1", + "userAgent1", + new Date().toISOString() + ), + new NameRecord( + "testName2", + "pubkey2", + ["wss://relay2.com"], + "clientIp2", + "userAgent2", + new Date().toISOString() + ), + ]; + + const repo = new NameRecordRepository(redisClient); + + for (const record of testRecords) { + await repo.save(record); + } + + const pendingCountBefore = await redisClient.zcount( + "pending_notifications", + "-inf", + "+inf" + ); + expect(pendingCountBefore).toEqual(testRecords.length); + + const fetchedRecords = await repo.fetchAndClearPendingNotifications(); + + const fetchedNames = fetchedRecords.map((record) => record.name); + expect(fetchedNames.sort()).toEqual( + testRecords.map((record) => record.name).sort() + ); + + const pendingCountAfter = await redisClient.zcount( + "pending_notifications", + "-inf", + "+inf" + ); + expect(pendingCountAfter).toEqual(0); + }); });