diff options
-rw-r--r-- | include/nostr.hpp | 20 | ||||
-rw-r--r-- | src/event.cpp | 8 | ||||
-rw-r--r-- | src/nostr_service.cpp | 64 | ||||
-rw-r--r-- | test/nostr_service_test.cpp | 115 |
4 files changed, 136 insertions, 71 deletions
diff --git a/include/nostr.hpp b/include/nostr.hpp index 2b04862..2f37c51 100644 --- a/include/nostr.hpp +++ b/include/nostr.hpp @@ -42,7 +42,7 @@ struct Event * @returns A stringified JSON object representing the event. * @throws `std::invalid_argument` if the event object is invalid. */ - std::string serialize(std::shared_ptr<ISigner> signer); + std::string serialize(); /** * @brief Deserializes the event from a JSON string. @@ -105,14 +105,15 @@ private: class NostrService { -// TODO: Setup signer in the constructor. public: NostrService( std::shared_ptr<plog::IAppender> appender, - std::shared_ptr<client::IWebSocketClient> client); + std::shared_ptr<client::IWebSocketClient> client, + std::shared_ptr<ISigner> signer); NostrService( std::shared_ptr<plog::IAppender> appender, std::shared_ptr<client::IWebSocketClient> client, + std::shared_ptr<ISigner> signer, RelayList relays); ~NostrService(); @@ -149,7 +150,7 @@ public: * to which relays the event was published successfully, and to which relays the event failed * to publish. */ - std::tuple<RelayList, RelayList> publishEvent(Event event); + std::tuple<RelayList, RelayList> publishEvent(std::shared_ptr<Event> event); /** * @brief Queries all open relay connections for events matching the given set of filters. @@ -229,8 +230,8 @@ private: std::unordered_map<std::string, std::vector<std::string>> _subscriptions; ///< A map from subscription IDs to the events returned by the relays for each subscription. std::unordered_map<std::string, std::vector<Event>> _events; - ///< A map from the subscription IDs to the latest read event for each subscription. - std::unordered_map<std::string, std::vector<Event>::iterator> _eventIterators; + ///< A map from the subscription IDs to the ID of the latest read event for each subscription. + std::unordered_map<std::string, std::string> _lastRead; /** * @brief Determines which of the given relays are currently connected. @@ -302,6 +303,11 @@ private: class ISigner { public: - virtual std::string generateSignature(std::shared_ptr<Event> event) = 0; + /** + * @brief Signs the given Nostr event. + * @param event The event to sign. + * @remark The event's `sig` field will be updated in-place with the signature. + */ + virtual void sign(std::shared_ptr<Event> event) = 0; }; } // namespace nostr diff --git a/src/event.cpp b/src/event.cpp index a24a594..e77e33d 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -11,6 +11,7 @@ using nlohmann::json; using std::hex; using std::invalid_argument; +using std::make_shared; using std::setw; using std::setfill; using std::shared_ptr; @@ -20,7 +21,7 @@ using std::time; namespace nostr { -string Event::serialize(shared_ptr<ISigner> signer) +string Event::serialize() { try { @@ -41,7 +42,6 @@ string Event::serialize(shared_ptr<ISigner> signer) }; j["id"] = this->generateId(j.dump()); - j["sig"] = signer->generateSignature(shared_ptr<Event>(this)); json jarr = json::array({ "EVENT", j }); @@ -80,8 +80,8 @@ void Event::validate() throw std::invalid_argument("Event::validate: A valid event kind is required."); } - bool hasSig = this->sig.length() > 0; - if (!hasSig) + bool hasSignature = this->sig.length() > 0; + if (!hasSignature) { throw std::invalid_argument("Event::validate: The event must be signed."); } diff --git a/src/nostr_service.cpp b/src/nostr_service.cpp index ac63f23..971516f 100644 --- a/src/nostr_service.cpp +++ b/src/nostr_service.cpp @@ -1,3 +1,4 @@ +#include <algorithm> #include <boost/uuid/uuid.hpp> #include <boost/uuid/uuid_generators.hpp> #include <boost/uuid/uuid_io.hpp> @@ -15,6 +16,7 @@ using boost::uuids::to_string; using boost::uuids::uuid; using nlohmann::json; using std::async; +using std::find_if; using std::function; using std::future; using std::lock_guard; @@ -33,14 +35,16 @@ namespace nostr { NostrService::NostrService( shared_ptr<plog::IAppender> appender, - shared_ptr<client::IWebSocketClient> client) -: NostrService(appender, client, {}) { }; + shared_ptr<client::IWebSocketClient> client, + shared_ptr<ISigner> signer) +: NostrService(appender, client, signer, {}) { }; NostrService::NostrService( shared_ptr<plog::IAppender> appender, shared_ptr<client::IWebSocketClient> client, + shared_ptr<ISigner> signer, RelayList relays) -: _defaultRelays(relays), _client(client) +: _defaultRelays(relays), _client(client), _signer(signer) { plog::init(plog::debug, appender.get()); client->start(); @@ -118,20 +122,33 @@ void NostrService::closeRelayConnections(RelayList relays) } }; -tuple<RelayList, RelayList> NostrService::publishEvent(Event event) +tuple<RelayList, RelayList> NostrService::publishEvent(shared_ptr<Event> event) { RelayList successfulRelays; RelayList failedRelays; PLOG_INFO << "Attempting to publish event to Nostr relays."; + string serializedEvent; + try + { + this->_signer->sign(event); + serializedEvent = event->serialize(); + } + catch (const std::invalid_argument& error) + { + PLOG_ERROR << "Failed to sign event: " << error.what(); + throw error; + } + + lock_guard<mutex> lock(this->_propertyMutex); vector<future<tuple<string, bool>>> publishFutures; for (const string& relay : this->_activeRelays) { - future<tuple<string, bool>> publishFuture = async([this, &relay, &event]() { - return this->_client->send(event.serialize(this->_signer), relay); + PLOG_INFO << "Entering lambda."; + future<tuple<string, bool>> publishFuture = async([this, relay, serializedEvent]() { + return this->_client->send(serializedEvent, relay); }); - publishFutures.push_back(move(publishFuture)); } @@ -159,7 +176,7 @@ string NostrService::queryRelays(Filters filters) { return this->queryRelays(filters, [this](string subscriptionId, Event event) { lock_guard<mutex> lock(this->_propertyMutex); - this->_eventIterators[subscriptionId] = this->_events[subscriptionId].begin(); + this->_lastRead[subscriptionId] = event.id; this->onEvent(subscriptionId, event); }); }; @@ -228,16 +245,21 @@ vector<Event> NostrService::getNewEvents(string subscriptionId) throw out_of_range("No events found for subscription: " + subscriptionId); } - if (this->_eventIterators.find(subscriptionId) == this->_eventIterators.end()) + if (this->_lastRead.find(subscriptionId) == this->_lastRead.end()) { - PLOG_ERROR << "No event iterator found for subscription: " << subscriptionId; - throw out_of_range("No event iterator found for subscription: " + subscriptionId); + PLOG_ERROR << "No last read event ID found for subscription: " << subscriptionId; + throw out_of_range("No last read event ID found for subscription: " + subscriptionId); } lock_guard<mutex> lock(this->_propertyMutex); vector<Event> newEvents; vector<Event> receivedEvents = this->_events[subscriptionId]; - vector<Event>::iterator eventIt = this->_eventIterators[subscriptionId]; + vector<Event>::iterator eventIt = find_if( + receivedEvents.begin(), + receivedEvents.end(), + [this,subscriptionId](Event event) { + return event.id == this->_lastRead[subscriptionId]; + }) + 1; while (eventIt != receivedEvents.end()) { @@ -480,20 +502,26 @@ void NostrService::onMessage(string message, function<void(const string&, Event) void NostrService::onEvent(string subscriptionId, Event event) { lock_guard<mutex> lock(this->_propertyMutex); - _events[subscriptionId].push_back(event); + this->_events[subscriptionId].push_back(event); PLOG_INFO << "Received event for subscription: " << subscriptionId; // To protect memory, only keep a limited number of events per subscription. - while (_events[subscriptionId].size() > NostrService::MAX_EVENTS_PER_SUBSCRIPTION) + while (this->_events[subscriptionId].size() > NostrService::MAX_EVENTS_PER_SUBSCRIPTION) { - auto startIt = _events[subscriptionId].begin(); - auto eventIt = _eventIterators[subscriptionId]; - - if (eventIt == startIt) + auto events = this->_events[subscriptionId]; + auto eventIt = find_if( + events.begin(), + events.end(), + [this, subscriptionId](Event event) { + return event.id == this->_lastRead[subscriptionId]; + }); + + if (eventIt == events.begin()) { eventIt++; } + this->_lastRead[subscriptionId] = eventIt->id; _events[subscriptionId].erase(_events[subscriptionId].begin()); } }; diff --git a/test/nostr_service_test.cpp b/test/nostr_service_test.cpp index 1679ac5..64c14e8 100644 --- a/test/nostr_service_test.cpp +++ b/test/nostr_service_test.cpp @@ -10,6 +10,7 @@ using std::function; using std::lock_guard; using std::make_shared; +using std::make_unique; using std::mutex; using std::shared_ptr; using std::string; @@ -32,6 +33,15 @@ public: MOCK_METHOD(void, closeConnection, (string uri), (override)); }; +class FakeSigner : public nostr::ISigner +{ +public: + void sign(shared_ptr<nostr::Event> event) override + { + event->sig = "fake_signature"; + } +}; + class NostrServiceTest : public testing::Test { public: @@ -41,27 +51,45 @@ public: "wss://nostr.thesamecat.io" }; + static const nostr::Event getTestEvent() + { + nostr::Event event; + event.pubkey = "13tn5ccv2guflxgffq4aj0hw5x39pz70zcdrfd6vym887gry38zys28dask"; + event.kind = 1; + event.tags = + { + { "e", "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", "wss://nostr.example.com" }, + { "p", "f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca" }, + { "a", "30023:f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca:abcd", "wss://nostr.example.com" } + }; + event.content = "Hello, World!"; + + return event; + }; + protected: shared_ptr<plog::ConsoleAppender<plog::TxtFormatter>> testAppender; - shared_ptr<MockWebSocketClient> testClient; + shared_ptr<MockWebSocketClient> mockClient; + shared_ptr<FakeSigner> fakeSigner; void SetUp() override { testAppender = make_shared<plog::ConsoleAppender<plog::TxtFormatter>>(); - testClient = make_shared<MockWebSocketClient>(); + mockClient = make_shared<MockWebSocketClient>(); + fakeSigner = make_shared<FakeSigner>(); }; }; TEST_F(NostrServiceTest, Constructor_StartsClient) { - EXPECT_CALL(*testClient, start()).Times(1); + EXPECT_CALL(*mockClient, start()).Times(1); - auto nostrService = new nostr::NostrService(testAppender, testClient); + auto nostrService = make_unique<nostr::NostrService>(testAppender, mockClient, fakeSigner); }; TEST_F(NostrServiceTest, Constructor_InitializesService_WithNoDefaultRelays) { - auto nostrService = new nostr::NostrService(testAppender, testClient); + auto nostrService = make_unique<nostr::NostrService>(testAppender, mockClient, fakeSigner); auto defaultRelays = nostrService->defaultRelays(); auto activeRelays = nostrService->activeRelays(); @@ -71,7 +99,7 @@ TEST_F(NostrServiceTest, Constructor_InitializesService_WithNoDefaultRelays) TEST_F(NostrServiceTest, Constructor_InitializesService_WithProvidedDefaultRelays) { - auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); + auto nostrService = make_unique<nostr::NostrService>(testAppender, mockClient, fakeSigner, defaultTestRelays); auto defaultRelays = nostrService->defaultRelays(); auto activeRelays = nostrService->activeRelays(); @@ -85,9 +113,9 @@ TEST_F(NostrServiceTest, Constructor_InitializesService_WithProvidedDefaultRelay TEST_F(NostrServiceTest, Destructor_StopsClient) { - EXPECT_CALL(*testClient, start()).Times(1); + EXPECT_CALL(*mockClient, start()).Times(1); - auto nostrService = new nostr::NostrService(testAppender, testClient); + auto nostrService = make_unique<nostr::NostrService>(testAppender, mockClient, fakeSigner); }; TEST_F(NostrServiceTest, OpenRelayConnections_OpensConnections_ToDefaultRelays) @@ -97,10 +125,10 @@ TEST_F(NostrServiceTest, OpenRelayConnections_OpensConnections_ToDefaultRelays) connectionStatus->insert({ defaultTestRelays[0], false }); connectionStatus->insert({ defaultTestRelays[1], false }); - EXPECT_CALL(*testClient, openConnection(defaultTestRelays[0])).Times(1); - EXPECT_CALL(*testClient, openConnection(defaultTestRelays[1])).Times(1); + EXPECT_CALL(*mockClient, openConnection(defaultTestRelays[0])).Times(1); + EXPECT_CALL(*mockClient, openConnection(defaultTestRelays[1])).Times(1); - EXPECT_CALL(*testClient, isConnected(_)) + EXPECT_CALL(*mockClient, isConnected(_)) .WillRepeatedly(Invoke([connectionStatus, &connectionStatusMutex](string uri) { lock_guard<mutex> lock(connectionStatusMutex); @@ -112,7 +140,7 @@ TEST_F(NostrServiceTest, OpenRelayConnections_OpensConnections_ToDefaultRelays) return status; })); - auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); + auto nostrService = make_unique<nostr::NostrService>(testAppender, mockClient, fakeSigner, defaultTestRelays); nostrService->openRelayConnections(); auto activeRelays = nostrService->activeRelays(); @@ -131,11 +159,11 @@ TEST_F(NostrServiceTest, OpenRelayConnections_OpensConnections_ToProvidedRelays) auto connectionStatus = make_shared<unordered_map<string, bool>>(); connectionStatus -> insert({ testRelays[0], false }); - EXPECT_CALL(*testClient, openConnection(testRelays[0])).Times(1); - EXPECT_CALL(*testClient, openConnection(defaultTestRelays[0])).Times(0); - EXPECT_CALL(*testClient, openConnection(defaultTestRelays[1])).Times(0); + EXPECT_CALL(*mockClient, openConnection(testRelays[0])).Times(1); + EXPECT_CALL(*mockClient, openConnection(defaultTestRelays[0])).Times(0); + EXPECT_CALL(*mockClient, openConnection(defaultTestRelays[1])).Times(0); - EXPECT_CALL(*testClient, isConnected(_)) + EXPECT_CALL(*mockClient, isConnected(_)) .WillRepeatedly(Invoke([connectionStatus, &connectionStatusMutex](string uri) { lock_guard<mutex> lock(connectionStatusMutex); @@ -147,7 +175,7 @@ TEST_F(NostrServiceTest, OpenRelayConnections_OpensConnections_ToProvidedRelays) return status; })); - auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); + auto nostrService = make_unique<nostr::NostrService>(testAppender, mockClient, fakeSigner, defaultTestRelays); nostrService->openRelayConnections(testRelays); auto activeRelays = nostrService->activeRelays(); @@ -168,11 +196,11 @@ TEST_F(NostrServiceTest, OpenRelayConnections_AddsOpenConnections_ToActiveRelays connectionStatus->insert({ defaultTestRelays[1], false }); connectionStatus->insert({ testRelays[0], false }); - EXPECT_CALL(*testClient, openConnection(defaultTestRelays[0])).Times(1); - EXPECT_CALL(*testClient, openConnection(defaultTestRelays[1])).Times(1); - EXPECT_CALL(*testClient, openConnection(testRelays[0])).Times(1); + EXPECT_CALL(*mockClient, openConnection(defaultTestRelays[0])).Times(1); + EXPECT_CALL(*mockClient, openConnection(defaultTestRelays[1])).Times(1); + EXPECT_CALL(*mockClient, openConnection(testRelays[0])).Times(1); - EXPECT_CALL(*testClient, isConnected(_)) + EXPECT_CALL(*mockClient, isConnected(_)) .WillRepeatedly(Invoke([connectionStatus, &connectionStatusMutex](string uri) { lock_guard<mutex> lock(connectionStatusMutex); @@ -184,7 +212,7 @@ TEST_F(NostrServiceTest, OpenRelayConnections_AddsOpenConnections_ToActiveRelays return status; })); - auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); + auto nostrService = make_unique<nostr::NostrService>(testAppender, mockClient, fakeSigner, defaultTestRelays); nostrService->openRelayConnections(); auto activeRelays = nostrService->activeRelays(); @@ -215,7 +243,7 @@ TEST_F(NostrServiceTest, CloseRelayConnections_ClosesConnections_ToActiveRelays) connectionStatus->insert({ defaultTestRelays[0], false }); connectionStatus->insert({ defaultTestRelays[1], false }); - EXPECT_CALL(*testClient, isConnected(_)) + EXPECT_CALL(*mockClient, isConnected(_)) .WillRepeatedly(Invoke([connectionStatus, &connectionStatusMutex](string uri) { lock_guard<mutex> lock(connectionStatusMutex); @@ -227,11 +255,11 @@ TEST_F(NostrServiceTest, CloseRelayConnections_ClosesConnections_ToActiveRelays) return status; })); - auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); + auto nostrService = make_unique<nostr::NostrService>(testAppender, mockClient, fakeSigner, defaultTestRelays); nostrService->openRelayConnections(); - EXPECT_CALL(*testClient, closeConnection(defaultTestRelays[0])).Times(1); - EXPECT_CALL(*testClient, closeConnection(defaultTestRelays[1])).Times(1); + EXPECT_CALL(*mockClient, closeConnection(defaultTestRelays[0])).Times(1); + EXPECT_CALL(*mockClient, closeConnection(defaultTestRelays[1])).Times(1); nostrService->closeRelayConnections(); @@ -250,7 +278,7 @@ TEST_F(NostrServiceTest, CloseRelayConnections_RemovesClosedConnections_FromActi connectionStatus->insert({ defaultTestRelays[1], false }); connectionStatus->insert({ testRelays[0], false }); - EXPECT_CALL(*testClient, isConnected(_)) + EXPECT_CALL(*mockClient, isConnected(_)) .WillRepeatedly(Invoke([connectionStatus, &connectionStatusMutex](string uri) { lock_guard<mutex> lock(connectionStatusMutex); @@ -262,10 +290,10 @@ TEST_F(NostrServiceTest, CloseRelayConnections_RemovesClosedConnections_FromActi return status; })); - auto nostrService = new nostr::NostrService(testAppender, testClient, allTestRelays); + auto nostrService = make_unique<nostr::NostrService>(testAppender, mockClient, fakeSigner, allTestRelays); nostrService->openRelayConnections(); - EXPECT_CALL(*testClient, closeConnection(testRelays[0])).Times(1); + EXPECT_CALL(*mockClient, closeConnection(testRelays[0])).Times(1); nostrService->closeRelayConnections(testRelays); @@ -288,7 +316,7 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_AllSuccesses) connectionStatus->insert({ defaultTestRelays[0], false }); connectionStatus->insert({ defaultTestRelays[1], false }); - EXPECT_CALL(*testClient, isConnected(_)) + EXPECT_CALL(*mockClient, isConnected(_)) .WillRepeatedly(Invoke([connectionStatus, &connectionStatusMutex](string uri) { lock_guard<mutex> lock(connectionStatusMutex); @@ -300,17 +328,18 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_AllSuccesses) return status; })); - auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); + auto nostrService = make_unique<nostr::NostrService>(testAppender, mockClient, fakeSigner, defaultTestRelays); nostrService->openRelayConnections(); - EXPECT_CALL(*testClient, send(_, _)) + EXPECT_CALL(*mockClient, send(_, _)) .Times(2) .WillRepeatedly(Invoke([](string message, string uri) { return make_tuple(uri, true); })); - auto [successes, failures] = nostrService->publishEvent(nostr::Event()); + auto testEvent = make_shared<nostr::Event>(getTestEvent()); + auto [successes, failures] = nostrService->publishEvent(testEvent); ASSERT_EQ(successes.size(), defaultTestRelays.size()); for (auto relay : successes) @@ -328,7 +357,7 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_AllFailures) connectionStatus->insert({ defaultTestRelays[0], false }); connectionStatus->insert({ defaultTestRelays[1], false }); - EXPECT_CALL(*testClient, isConnected(_)) + EXPECT_CALL(*mockClient, isConnected(_)) .WillRepeatedly(Invoke([connectionStatus, &connectionStatusMutex](string uri) { lock_guard<mutex> lock(connectionStatusMutex); @@ -340,17 +369,18 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_AllFailures) return status; })); - auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); + auto nostrService = make_unique<nostr::NostrService>(testAppender, mockClient, fakeSigner, defaultTestRelays); nostrService->openRelayConnections(); - EXPECT_CALL(*testClient, send(_, _)) + EXPECT_CALL(*mockClient, send(_, _)) .Times(2) .WillRepeatedly(Invoke([](string message, string uri) { return make_tuple(uri, false); })); - auto [successes, failures] = nostrService->publishEvent(nostr::Event()); + auto testEvent = make_shared<nostr::Event>(getTestEvent()); + auto [successes, failures] = nostrService->publishEvent(testEvent); ASSERT_EQ(successes.size(), 0); @@ -368,7 +398,7 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_MixedSuccessesAndFailur connectionStatus->insert({ defaultTestRelays[0], false }); connectionStatus->insert({ defaultTestRelays[1], false }); - EXPECT_CALL(*testClient, isConnected(_)) + EXPECT_CALL(*mockClient, isConnected(_)) .WillRepeatedly(Invoke([connectionStatus, &connectionStatusMutex](string uri) { lock_guard<mutex> lock(connectionStatusMutex); @@ -380,23 +410,24 @@ TEST_F(NostrServiceTest, PublishEvent_CorrectlyIndicates_MixedSuccessesAndFailur return status; })); - auto nostrService = new nostr::NostrService(testAppender, testClient, defaultTestRelays); + auto nostrService = make_unique<nostr::NostrService>(testAppender, mockClient, fakeSigner, defaultTestRelays); nostrService->openRelayConnections(); - EXPECT_CALL(*testClient, send(_, defaultTestRelays[0])) + EXPECT_CALL(*mockClient, send(_, defaultTestRelays[0])) .Times(1) .WillRepeatedly(Invoke([](string message, string uri) { return make_tuple(uri, true); })); - EXPECT_CALL(*testClient, send(_, defaultTestRelays[1])) + EXPECT_CALL(*mockClient, send(_, defaultTestRelays[1])) .Times(1) .WillRepeatedly(Invoke([](string message, string uri) { return make_tuple(uri, false); })); - auto [successes, failures] = nostrService->publishEvent(nostr::Event()); + auto testEvent = make_shared<nostr::Event>(getTestEvent()); + auto [successes, failures] = nostrService->publishEvent(testEvent); ASSERT_EQ(successes.size(), 1); ASSERT_EQ(successes[0], defaultTestRelays[0]); |