Skip to content

Commit 6b99d3b

Browse files
committed
Initial commit
- Port existing work over from ShadowySupercode/gitrepublic-core#2 - Adjust project layout and configure CMake scripts accordingly
1 parent d02431d commit 6b99d3b

File tree

8 files changed

+1053
-0
lines changed

8 files changed

+1053
-0
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,9 @@
3030
*.exe
3131
*.out
3232
*.app
33+
34+
# Outputs
35+
build/
36+
37+
# VS Code Settings
38+
.vscode/

CMakeLists.txt

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
cmake_minimum_required(VERSION 3.14)
2+
project(NostrSDK VERSION 0.0.1)
3+
4+
# Specify the C++ standard
5+
set(CMAKE_CXX_STANDARD 17)
6+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
7+
8+
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
9+
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
10+
11+
# Build the project.
12+
set(INCLUDE_DIR ./include)
13+
set(CLIENT_INCLUDE_DIR ./include/client)
14+
include_directories(${INCLUDE_DIR})
15+
include_directories(${CLIENT_INCLUDE_DIR})
16+
set(HEADERS
17+
${INCLUDE_DIR}/nostr.hpp
18+
${CLIENT_INCLUDE_DIR}/web_socket_client.hpp
19+
)
20+
21+
set(SOURCE_DIR ./src)
22+
set(CLIENT_SOURCE_DIR ./src/client)
23+
set(SOURCES
24+
${SOURCE_DIR}/event.cpp
25+
${SOURCE_DIR}/nostr_service.cpp
26+
${CLIENT_SOURCE_DIR}/websocketpp_client.cpp
27+
)
28+
29+
find_package(Boost REQUIRED COMPONENTS random system)
30+
find_package(nlohmann_json CONFIG REQUIRED)
31+
find_package(OpenSSL REQUIRED)
32+
find_package(plog CONFIG REQUIRED)
33+
find_package(websocketpp CONFIG REQUIRED)
34+
35+
add_library(NostrSDK ${SOURCES} ${HEADERS})
36+
target_link_libraries(NostrSDK PRIVATE
37+
Boost::random
38+
Boost::system
39+
nlohmann_json::nlohmann_json
40+
OpenSSL::SSL
41+
OpenSSL::Crypto
42+
plog::plog
43+
websocketpp::websocketpp
44+
)
45+
set_target_properties(NostrSDK PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS YES)
46+
47+
# Build the tests.
48+
enable_testing()
49+
include(GoogleTest)
50+
51+
include(FetchContent)
52+
FetchContent_Declare(
53+
googletest
54+
URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
55+
)
56+
57+
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
58+
FetchContent_MakeAvailable(googletest)
59+
60+
enable_testing()
61+
62+
set(TEST_DIR ./test)
63+
set(TEST_SOURCES
64+
${TEST_DIR}/nostr_service_test.cpp
65+
)
66+
67+
add_executable(NostrSDKTest ${TEST_SOURCES} ${HEADERS})
68+
target_link_libraries(NostrSDKTest PRIVATE
69+
GTest::gmock
70+
GTest::gtest
71+
GTest::gtest_main
72+
NostrSDK
73+
plog::plog
74+
websocketpp::websocketpp
75+
)
76+
set_target_properties(NostrSDKTest PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS YES)
77+
78+
gtest_add_tests(TARGET NostrSDKTest)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#pragma once
2+
3+
#include <string>
4+
5+
namespace client
6+
{
7+
/**
8+
* @brief An interface for a WebSocket client singleton.
9+
*/
10+
class IWebSocketClient
11+
{
12+
public:
13+
/**
14+
* @brief Starts the client.
15+
* @remark This method must be called before any other client methods.
16+
*/
17+
virtual void start() = 0;
18+
19+
/**
20+
* @brief Stops the client.
21+
* @remark This method should be called when the client is no longer needed, before it is
22+
* destroyed.
23+
*/
24+
virtual void stop() = 0;
25+
26+
/**
27+
* @brief Opens a connection to the given server.
28+
*/
29+
virtual void openConnection(std::string uri) = 0;
30+
31+
/**
32+
* @brief Indicates whether the client is connected to the given server.
33+
* @returns True if the client is connected, false otherwise.
34+
*/
35+
virtual bool isConnected(std::string uri) = 0;
36+
37+
/**
38+
* @brief Sends the given message to the given server.
39+
* @returns A tuple indicating the server URI and whether the message was successfully
40+
* sent.
41+
*/
42+
virtual std::tuple<std::string, bool> send(std::string message, std::string uri) = 0;
43+
44+
/**
45+
* @brief Closes the connection to the given server.
46+
*/
47+
virtual void closeConnection(std::string uri) = 0;
48+
};
49+
} // namespace client

include/nostr.hpp

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#pragma once
2+
3+
#include <mutex>
4+
#include <string>
5+
#include <tuple>
6+
#include <vector>
7+
8+
#include <nlohmann/json.hpp>
9+
#include <plog/Log.h>
10+
#include <websocketpp/client.hpp>
11+
#include <websocketpp/config/asio_client.hpp>
12+
13+
#include "client/web_socket_client.hpp"
14+
15+
namespace nostr
16+
{
17+
typedef std::vector<std::string> RelayList;
18+
19+
// TODO: Add null checking to seralization and deserialization methods.
20+
/**
21+
* @brief A Nostr event.
22+
* @remark All data transmitted over the Nostr protocol is encoded in JSON blobs. This struct
23+
* is common to every Nostr event kind. The significance of each event is determined by the
24+
* `tags` and `content` fields.
25+
*/
26+
struct Event
27+
{
28+
std::string id; ///< SHA-256 hash of the event data.
29+
std::string pubkey; ///< Public key of the event creator.
30+
std::string created_at; ///< Unix timestamp of the event creation.
31+
int kind; ///< Event kind.
32+
std::vector<std::vector<std::string>> tags; ///< Arbitrary event metadata.
33+
std::string content; ///< Event content.
34+
std::string sig; ///< Event signature created with the private key of the event creator.
35+
36+
nlohmann::json serialize() const;
37+
void deserialize(std::string jsonString);
38+
};
39+
40+
class NostrService
41+
{
42+
public:
43+
NostrService(plog::IAppender* appender, client::IWebSocketClient* client);
44+
NostrService(plog::IAppender* appender, client::IWebSocketClient* client, RelayList relays);
45+
~NostrService();
46+
47+
RelayList defaultRelays() const;
48+
49+
RelayList activeRelays() const;
50+
51+
/**
52+
* @brief Opens connections to the default Nostr relays of the instance, as specified in
53+
* the constructor.
54+
* @return A list of the relay URLs to which connections were successfully opened.
55+
*/
56+
RelayList openRelayConnections();
57+
58+
/**
59+
* @brief Opens connections to the specified Nostr relays.
60+
* @returns A list of the relay URLs to which connections were successfully opened.
61+
*/
62+
RelayList openRelayConnections(RelayList relays);
63+
64+
/**
65+
* @brief Closes all open relay connections.
66+
*/
67+
void closeRelayConnections();
68+
69+
/**
70+
* @brief Closes any open connections to the specified Nostr relays.
71+
*/
72+
void closeRelayConnections(RelayList relays);
73+
74+
/**
75+
* @brief Publishes a Nostr event to all open relay connections.
76+
* @returns A tuple of `RelayList` objects, of the form `<successes, failures>`, indicating
77+
* to which relays the event was published successfully, and to which relays the event failed
78+
* to publish.
79+
*/
80+
std::tuple<RelayList, RelayList> publishEvent(Event event);
81+
82+
// TODO: Add methods for reading events from relays.
83+
84+
private:
85+
std::mutex _propertyMutex;
86+
RelayList _defaultRelays;
87+
RelayList _activeRelays;
88+
client::IWebSocketClient* _client;
89+
90+
/**
91+
* @brief Determines which of the given relays are currently connected.
92+
* @returns A list of the URIs of currently-open relay connections from the given list.
93+
*/
94+
RelayList getConnectedRelays(RelayList relays);
95+
96+
/**
97+
* @brief Determines which of the given relays are not currently connected.
98+
* @returns A list of the URIs of currently-unconnected relays from the given list.
99+
*/
100+
RelayList getUnconnectedRelays(RelayList relays);
101+
102+
/**
103+
* @brief Determines whether the given relay is currently connected.
104+
* @returns True if the relay is connected, false otherwise.
105+
*/
106+
bool isConnected(std::string relay);
107+
108+
/**
109+
* @brief Removes the given relay from the instance's list of active relays.
110+
*/
111+
void eraseActiveRelay(std::string relay);
112+
113+
/**
114+
* @brief Opens a connection from the client to the given relay.
115+
*/
116+
void connect(std::string relay);
117+
118+
/**
119+
* @brief Closes the connection from the client to the given relay.
120+
*/
121+
void disconnect(std::string relay);
122+
};
123+
} // namespace nostr

src/client/websocketpp_client.cpp

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#include <websocketpp/client.hpp>
2+
#include <websocketpp/config/asio_client.hpp>
3+
4+
#include "web_socket_client.hpp"
5+
6+
using std::error_code;
7+
using std::lock_guard;
8+
using std::make_tuple;
9+
using std::mutex;
10+
using std::string;
11+
using std::tuple;
12+
using std::unordered_map;
13+
14+
namespace client
15+
{
16+
/**
17+
* @brief An implementation of the `IWebSocketClient` interface that uses the WebSocket++ library.
18+
*/
19+
class WebsocketppClient : public IWebSocketClient
20+
{
21+
public:
22+
void start() override
23+
{
24+
this->_client.init_asio();
25+
this->_client.start_perpetual();
26+
};
27+
28+
void stop() override
29+
{
30+
this->_client.stop_perpetual();
31+
this->_client.stop();
32+
};
33+
34+
void openConnection(string uri) override
35+
{
36+
error_code error;
37+
websocketpp_client::connection_ptr connection = this->_client.get_connection(uri, error);
38+
39+
if (error.value() == -1)
40+
{
41+
// PLOG_ERROR << "Error connecting to relay " << relay << ": " << error.message();
42+
}
43+
44+
// Configure the connection here via the connection pointer.
45+
connection->set_fail_handler([this, uri](auto handle) {
46+
// PLOG_ERROR << "Error connecting to relay " << relay << ": Handshake failed.";
47+
lock_guard<mutex> lock(this->_propertyMutex);
48+
if (this->isConnected(uri))
49+
{
50+
this->_connectionHandles.erase(uri);
51+
}
52+
});
53+
54+
lock_guard<mutex> lock(this->_propertyMutex);
55+
this->_connectionHandles[uri] = connection->get_handle();
56+
this->_client.connect(connection);
57+
};
58+
59+
bool isConnected(string uri) override
60+
{
61+
lock_guard<mutex> lock(this->_propertyMutex);
62+
return this->_connectionHandles.find(uri) != this->_connectionHandles.end();
63+
};
64+
65+
tuple<string, bool> send(string message, string uri) override
66+
{
67+
error_code error;
68+
69+
// Make sure the connection isn't closed from under us.
70+
lock_guard<mutex> lock(this->_propertyMutex);
71+
this->_client.send(
72+
this->_connectionHandles[uri],
73+
message,
74+
websocketpp::frame::opcode::text,
75+
error);
76+
77+
if (error.value() == -1)
78+
{
79+
// PLOG_ERROR << "Error publishing event to relay " << relay << ": " << error.message();
80+
return make_tuple(uri, false);
81+
}
82+
83+
return make_tuple(uri, true);
84+
};
85+
86+
void closeConnection(string uri) override
87+
{
88+
lock_guard<mutex> lock(this->_propertyMutex);
89+
90+
websocketpp::connection_hdl handle = this->_connectionHandles[uri];
91+
this->_client.close(
92+
handle,
93+
websocketpp::close::status::going_away,
94+
"_client requested close.");
95+
96+
this->_connectionHandles.erase(uri);
97+
};
98+
99+
private:
100+
typedef websocketpp::client<websocketpp::config::asio_client> websocketpp_client;
101+
typedef unordered_map<string, websocketpp::connection_hdl>::iterator connection_hdl_iterator;
102+
103+
websocketpp_client _client;
104+
unordered_map<string, websocketpp::connection_hdl> _connectionHandles;
105+
mutex _propertyMutex;
106+
};
107+
} // namespace client

0 commit comments

Comments
 (0)