diff --git a/nostr-java-api/pom.xml b/nostr-java-api/pom.xml index 3c3ab98db..c324dde9f 100644 --- a/nostr-java-api/pom.xml +++ b/nostr-java-api/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric nostr-java - 0.6.3-SNAPSHOT + 0.6.4-SNAPSHOT nostr-java-api jar @@ -45,9 +45,9 @@ ${project.version} - ${project.groupId} - nostr-java-command-provider - ${project.version} + org.apache.commons + commons-lang3 + ${commons-lang3.version} diff --git a/nostr-java-api/src/main/java/module-info.java b/nostr-java-api/src/main/java/module-info.java index e7b5625f1..3290a781e 100644 --- a/nostr-java-api/src/main/java/module-info.java +++ b/nostr-java-api/src/main/java/module-info.java @@ -5,16 +5,16 @@ requires nostr.id; requires nostr.client; requires nostr.context; - requires nostr.context.impl; requires nostr.encryption; requires nostr.encryption.nip04dm; requires nostr.encryption.nip44dm; - + requires com.fasterxml.jackson.databind; - + requires lombok; requires java.logging; requires nostr.crypto; + requires org.apache.commons.lang3; exports nostr.api; } diff --git a/nostr-java-api/src/main/java/nostr/api/EventNostr.java b/nostr-java-api/src/main/java/nostr/api/EventNostr.java index 53d91db10..af3310462 100644 --- a/nostr-java-api/src/main/java/nostr/api/EventNostr.java +++ b/nostr-java-api/src/main/java/nostr/api/EventNostr.java @@ -10,15 +10,16 @@ import lombok.Setter; import nostr.api.factory.impl.GenericEventFactory; import nostr.base.PublicKey; +import nostr.event.BaseMessage; import nostr.event.BaseTag; import nostr.event.impl.GenericEvent; import nostr.event.json.codec.BaseMessageDecoder; import nostr.id.Identity; +import org.apache.commons.lang3.stream.Streams.FailableStream; import java.util.List; import java.util.Map; - -import nostr.event.BaseMessage; +import java.util.Objects; /** * @author guilhermegps @@ -46,14 +47,14 @@ public U send() { return this.send(getRelays()); } - @SuppressWarnings("unchecked") public U send(Map relays) { - List messages = super.send(this.event, relays); - BaseMessageDecoder decoder = new BaseMessageDecoder(); + List messages = super.sendEvent(this.event, relays); + BaseMessageDecoder decoder = new BaseMessageDecoder<>(); - return messages.stream() + return new FailableStream<>(messages.stream()) .map(msg -> (U) decoder.decode(msg)) - .filter(msg -> msg != null) + .filter(Objects::nonNull) + .stream() .findFirst() .orElseThrow(() -> new RuntimeException("No message received")); } diff --git a/nostr-java-api/src/main/java/nostr/api/NIP01.java b/nostr-java-api/src/main/java/nostr/api/NIP01.java index c91ccf304..16591fe8f 100644 --- a/nostr-java-api/src/main/java/nostr/api/NIP01.java +++ b/nostr-java-api/src/main/java/nostr/api/NIP01.java @@ -19,17 +19,14 @@ import nostr.api.factory.impl.NIP01Impl.ReplaceableEventFactory; import nostr.api.factory.impl.NIP01Impl.ReqMessageFactory; import nostr.api.factory.impl.NIP01Impl.TextNoteEventFactory; -import nostr.base.GenericTagQuery; import nostr.base.IEvent; import nostr.base.PublicKey; import nostr.base.Relay; import nostr.base.UserProfile; import nostr.event.BaseTag; -import nostr.event.Kind; import nostr.event.Marker; import nostr.event.NIP01Event; -import nostr.event.impl.Filters; -import nostr.event.impl.GenericEvent; +import nostr.event.filter.Filters; import nostr.event.message.CloseMessage; import nostr.event.message.EoseMessage; import nostr.event.message.EventMessage; @@ -42,7 +39,6 @@ import nostr.id.Identity; import java.util.List; -import java.util.Map; /** * @@ -98,7 +94,7 @@ public NIP01 createMetadataEvent(@NonNull UserProfile profile) { /** * Create a replaceable event - * + * * @param kind the kind (10000 <= kind < 20000 || kind == 0 || kind == 3) * @param content the content */ @@ -111,7 +107,7 @@ public NIP01 createReplaceableEvent(@NonNull Integer kind, String content) { /** * Create a replaceable event - * + * * @param tags the note's tags * @param kind the kind (10000 <= kind < 20000 || kind == 0 || kind == 3) * @param content the note's content @@ -125,7 +121,7 @@ public NIP01 createReplaceableEvent(@NonNull List tags, @NonNull Int /** * Create an ephemeral event - * + * * @param kind the kind (20000 <= n < 30000) * @param content the note's content */ @@ -190,48 +186,6 @@ public static PubKeyTag createPubKeyTag(@NonNull PublicKey publicKey, String mai return result; } - /** - * Create a NIP01 filters object (all parameters are optional) - * - * @param events a list of event - * @param authors a list of pubkeys or prefixes, the pubkey of an event - * must - * be one of these - * @param kinds a list of a kind numbers - * @param referencedEvents a list of event ids that are referenced in an "e" - * tag - * @param referencePubKeys a list of pubkeys that are referenced in a "p" - * tag - * @param since an integer unix timestamp in seconds, events must be - * newer - * than this to pass - * @param until an integer unix timestamp in seconds, events must be - * older - * than this to pass - * @param limit maximum number of events to be returned in the - * initial query - * @param genericTagQuery a generic tag query - * @return a filters object - */ - @Deprecated(forRemoval = true) - public static Filters createFilters(List events, List authors, List kinds, - List referencedEvents, List referencePubKeys, Long since, Long until, - Integer limit, GenericTagQuery genericTagQuery) { - return Filters.builder() - .authors(authors) - .events(events) - .genericTagQuery( - Map.of( - genericTagQuery.getTagName(), - genericTagQuery.getValue())) - .kinds(kinds).limit(limit) - .referencePubKeys(referencePubKeys) - .referencedEvents(referencedEvents) - .since(since) - .until(until) - .build(); - } - /** * Create an event message to send events requested by clients * diff --git a/nostr-java-api/src/main/java/nostr/api/NIP52.java b/nostr-java-api/src/main/java/nostr/api/NIP52.java index 8189316a4..fc2e6e0e5 100644 --- a/nostr-java-api/src/main/java/nostr/api/NIP52.java +++ b/nostr-java-api/src/main/java/nostr/api/NIP52.java @@ -1,21 +1,28 @@ package nostr.api; import lombok.NonNull; +import nostr.api.factory.impl.NIP52Impl.CalendarRsvpEventFactory; import nostr.api.factory.impl.NIP52Impl.CalendarTimeBasedEventFactory; import nostr.event.BaseTag; import nostr.event.NIP52Event; import nostr.event.impl.CalendarContent; +import nostr.event.impl.CalendarRsvpContent; import nostr.id.Identity; import java.util.List; public class NIP52 extends EventNostr { - public NIP52(@NonNull Identity sender) { - setSender(sender); - } + public NIP52(@NonNull Identity sender) { + setSender(sender); + } - public NIP52 createCalendarTimeBasedEvent(@NonNull List baseTags, @NonNull String content, @NonNull CalendarContent calendarContent) { - setEvent((T) new CalendarTimeBasedEventFactory(getSender(), baseTags, content, calendarContent).create()); - return this; - } + public NIP52 createCalendarTimeBasedEvent(@NonNull List baseTags, @NonNull String content, @NonNull CalendarContent calendarContent) { + setEvent((T) new CalendarTimeBasedEventFactory(getSender(), baseTags, content, calendarContent).create()); + return this; + } + + public NIP52 createCalendarRsvpEvent(@NonNull List baseTags, @NonNull String content, @NonNull CalendarRsvpContent calendarRsvpContent) { + setEvent((T) new CalendarRsvpEventFactory(getSender(), baseTags, content, calendarRsvpContent).create()); + return this; + } } diff --git a/nostr-java-api/src/main/java/nostr/api/Nostr.java b/nostr-java-api/src/main/java/nostr/api/Nostr.java deleted file mode 100644 index aaf4b488d..000000000 --- a/nostr-java-api/src/main/java/nostr/api/Nostr.java +++ /dev/null @@ -1,334 +0,0 @@ -/* - * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license - * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template - */ -package nostr.api; - -import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.GenericTagQuery; -import nostr.base.IElement; -import nostr.base.IEvent; -import nostr.base.ISignable; -import nostr.base.Relay; -import nostr.client.Client; -import nostr.context.RequestContext; -import nostr.context.impl.DefaultRequestContext; -import nostr.crypto.schnorr.Schnorr; -import nostr.event.BaseEvent; -import nostr.event.BaseMessage; -import nostr.event.BaseTag; -import nostr.event.impl.Filters; -import nostr.event.impl.GenericEvent; -import nostr.event.json.codec.BaseEventEncoder; -import nostr.event.json.codec.BaseMessageDecoder; -import nostr.event.json.codec.BaseTagDecoder; -import nostr.event.json.codec.BaseTagEncoder; -import nostr.event.json.codec.FiltersDecoder; -import nostr.event.json.codec.FiltersListEncoder; -import nostr.event.json.codec.GenericEventDecoder; -import nostr.event.json.codec.GenericTagQueryEncoder; -import nostr.event.message.EventMessage; -import nostr.event.message.ReqMessage; -import nostr.id.Identity; -import nostr.util.NostrUtil; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeoutException; - -/** - * @author eric - */ -@NoArgsConstructor -public class Nostr implements NostrIF { - - private static Nostr INSTANCE; - - private Client client; - @Getter - private Identity sender; - - @Getter - private Map relays; - - public static NostrIF getInstance() { - return (INSTANCE == null) ? new Nostr() : INSTANCE; - } - - public static NostrIF getInstance(@NonNull Identity sender) { - return (INSTANCE == null) ? new Nostr(sender) : INSTANCE; - } - - public Nostr(@NonNull Identity sender) { - this.sender = sender; - } - - @Override - public NostrIF setSender(@NonNull Identity sender) { - this.sender = sender; - - return this; - } - - @Override - public NostrIF setRelays(@NonNull Map relays) { - this.relays = relays; - - return this; - } - - @Override - public void close() { - if (client == null) { - throw new IllegalStateException("Client is not initialized"); - } - try { - this.client.disconnect(); - } catch (TimeoutException e) { - throw new RuntimeException(e); - } - } - - @Override - public List send(@NonNull IEvent event) { - return send(event, getRelays()); - } - - @Override - public List send(@NonNull IEvent event, Map relays) { - var context = new DefaultRequestContext(); - context.setPrivateKey(getSender().getPrivateKey().getRawData()); - context.setRelays(relays); - - return send(new EventMessage(event), context); - } - - - @Override - public List send(@NonNull Filters filters, @NonNull String subscriptionId) { - return send(filters, subscriptionId, getRelays()); - } - - @Override - public List send(@NonNull Filters filters, @NonNull String subscriptionId, Map relays) { - List filtersList = new ArrayList<>(); - filtersList.add(filters); - - return send(filtersList, subscriptionId, relays); - } - - @Override - public List send(@NonNull List filtersList, @NonNull String subscriptionId) { - return send(filtersList, subscriptionId, getRelays()); - } - - @Override - public List send(@NonNull List filtersList, @NonNull String subscriptionId, Map relays) { - - var context = new DefaultRequestContext(); - context.setRelays(relays); - context.setPrivateKey(getSender().getPrivateKey().getRawData()); - var message = new ReqMessage(subscriptionId, filtersList); - - return send(message, context); - } - - @Override - public List send(@NonNull BaseMessage message, @NonNull RequestContext context) { - if (context instanceof DefaultRequestContext) { - try { - Client.getInstance().connect(context).send(message); - } catch (TimeoutException e) { - throw new RuntimeException(e); - } - } - return List.of(); - } - - /** - * @param signable - */ - @Override - public NostrIF sign(@NonNull Identity identity, @NonNull ISignable signable) { - identity.sign(signable); - - return this; - } - - @Override - public boolean verify(@NonNull GenericEvent event) { - if (!event.isSigned()) { - throw new IllegalStateException("The event is not signed"); - } - - var signature = event.getSignature(); - - try { - var message = NostrUtil.sha256(event.get_serializedEvent()); - return Schnorr.verify(message, event.getPubKey().getRawData(), signature.getRawData()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private static Map toMapRelays(List relayList) { - Map relays = new HashMap<>(); - relayList.forEach(r -> relays.put(r.getName(), r.getUri())); - return relays; - } - - public static class Json { - - // Events - - /** - * @param event - */ - public static String encode(@NonNull BaseEvent event) { - return Nostr.Json.encode(event, null); - } - - /** - * @param event - * @param relay - * @return - */ - public static String encode(@NonNull BaseEvent event, Relay relay) { - final var enc = new BaseEventEncoder(event); - return enc.encode(); - } - - /** - * @param json - */ - public static GenericEvent decodeEvent(@NonNull String json) { - return new GenericEventDecoder<>().decode(json); - } - - // Messages - - /** - * @param message - * @param relay - */ - public static String encode(@NonNull BaseMessage message, Relay relay) throws JsonProcessingException { - return message.encode(); - } - - /** - * @param message - */ - public static String encode(@NonNull BaseMessage message) throws JsonProcessingException { - return Nostr.Json.encode(message, null); - } - - /** - * @param json - */ - public static BaseMessage decodeMessage(@NonNull String json) { - return new BaseMessageDecoder().decode(json); - } - - // Tags - - /** - * @param tag - * @param relay - */ - public static String encode(@NonNull BaseTag tag, Relay relay) { - final var enc = new BaseTagEncoder(tag, relay); - return enc.encode(); - } - - /** - * @param tag - */ - public static String encode(@NonNull BaseTag tag) { - return Nostr.Json.encode(tag, null); - } - - /** - * @param json - */ - public static BaseTag decodeTag(@NonNull String json) { - return new BaseTagDecoder<>().decode(json); - } - - // Filters - - /** - * @param filtersList - * @param relay - */ - public static String encode(@NonNull List filtersList, Relay relay) { - final var enc = new FiltersListEncoder(filtersList); - return enc.encode(); - } - - /** - * @param filtersList - */ - public static String encode(@NonNull List filtersList) { - return Nostr.Json.encode(filtersList, null); - } - - /** - * @param json - */ - public static Filters decodeFilters(@NonNull String json) { - return new FiltersDecoder<>().decode(json); - } - - // Generic Tag Queries - - /** - * @param gtq - * @param relay - */ - public static String encode(@NonNull GenericTagQuery gtq, Relay relay) { - final var enc = new GenericTagQueryEncoder(gtq, relay); - return enc.encode(); - } - - /** - * @param gtq - */ - public static String encode(@NonNull GenericTagQuery gtq) { - return Nostr.Json.encode(gtq, null); - } - - /** - * @param json - * @param clazz - */ - public static IElement decode(@NonNull String json, @NonNull Class clazz) { - switch (clazz.getName()) { - case "nostr.event.BaseEvent.class" -> { - return decodeEvent(json); - } - case "nostr.event.BaseMessage.class" -> { - return decodeMessage(json); - } - case "nostr.event.BaseTag.class" -> { - return decodeTag(json); - } - default -> throw new AssertionError(); - } - } - - public static Filters decodeFilters(@NonNull String json, @NonNull Class clazz) { - switch (clazz.getName()) { - case "nostr.event.Filters.class" -> { - return decodeFilters(json); - } - default -> throw new AssertionError(); - } - } - - } -} diff --git a/nostr-java-api/src/main/java/nostr/api/NostrIF.java b/nostr-java-api/src/main/java/nostr/api/NostrIF.java index 0d920f7c9..aecd93a6a 100644 --- a/nostr-java-api/src/main/java/nostr/api/NostrIF.java +++ b/nostr-java-api/src/main/java/nostr/api/NostrIF.java @@ -5,7 +5,7 @@ import nostr.base.ISignable; import nostr.context.RequestContext; import nostr.event.BaseMessage; -import nostr.event.impl.Filters; +import nostr.event.filter.Filters; import nostr.event.impl.GenericEvent; import nostr.id.Identity; @@ -16,13 +16,13 @@ public interface NostrIF { NostrIF setSender(@NonNull Identity sender); NostrIF setRelays(@NonNull Map relays); - List send(@NonNull IEvent event); - List send(@NonNull IEvent event, Map relays); - List send(@NonNull Filters filters, @NonNull String subscriptionId); - List send(@NonNull Filters filters, @NonNull String subscriptionId, Map relays); - List send(@NonNull List filtersList, @NonNull String subscriptionId); - List send(@NonNull List filtersList, @NonNull String subscriptionId, Map relays); - List send(@NonNull BaseMessage message, @NonNull RequestContext context); + List sendEvent(@NonNull IEvent event); + List sendEvent(@NonNull IEvent event, Map relays); + List sendRequest(@NonNull Filters filters, @NonNull String subscriptionId); + List sendRequest(@NonNull Filters filters, @NonNull String subscriptionId, Map relays); + List sendRequest(@NonNull List filtersList, @NonNull String subscriptionId); + List sendRequest(@NonNull List filtersList, @NonNull String subscriptionId, Map relays); + List sendRequest(@NonNull BaseMessage message, @NonNull RequestContext context); NostrIF sign(@NonNull Identity identity, @NonNull ISignable signable); boolean verify(@NonNull GenericEvent event); Identity getSender(); diff --git a/nostr-java-api/src/main/java/nostr/api/NostrSpringWebSocketClient.java b/nostr-java-api/src/main/java/nostr/api/NostrSpringWebSocketClient.java index 5389e644d..a2955c1d3 100644 --- a/nostr-java-api/src/main/java/nostr/api/NostrSpringWebSocketClient.java +++ b/nostr-java-api/src/main/java/nostr/api/NostrSpringWebSocketClient.java @@ -1,45 +1,36 @@ package nostr.api; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; -import lombok.SneakyThrows; import nostr.base.IEvent; import nostr.base.ISignable; -import nostr.client.springwebsocket.SpringWebSocketClient; import nostr.context.RequestContext; import nostr.crypto.schnorr.Schnorr; import nostr.event.BaseMessage; -import nostr.event.impl.Filters; +import nostr.event.filter.Filters; import nostr.event.impl.GenericEvent; -import nostr.event.message.EventMessage; -import nostr.event.message.ReqMessage; import nostr.id.Identity; import nostr.util.NostrUtil; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + @NoArgsConstructor public class NostrSpringWebSocketClient implements NostrIF { - - private static NostrSpringWebSocketClient INSTANCE; - - private List clients = new ArrayList<>(); - + private final Map clientMap = new ConcurrentHashMap<>(); @Getter private Identity sender; - private NostrSpringWebSocketClient(@NonNull Map relays) { - relays.values().forEach(this::createClientForRelay); - } + private static NostrSpringWebSocketClient INSTANCE; - public NostrSpringWebSocketClient(@NonNull Identity sender) { - this.sender = sender; + public NostrSpringWebSocketClient(String relayName, String relayUri) { + setRelays(Map.of(relayName, relayUri)); } public static NostrIF getInstance() { @@ -50,82 +41,83 @@ public static NostrIF getInstance(@NonNull Identity sender) { return (INSTANCE == null) ? new NostrSpringWebSocketClient(sender) : INSTANCE; } - private void createClientForRelay(String relay) { - clients.add(new SpringWebSocketClient(relay)); + public NostrSpringWebSocketClient(@NonNull Identity sender) { + this.sender = sender; } - @Override public NostrIF setSender(@NonNull Identity sender) { this.sender = sender; return this; } - @Override - public NostrIF setRelays(@NonNull Map relays) { - relays.values().stream() - .filter(relay -> - clients.stream() - .map(SpringWebSocketClient::getRelayUrl) - .noneMatch( - relay::equals)) - .forEach(this::addClient); - return this; - } - - @SneakyThrows - private void addClient(String relay) { - clients.add(new SpringWebSocketClient(relay)); + public List sendEvent(T event, Map relays) { + setRelays(relays); + return relays.keySet().stream().map(s -> + clientMap.get(s).sendEvent(event)) + .flatMap(List::stream) + .distinct().toList(); } @Override - public void close() throws IOException { - for (SpringWebSocketClient client : clients) { - client.closeSocket(); - } + public NostrIF setRelays(@NonNull Map relays) { + relays.entrySet().stream().forEach(relayEntry -> + clientMap.putIfAbsent(relayEntry.getKey(), + new WebSocketClientHandler( + relayEntry.getKey(), + relayEntry.getValue()))); + return this; } @Override - public List send(@NonNull IEvent event) { - EventMessage message = new EventMessage(event); - return clients.stream().flatMap(client -> send(client, message).stream()).toList(); + public List sendEvent(@NonNull IEvent event) { + return clientMap.values().stream().map(client -> + client.sendEvent(event)).flatMap(List::stream).distinct().toList(); } - @Override - public List send(@NonNull IEvent event, Map relays) { + public List sendEvent(@NonNull IEvent event, Map relays) { setRelays(relays); - return send(event); + return sendEvent(event); } @Override - public List send(@NonNull Filters filters, @NonNull String subscriptionId) { - ReqMessage reqMessage = new ReqMessage(subscriptionId, filters); - return clients.stream().flatMap(client -> send(client, reqMessage).stream()).toList(); + public List sendRequest(@NonNull Filters filters, @NonNull String subscriptionId, Map relays) { + return sendRequest(List.of(filters), subscriptionId, relays); } @Override - public List send(@NonNull Filters filters, @NonNull String subscriptionId, Map relays) { + public List sendRequest(@NonNull List filtersList, @NonNull String subscriptionId, Map relays) { setRelays(relays); - return send(filters, subscriptionId); + return sendRequest(filtersList, subscriptionId); } @Override - public List send(@NonNull List filtersList, @NonNull String subscriptionId) { - return filtersList.stream().flatMap(filters -> send(filters, subscriptionId).stream()).toList(); + public List sendRequest(@NonNull List filtersList, @NonNull String subscriptionId) { + return filtersList.stream().map(filters -> sendRequest( + filters, + subscriptionId + )) + .flatMap(List::stream) + .distinct().toList(); } @Override - public List send(@NonNull List filtersList, @NonNull String subscriptionId, Map relays) { - return filtersList.stream().flatMap(filters -> send(filters, subscriptionId, relays).stream()).toList(); - } + public List sendRequest(@NonNull Filters filters, @NonNull String subscriptionId) { + createRequestClient(subscriptionId); - @SneakyThrows - private List send(SpringWebSocketClient client, BaseMessage message) { - return client.send(message); + return clientMap.entrySet().stream().filter(entry -> + entry.getValue().getRelayName().equals(String.join(entry.getKey(), subscriptionId))) + .map(Entry::getValue) + .map(webSocketClientHandler -> + webSocketClientHandler.sendRequest( + filters, + webSocketClientHandler.getRelayName())) + .flatMap(List::stream).toList(); } + @Override - public List send(@NonNull BaseMessage message, @NonNull RequestContext context) { + public List sendRequest(@NonNull BaseMessage message, @NonNull RequestContext context) { return List.of(); } @@ -151,9 +143,35 @@ public boolean verify(@NonNull GenericEvent event) { } } + @Override + public Identity getSender() { + return sender; + } + @Override public Map getRelays() { - return clients.stream() - .collect(Collectors.toMap(SpringWebSocketClient::getRelayUrl, SpringWebSocketClient::getRelayUrl, (prev, next) -> next, HashMap::new)); + return clientMap.values().stream() + .collect(Collectors.toMap(WebSocketClientHandler::getRelayName, WebSocketClientHandler::getRelayUri, + (prev, next) -> next, HashMap::new)); + } + + public void close() throws IOException { + for (WebSocketClientHandler client : clientMap.values()) { + client.close(); + } + } + + private void createRequestClient(String subscriptionId) { + if (clientMap.entrySet().stream() // if a request client doesn't yet exist for subscriptionId... + .noneMatch(entry -> + entry.getValue().getRelayName().equals(String.join(entry.getKey(), subscriptionId)))) { + clientMap.keySet().forEach(clientMapKey -> // ... create one for each relay and add it to the client map + clientMap.entrySet().stream().map(entry -> + new WebSocketClientHandler( + String.join(entry.getKey(), subscriptionId), + entry.getValue().getRelayUri())) + .toList().forEach(webSocketClientHandler -> + clientMap.put(clientMapKey, webSocketClientHandler))); + } } } diff --git a/nostr-java-api/src/main/java/nostr/api/WebSocketClientHandler.java b/nostr-java-api/src/main/java/nostr/api/WebSocketClientHandler.java new file mode 100644 index 000000000..be0954b6f --- /dev/null +++ b/nostr-java-api/src/main/java/nostr/api/WebSocketClientHandler.java @@ -0,0 +1,57 @@ +package nostr.api; + +import lombok.Getter; +import lombok.NonNull; +import nostr.base.IEvent; +import nostr.client.springwebsocket.SpringWebSocketClient; +import nostr.event.filter.Filters; +import nostr.event.message.EventMessage; +import nostr.event.message.ReqMessage; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +public class WebSocketClientHandler { + private final SpringWebSocketClient eventClient; + private final Map requestClientMap = new ConcurrentHashMap<>(); + + @Getter + private String relayName; + @Getter + private String relayUri; + + protected WebSocketClientHandler(@NonNull String relayName, @NonNull String relayUri) { + this.relayName = relayName; + this.relayUri = relayUri; + this.eventClient = new SpringWebSocketClient(relayUri); + } + + protected List sendEvent(@NonNull IEvent event) { + return eventClient.send(new EventMessage(event)).stream().toList(); + } + + protected List sendRequest(@NonNull Filters filters, @NonNull String subscriptionId) { + return Optional + .ofNullable( + requestClientMap.get(subscriptionId)) + .map(client -> + client.send(new ReqMessage(subscriptionId, filters))).or(() -> { + requestClientMap.put(subscriptionId, new SpringWebSocketClient(relayUri)); + return Optional.ofNullable( + requestClientMap.get(subscriptionId).send( + new ReqMessage(subscriptionId, filters))); + }) + .orElse(new ArrayList<>()); + } + + public void close() throws IOException { + eventClient.closeSocket(); + for (SpringWebSocketClient client : requestClientMap.values()) { + client.closeSocket(); + } + } +} diff --git a/nostr-java-api/src/main/java/nostr/api/factory/impl/NIP01Impl.java b/nostr-java-api/src/main/java/nostr/api/factory/impl/NIP01Impl.java index 94e6211fc..98e9a679b 100644 --- a/nostr-java-api/src/main/java/nostr/api/factory/impl/NIP01Impl.java +++ b/nostr-java-api/src/main/java/nostr/api/factory/impl/NIP01Impl.java @@ -17,8 +17,8 @@ import nostr.base.UserProfile; import nostr.event.BaseTag; import nostr.event.Marker; +import nostr.event.filter.Filters; import nostr.event.impl.EphemeralEvent; -import nostr.event.impl.Filters; import nostr.event.impl.MetadataEvent; import nostr.event.impl.ParameterizedReplaceableEvent; import nostr.event.impl.ReplaceableEvent; diff --git a/nostr-java-api/src/main/java/nostr/api/factory/impl/NIP23Impl.java b/nostr-java-api/src/main/java/nostr/api/factory/impl/NIP23Impl.java index 08a5ffc40..2bcd9f642 100644 --- a/nostr-java-api/src/main/java/nostr/api/factory/impl/NIP23Impl.java +++ b/nostr-java-api/src/main/java/nostr/api/factory/impl/NIP23Impl.java @@ -10,6 +10,7 @@ import nostr.api.factory.EventFactory; import nostr.api.factory.TagFactory; import nostr.event.BaseTag; +import nostr.event.Kind; import nostr.event.impl.GenericEvent; import nostr.id.Identity; @@ -22,8 +23,6 @@ */ public class NIP23Impl { - public static final Integer KIND_PRE_LONG_FORM_CONTENT = 30023; - @Data @EqualsAndHashCode(callSuper = false) public static class LongFormContentEventFactory extends EventFactory { @@ -38,7 +37,7 @@ public LongFormContentEventFactory(@NonNull Identity sender, @NonNull List { + private final CalendarRsvpContent calendarRsvpContent; + + public CalendarRsvpEventFactory(@NonNull Identity sender, @NonNull List baseTags, @NonNull String content, @NonNull CalendarRsvpContent calendarRsvpContent) { + super(sender, baseTags, content); + this.calendarRsvpContent = calendarRsvpContent; + } + + @Override + public CalendarRsvpEvent create() { + return new CalendarRsvpEvent(getSender(), getTags(), getContent(), calendarRsvpContent); + } + } } diff --git a/nostr-java-api/src/main/java/nostr/api/factory/impl/NIP60Impl.java b/nostr-java-api/src/main/java/nostr/api/factory/impl/NIP60Impl.java index 54218f53f..38d5b4609 100644 --- a/nostr-java-api/src/main/java/nostr/api/factory/impl/NIP60Impl.java +++ b/nostr-java-api/src/main/java/nostr/api/factory/impl/NIP60Impl.java @@ -5,6 +5,7 @@ import lombok.NonNull; import nostr.api.factory.EventFactory; import nostr.event.BaseTag; +import nostr.event.Kind; import nostr.event.impl.GenericEvent; import nostr.id.Identity; @@ -18,7 +19,7 @@ public WalletEventFactory(@NonNull Identity sender, List tags, @NonNull @Override public GenericEvent create() { - return new GenericEvent(getIdentity().getPublicKey(), 37375, getTags(), getContent()); + return new GenericEvent(getIdentity().getPublicKey(), Kind.WALLET, getTags(), getContent()); } } @@ -30,7 +31,7 @@ public TokenEventFactory(@NonNull Identity sender, List tags, @NonNull @Override public GenericEvent create() { - return new GenericEvent(getIdentity().getPublicKey(), 7375, getTags(), getContent()); + return new GenericEvent(getIdentity().getPublicKey(), Kind.WALLET_UNSPENT_PROOF, getTags(), getContent()); } } @@ -42,7 +43,7 @@ public SpendingHistoryEventFactory(@NonNull Identity sender, List tags, @Override public GenericEvent create() { - return new GenericEvent(getIdentity().getPublicKey(), 7376, getTags(), getContent()); + return new GenericEvent(getIdentity().getPublicKey(), Kind.WALLET_TX_HISTORY, getTags(), getContent()); } } @@ -54,7 +55,7 @@ public RedemptionQuoteEventFactory(@NonNull Identity sender, List tags, @Override public GenericEvent create() { - return new GenericEvent(getIdentity().getPublicKey(), 7374, getTags(), getContent()); + return new GenericEvent(getIdentity().getPublicKey(), Kind.RESERVED_CASHU_WALLET_TOKENS, getTags(), getContent()); } } diff --git a/nostr-java-base/pom.xml b/nostr-java-base/pom.xml index 6ff5f6659..7a1f2e4b6 100644 --- a/nostr-java-base/pom.xml +++ b/nostr-java-base/pom.xml @@ -6,7 +6,7 @@ xyz.tcheeric nostr-java - 0.6.3-SNAPSHOT + 0.6.4-SNAPSHOT nostr-java-base diff --git a/nostr-java-base/src/main/java/nostr/base/GenericTagQuery.java b/nostr-java-base/src/main/java/nostr/base/GenericTagQuery.java index 2b2238806..c11446680 100644 --- a/nostr-java-base/src/main/java/nostr/base/GenericTagQuery.java +++ b/nostr-java-base/src/main/java/nostr/base/GenericTagQuery.java @@ -2,21 +2,23 @@ package nostr.base; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import java.util.List; - /** - * * @author squirrel */ @Data @NoArgsConstructor +@AllArgsConstructor public class GenericTagQuery { - + private String tagName; - private List value; + + @JsonProperty + private String value; @JsonIgnore public Integer getNip() { diff --git a/nostr-java-base/src/main/java/nostr/base/IDecoder.java b/nostr-java-base/src/main/java/nostr/base/IDecoder.java index c1dd430be..f94d95120 100644 --- a/nostr-java-base/src/main/java/nostr/base/IDecoder.java +++ b/nostr-java-base/src/main/java/nostr/base/IDecoder.java @@ -1,5 +1,7 @@ package nostr.base; +import com.fasterxml.jackson.core.JsonProcessingException; + /** * * @author eric @@ -7,6 +9,6 @@ */ public interface IDecoder { - T decode(String str); + T decode(String str) throws JsonProcessingException; } diff --git a/nostr-java-client/pom.xml b/nostr-java-client/pom.xml index 2ad124eea..4aa6500ba 100644 --- a/nostr-java-client/pom.xml +++ b/nostr-java-client/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric nostr-java - 0.6.3-SNAPSHOT + 0.6.4-SNAPSHOT nostr-java-client jar @@ -24,12 +24,6 @@ nostr-java-id ${project.version} - - ${project.groupId} - nostr-java-connection - ${project.version} - compile - org.springframework spring-websocket @@ -64,4 +58,4 @@ 4.2.2 - \ No newline at end of file + diff --git a/nostr-java-client/src/main/java/module-info.java b/nostr-java-client/src/main/java/module-info.java index 75a2fccc2..ff959a1f2 100644 --- a/nostr-java-client/src/main/java/module-info.java +++ b/nostr-java-client/src/main/java/module-info.java @@ -4,9 +4,7 @@ requires java.logging; requires nostr.util; requires nostr.base; - requires nostr.connection; requires nostr.context; - requires nostr.context.impl; requires com.fasterxml.jackson.core; requires reactor.core; requires spring.webflux; @@ -14,9 +12,7 @@ requires spring.beans; requires spring.websocket; requires jakarta.websocket.client; - requires annotations; requires awaitility; - exports nostr.client; exports nostr.client.springwebsocket; } diff --git a/nostr-java-client/src/main/java/nostr/client/Client.java b/nostr-java-client/src/main/java/nostr/client/Client.java deleted file mode 100644 index d1cc550f2..000000000 --- a/nostr-java-client/src/main/java/nostr/client/Client.java +++ /dev/null @@ -1,117 +0,0 @@ -package nostr.client; - -import lombok.AllArgsConstructor; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import lombok.SneakyThrows; -import lombok.extern.java.Log; -import nostr.base.Relay; -import nostr.connection.impl.ConnectionPool; -import nostr.context.Context; -import nostr.context.RequestContext; -import nostr.context.impl.DefaultRequestContext; -import nostr.event.BaseMessage; -import nostr.event.impl.GenericTag; -import nostr.event.message.CanonicalAuthenticationMessage; -import nostr.util.thread.Task; -import nostr.util.thread.ThreadUtil; - -import java.util.concurrent.TimeoutException; -import java.util.logging.Level; - -@Log -@NoArgsConstructor -public class Client { - - private static class Holder { - private static final Client INSTANCE = new Client(); - } - - private RequestContext context; - - private ConnectionPool connectionPool; - - public static Client getInstance() { - return Holder.INSTANCE; - } - - public Client connect(@NonNull RequestContext context) throws TimeoutException { - if (context instanceof DefaultRequestContext defaultRequestContext) { - Holder.INSTANCE.context = context; - connectionPool = ConnectionPool.getInstance(defaultRequestContext); - ThreadUtil.builder().blocking(true).lock(true).task(new RelayConnectionTask(this.connectionPool)).build().run(context); - } - return this; - } - - public void disconnect() throws TimeoutException { - ThreadUtil.builder().blocking(true).task(new RelayDisconnectionTask(this.connectionPool)).build().run(this.context); - } - - public int getOpenConnectionsCount() { - return connectionPool.connectionCount(); - } - - public void send(@NonNull BaseMessage message) throws TimeoutException { - log.log(Level.FINE, "Requesting to send the message {0}...", message); - ThreadUtil.builder().blocking(false).lock(true).task(new SendMessageTask(message, this.connectionPool)).build().run(this.context); - } - - boolean isConnected(@NonNull Relay relay) { - return this.connectionPool.isConnectedTo(relay); - } - - @AllArgsConstructor - private static class RelayConnectionTask implements Task { - - private final ConnectionPool connectionManager; - - @Override - public Void execute(@NonNull Context context) { - connectionManager.connect(); - return null; - } - } - - @AllArgsConstructor - private static class RelayDisconnectionTask implements Task { - - private final ConnectionPool connectionPool; - - @Override - public Void execute(@NonNull Context context) { - connectionPool.disconnect(); - return null; - } - } - - @AllArgsConstructor - private static class SendMessageTask implements Task { - - private final BaseMessage message; - private final ConnectionPool connectionPool; - - @SneakyThrows - @Override - public Void execute(@NonNull Context context) { - // Only send AUTH messages to the relay mentioned in the tag https://github.com/tcheeric/nostr-java/issues/129 - if (message instanceof CanonicalAuthenticationMessage canonicalAuthenticationMessage) { - log.log(Level.FINE, ">>> Authentication message {0}...", canonicalAuthenticationMessage); - var event = canonicalAuthenticationMessage.getEvent(); - var relayTag = event.getTags().stream().filter(t -> t.getCode().equalsIgnoreCase("relay")).findFirst(); - if (relayTag.isPresent()) { - var relayTagValue = ((GenericTag) relayTag.get()).getAttributes().get(0).getValue().toString(); - log.log(Level.FINEST, "**** Relay found in CanonicalAuthenticationMessage: {0}", relayTagValue); - var r = new Relay(relayTagValue); - connectionPool.send(canonicalAuthenticationMessage.encode(), r); - } else { - log.log(Level.SEVERE, "Relay tag not found in CanonicalAuthenticationMessage. Ignoring..."); - } - } else { - log.log(Level.FINER, "+++ message {0}...", message); - connectionPool.send(message.encode()); - } - return null; - } - } -} diff --git a/nostr-java-client/src/main/java/nostr/client/springwebsocket/ReactiveWebSocketClient.java b/nostr-java-client/src/main/java/nostr/client/springwebsocket/ReactiveWebSocketClient.java index e71e9f06d..4f9f72647 100644 --- a/nostr-java-client/src/main/java/nostr/client/springwebsocket/ReactiveWebSocketClient.java +++ b/nostr-java-client/src/main/java/nostr/client/springwebsocket/ReactiveWebSocketClient.java @@ -18,7 +18,7 @@ @Component @Scope(BeanDefinition.SCOPE_PROTOTYPE) -public class ReactiveWebSocketClient implements WebSocketClient { +public class ReactiveWebSocketClient implements WebSocketClientIF { private final ReactorNettyWebSocketClient client; private final URI uri; diff --git a/nostr-java-client/src/main/java/nostr/client/springwebsocket/SpringWebSocketClient.java b/nostr-java-client/src/main/java/nostr/client/springwebsocket/SpringWebSocketClient.java index c685f09e2..c1647f99d 100644 --- a/nostr-java-client/src/main/java/nostr/client/springwebsocket/SpringWebSocketClient.java +++ b/nostr-java-client/src/main/java/nostr/client/springwebsocket/SpringWebSocketClient.java @@ -9,27 +9,27 @@ import java.util.List; public class SpringWebSocketClient { - private final WebSocketClient webSocketClient; + private final WebSocketClientIF webSocketClientIF; @Getter private final String relayUrl; public SpringWebSocketClient(@NonNull String relayUrl) { - webSocketClient = new StandardWebSocketClient(relayUrl); + webSocketClientIF = new StandardWebSocketClient(relayUrl); this.relayUrl = relayUrl; } @SneakyThrows public List send(@NonNull BaseMessage eventMessage) { - return webSocketClient.send(eventMessage.encode()); + return webSocketClientIF.send(eventMessage.encode()); } public List send(@NonNull String json) throws IOException { - return webSocketClient.send(json); + return webSocketClientIF.send(json); } public void closeSocket() throws IOException { - webSocketClient.closeSocket(); + webSocketClientIF.closeSocket(); } } diff --git a/nostr-java-client/src/main/java/nostr/client/springwebsocket/StandardWebSocketClient.java b/nostr-java-client/src/main/java/nostr/client/springwebsocket/StandardWebSocketClient.java index 23328d426..dd2ac463c 100644 --- a/nostr-java-client/src/main/java/nostr/client/springwebsocket/StandardWebSocketClient.java +++ b/nostr-java-client/src/main/java/nostr/client/springwebsocket/StandardWebSocketClient.java @@ -1,9 +1,8 @@ package nostr.client.springwebsocket; -import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.NonNull; import lombok.SneakyThrows; import nostr.event.BaseMessage; -import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Scope; @@ -17,15 +16,16 @@ import java.net.URI; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import static org.awaitility.Awaitility.await; @Component @Scope(BeanDefinition.SCOPE_PROTOTYPE) -public class StandardWebSocketClient extends TextWebSocketHandler implements WebSocketClient { +public class StandardWebSocketClient extends TextWebSocketHandler implements WebSocketClientIF { private final WebSocketSession clientSession; private List events = new ArrayList<>(); - private boolean completed = false; + private final AtomicBoolean completed = new AtomicBoolean(false); @SneakyThrows public StandardWebSocketClient(@Value("${nostr.relay.uri}") String relayUri) { @@ -33,23 +33,25 @@ public StandardWebSocketClient(@Value("${nostr.relay.uri}") String relayUri) { } @Override - protected void handleTextMessage(@NotNull WebSocketSession session, TextMessage message) { + protected void handleTextMessage(@NonNull WebSocketSession session, TextMessage message) { events.add(message.getPayload()); - completed = true; + completed.setRelease(true); } @Override - public List send(T eventMessage) throws JsonProcessingException, IOException { + public List send(T eventMessage) throws IOException { return send(eventMessage.encode()); } @Override public List send(String json) throws IOException { clientSession.sendMessage(new TextMessage(json)); - await().until(() -> completed); + await() +// .timeout(66, TimeUnit.MINUTES) + .untilTrue(completed); List eventList = List.copyOf(events); events = new ArrayList<>(); - completed = false; + completed.setRelease(false); return eventList; } @@ -57,4 +59,4 @@ public List send(String json) throws IOException { public void closeSocket() throws IOException { clientSession.close(); } -} \ No newline at end of file +} diff --git a/nostr-java-client/src/main/java/nostr/client/springwebsocket/WebSocketClient.java b/nostr-java-client/src/main/java/nostr/client/springwebsocket/WebSocketClientIF.java similarity index 89% rename from nostr-java-client/src/main/java/nostr/client/springwebsocket/WebSocketClient.java rename to nostr-java-client/src/main/java/nostr/client/springwebsocket/WebSocketClientIF.java index 600a4284e..75e538201 100644 --- a/nostr-java-client/src/main/java/nostr/client/springwebsocket/WebSocketClient.java +++ b/nostr-java-client/src/main/java/nostr/client/springwebsocket/WebSocketClientIF.java @@ -5,7 +5,7 @@ import java.io.IOException; import java.util.List; -public interface WebSocketClient { +public interface WebSocketClientIF { List send(T eventMessage) throws IOException; List send(String json) throws IOException; void closeSocket() throws IOException; diff --git a/nostr-java-command-interface/pom.xml b/nostr-java-command-interface/pom.xml deleted file mode 100644 index a42859beb..000000000 --- a/nostr-java-command-interface/pom.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - 4.0.0 - - - xyz.tcheeric - nostr-java - 0.6.3-SNAPSHOT - - - nostr-java-command-interface - jar - - - ${project.groupId} - nostr-java-util - ${project.version} - - - ${project.groupId} - nostr-java-base - ${project.version} - - - ${project.groupId} - nostr-java-event - ${project.version} - - - ${project.groupId} - nostr-java-context-interface - ${project.version} - compile - - - \ No newline at end of file diff --git a/nostr-java-command-interface/src/main/java/module-info.java b/nostr-java-command-interface/src/main/java/module-info.java deleted file mode 100644 index 6664203b6..000000000 --- a/nostr-java-command-interface/src/main/java/module-info.java +++ /dev/null @@ -1,10 +0,0 @@ -import nostr.command.CommandHandler; - -module nostr.command.handler { - - requires nostr.context; - - exports nostr.command; - - uses CommandHandler; -} diff --git a/nostr-java-command-interface/src/main/java/nostr/command/CommandHandler.java b/nostr-java-command-interface/src/main/java/nostr/command/CommandHandler.java deleted file mode 100644 index ca1ff6e50..000000000 --- a/nostr-java-command-interface/src/main/java/nostr/command/CommandHandler.java +++ /dev/null @@ -1,8 +0,0 @@ -package nostr.command; - -import nostr.context.CommandContext; - -public interface CommandHandler { - - void handle(CommandContext context); -} diff --git a/nostr-java-command-provider/pom.xml b/nostr-java-command-provider/pom.xml deleted file mode 100644 index 8ab9e4a0d..000000000 --- a/nostr-java-command-provider/pom.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - 4.0.0 - - - xyz.tcheeric - nostr-java - 0.6.3-SNAPSHOT - - - nostr-java-command-provider - jar - - - - ${project.groupId} - nostr-java-id - ${project.version} - - - ${project.groupId} - nostr-java-client - ${project.version} - - - ${project.groupId} - nostr-java-context - ${project.version} - compile - - - ${project.groupId} - nostr-java-command-interface - ${project.version} - - - - - - - \ No newline at end of file diff --git a/nostr-java-command-provider/src/main/java/module-info.java b/nostr-java-command-provider/src/main/java/module-info.java deleted file mode 100644 index 34f9f62c2..000000000 --- a/nostr-java-command-provider/src/main/java/module-info.java +++ /dev/null @@ -1,31 +0,0 @@ -import nostr.command.provider.AuthCommandHandler; -import nostr.command.provider.ClosedCommandHandler; -import nostr.command.provider.EoseCommandHandler; -import nostr.command.provider.EventCommandHandler; -import nostr.command.provider.NoticeCommandHandler; -import nostr.command.provider.OkCommandHandler; -import nostr.command.CommandHandler; - -module nostr.command.provider { - requires static lombok; - requires java.logging; - - requires nostr.base; - requires nostr.id; - requires nostr.client; - requires nostr.event; - requires nostr.context; - requires nostr.context.impl; - requires nostr.command.handler; - requires com.fasterxml.jackson.core; - - exports nostr.command.provider; - - provides CommandHandler with - OkCommandHandler, - NoticeCommandHandler, - EoseCommandHandler, - AuthCommandHandler, - EventCommandHandler, - ClosedCommandHandler; -} diff --git a/nostr-java-command-provider/src/main/java/nostr/command/provider/AuthCommandHandler.java b/nostr-java-command-provider/src/main/java/nostr/command/provider/AuthCommandHandler.java deleted file mode 100644 index 001c29f54..000000000 --- a/nostr-java-command-provider/src/main/java/nostr/command/provider/AuthCommandHandler.java +++ /dev/null @@ -1,62 +0,0 @@ -package nostr.command.provider; - -import lombok.NoArgsConstructor; -import lombok.NonNull; -import lombok.SneakyThrows; -import lombok.extern.java.Log; -import nostr.base.Command; -import nostr.base.PrivateKey; -import nostr.base.annotation.DefaultHandler; -import nostr.client.Client; -import nostr.command.CommandHandler; -import nostr.context.CommandContext; -import nostr.context.impl.DefaultCommandContext; -import nostr.event.impl.CanonicalAuthenticationEvent; -import nostr.event.message.CanonicalAuthenticationMessage; -import nostr.event.message.RelayAuthenticationMessage; -import nostr.id.Identity; - -import java.util.logging.Level; - -@DefaultHandler(command = Command.AUTH) -@NoArgsConstructor -@Log -public class AuthCommandHandler implements CommandHandler { - - @SneakyThrows - @Override - public void handle(@NonNull CommandContext context) { - - log.log(Level.INFO, "onAuth event - {0}", context); - - if (context instanceof DefaultCommandContext defaultCommandContext) { - var message = defaultCommandContext.getMessage(); - - if (message instanceof RelayAuthenticationMessage) { - log.log(Level.INFO, "Authentication required on relay {0}", defaultCommandContext.getRelay()); - - var privateKey = defaultCommandContext.getPrivateKey(); - var identity = Identity.create(new PrivateKey(privateKey)); - var publicKey = identity.getPublicKey(); - var challenge = defaultCommandContext.getChallenge(); - var relay = defaultCommandContext.getRelay(); - - // Create the event - var canonicalAuthenticationEvent = new CanonicalAuthenticationEvent(publicKey, challenge, relay); - - // Sign the event - identity.sign(canonicalAuthenticationEvent); - - var client = Client.getInstance(); - var canonicalAuthenticationMessage = new CanonicalAuthenticationMessage(canonicalAuthenticationEvent); - var encodedMessage = canonicalAuthenticationMessage.encode(); - log.log(Level.INFO, "Sending authentication event {0} to the relay {1}", new Object[]{encodedMessage, relay}); - - // Publish the event to the relay - client.send(canonicalAuthenticationMessage); - } - } else { - throw new IllegalArgumentException("Invalid context type"); - } - } -} diff --git a/nostr-java-command-provider/src/main/java/nostr/command/provider/ClosedCommandHandler.java b/nostr-java-command-provider/src/main/java/nostr/command/provider/ClosedCommandHandler.java deleted file mode 100644 index 20400e981..000000000 --- a/nostr-java-command-provider/src/main/java/nostr/command/provider/ClosedCommandHandler.java +++ /dev/null @@ -1,63 +0,0 @@ -package nostr.command.provider; - -import lombok.NoArgsConstructor; -import lombok.SneakyThrows; -import lombok.extern.java.Log; -import nostr.base.Command; -import nostr.base.PrivateKey; -import nostr.base.annotation.DefaultHandler; -import nostr.client.Client; -import nostr.command.CommandHandler; -import nostr.context.CommandContext; -import nostr.context.impl.DefaultCommandContext; -import nostr.event.impl.CanonicalAuthenticationEvent; -import nostr.event.message.CanonicalAuthenticationMessage; -import nostr.event.message.ClosedMessage; -import nostr.id.Identity; - -import java.util.logging.Level; - -@DefaultHandler(command = Command.CLOSED) -@NoArgsConstructor -@Log -public class ClosedCommandHandler implements CommandHandler { - - @SneakyThrows - @Override - public void handle(CommandContext context) { - - log.info("onClosed event {0}" + context); - - if (context instanceof DefaultCommandContext defaultCommandContext) { - var message = defaultCommandContext.getMessage(); - - if (message instanceof ClosedMessage closedMessage) { - if (closedMessage.getMessage().startsWith("auth-required:")) { - log.log(Level.INFO, "Authentication required on relay {0}", defaultCommandContext.getRelay()); - - var privateKey = defaultCommandContext.getPrivateKey(); - var identity = Identity.create(new PrivateKey(privateKey)); - var publicKey = identity.getPublicKey(); - var challenge = defaultCommandContext.getChallenge(); - var relay = defaultCommandContext.getRelay(); - - // Create the event - var canonicalAuthenticationEvent = new CanonicalAuthenticationEvent(publicKey, challenge, relay); - - // Sign the event - identity.sign(canonicalAuthenticationEvent); - - var client = Client.getInstance(); // No need to pass the request context here. The client will use the default one - var canonicalAuthenticationMessage = new CanonicalAuthenticationMessage(canonicalAuthenticationEvent); - var encodedMessage = canonicalAuthenticationMessage.encode(); - log.log(Level.INFO, "Sending authentication event {0} to the relay {1}", new Object[]{encodedMessage, relay}); - - // Publish the event to the relay - client.send(canonicalAuthenticationMessage); - } - } - } else { - throw new IllegalArgumentException("Invalid context type"); - } - } -} diff --git a/nostr-java-command-provider/src/main/java/nostr/command/provider/EoseCommandHandler.java b/nostr-java-command-provider/src/main/java/nostr/command/provider/EoseCommandHandler.java deleted file mode 100644 index 3b9df1846..000000000 --- a/nostr-java-command-provider/src/main/java/nostr/command/provider/EoseCommandHandler.java +++ /dev/null @@ -1,21 +0,0 @@ -package nostr.command.provider; - -import lombok.NoArgsConstructor; -import lombok.extern.java.Log; -import nostr.base.Command; -import nostr.base.annotation.DefaultHandler; -import nostr.command.CommandHandler; -import nostr.context.CommandContext; - -import java.util.logging.Level; - -@Log -@DefaultHandler(command = Command.EOSE) -@NoArgsConstructor -public class EoseCommandHandler implements CommandHandler { - - @Override - public void handle(CommandContext context) { - log.log(Level.INFO, "onEose event - {0}", context); - } -} diff --git a/nostr-java-command-provider/src/main/java/nostr/command/provider/EventCommandHandler.java b/nostr-java-command-provider/src/main/java/nostr/command/provider/EventCommandHandler.java deleted file mode 100644 index ac2fb75fc..000000000 --- a/nostr-java-command-provider/src/main/java/nostr/command/provider/EventCommandHandler.java +++ /dev/null @@ -1,21 +0,0 @@ -package nostr.command.provider; - -import lombok.NoArgsConstructor; -import lombok.extern.java.Log; -import nostr.base.Command; -import nostr.base.annotation.DefaultHandler; -import nostr.command.CommandHandler; -import nostr.context.CommandContext; - -import java.util.logging.Level; - -@Log -@DefaultHandler(command = Command.EVENT) -@NoArgsConstructor -public class EventCommandHandler implements CommandHandler { - - @Override - public void handle(CommandContext context) { - log.log(Level.INFO, "onEvent event - {0}", context); - } -} diff --git a/nostr-java-command-provider/src/main/java/nostr/command/provider/NoticeCommandHandler.java b/nostr-java-command-provider/src/main/java/nostr/command/provider/NoticeCommandHandler.java deleted file mode 100644 index 22d862036..000000000 --- a/nostr-java-command-provider/src/main/java/nostr/command/provider/NoticeCommandHandler.java +++ /dev/null @@ -1,20 +0,0 @@ -package nostr.command.provider; - -import lombok.NoArgsConstructor; -import lombok.extern.java.Log; -import nostr.base.Command; -import nostr.base.annotation.DefaultHandler; -import nostr.command.CommandHandler; -import nostr.context.CommandContext; - -import java.util.logging.Level; - -@Log -@DefaultHandler(command = Command.NOTICE) -@NoArgsConstructor -public class NoticeCommandHandler implements CommandHandler { - @Override - public void handle(CommandContext context) { - log.log(Level.INFO, "onNotice event - {0}", context); - } -} diff --git a/nostr-java-command-provider/src/main/java/nostr/command/provider/OkCommandHandler.java b/nostr-java-command-provider/src/main/java/nostr/command/provider/OkCommandHandler.java deleted file mode 100644 index 5f2c1d982..000000000 --- a/nostr-java-command-provider/src/main/java/nostr/command/provider/OkCommandHandler.java +++ /dev/null @@ -1,21 +0,0 @@ -package nostr.command.provider; - -import lombok.NoArgsConstructor; -import lombok.extern.java.Log; -import nostr.base.Command; -import nostr.base.annotation.DefaultHandler; -import nostr.command.CommandHandler; -import nostr.context.CommandContext; - -import java.util.logging.Level; - -@Log -@DefaultHandler(command = Command.OK) -@NoArgsConstructor -public class OkCommandHandler implements CommandHandler { - - @Override - public void handle(CommandContext context) { - log.log(Level.INFO, "onOk event - {0}", context); - } -} diff --git a/nostr-java-command-provider/src/main/resources/META-INF/services/nostr.command.CommandHandler b/nostr-java-command-provider/src/main/resources/META-INF/services/nostr.command.CommandHandler deleted file mode 100644 index 723510a03..000000000 --- a/nostr-java-command-provider/src/main/resources/META-INF/services/nostr.command.CommandHandler +++ /dev/null @@ -1,6 +0,0 @@ -nostr.command.provider.EventCommandHandler -nostr.command.provider.AuthCommandHandler -nostr.command.provider.EoseCommandHandler -nostr.command.provider.NoticeCommandHandler -nostr.command.provider.OkCommandHandler -nostr.command.provider.ClosedCommandHandler \ No newline at end of file diff --git a/nostr-java-connection/pom.xml b/nostr-java-connection/pom.xml deleted file mode 100644 index f27b6bfd8..000000000 --- a/nostr-java-connection/pom.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - 4.0.0 - - - xyz.tcheeric - nostr-java - 0.6.3-SNAPSHOT - - - nostr-java-connection - jar - - - - - ${project.groupId} - nostr-java-event - ${project.version} - - - ${project.groupId} - nostr-java-util - ${project.version} - - - ${project.groupId} - nostr-java-controller - ${project.version} - compile - - - com.squareup.okio - okio - ${okio.version} - - - com.squareup.okhttp3 - okhttp - ${okhttp.version} - - - - diff --git a/nostr-java-connection/src/main/java/module-info.java b/nostr-java-connection/src/main/java/module-info.java deleted file mode 100644 index 974504604..000000000 --- a/nostr-java-connection/src/main/java/module-info.java +++ /dev/null @@ -1,21 +0,0 @@ - -module nostr.connection { - - requires static lombok; - - requires okio; - requires okhttp3; - - requires java.logging; - - requires nostr.base; - requires nostr.event; - requires nostr.controller; - requires nostr.context; - requires nostr.context.impl; - requires org.bouncycastle.provider; - - exports nostr.connection; - exports nostr.connection.impl; - exports nostr.connection.impl.listeners; -} diff --git a/nostr-java-connection/src/main/java/nostr/connection/Connection.java b/nostr-java-connection/src/main/java/nostr/connection/Connection.java deleted file mode 100644 index b6c328e95..000000000 --- a/nostr-java-connection/src/main/java/nostr/connection/Connection.java +++ /dev/null @@ -1,17 +0,0 @@ -package nostr.connection; - -import nostr.base.Relay; - -public interface Connection { - - void send(String message); - - void connect(); - - void disconnect(); - - Relay getRelay(); - - boolean isConnected(); - -} diff --git a/nostr-java-connection/src/main/java/nostr/connection/impl/ConnectionImpl.java b/nostr-java-connection/src/main/java/nostr/connection/impl/ConnectionImpl.java deleted file mode 100644 index 61c1640c6..000000000 --- a/nostr-java-connection/src/main/java/nostr/connection/impl/ConnectionImpl.java +++ /dev/null @@ -1,103 +0,0 @@ -package nostr.connection.impl; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NonNull; -import lombok.ToString; -import lombok.extern.java.Log; -import nostr.base.Relay; -import nostr.connection.Connection; -import nostr.connection.impl.listeners.CloseListener; -import nostr.connection.impl.listeners.CompositeListener; -import nostr.connection.impl.listeners.ErrorListener; -import nostr.connection.impl.listeners.OpenListener; -import nostr.connection.impl.listeners.TextListener; -import nostr.context.Context; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.WebSocket; - -import java.util.Arrays; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.logging.Level; - -@Log -@ToString(onlyExplicitlyIncluded = true) -@EqualsAndHashCode(onlyExplicitlyIncluded = true) -public class ConnectionImpl implements Connection { - - @Getter - @EqualsAndHashCode.Include - @ToString.Include - private final String id = UUID.randomUUID().toString(); - - @Getter - @EqualsAndHashCode.Include - @ToString.Include - private final Relay relay; - - private final Context context; - - private WebSocket webSocket = null; - - private final AtomicBoolean connected = new AtomicBoolean(false); - - public ConnectionImpl(@NonNull Relay relay, @NonNull Context context) { - this.relay = relay; - this.context = context; - } - - @Override - public void connect() { - var relay = getRelay(); - - try { - if (isConnected()) { - log.log(Level.FINE, "Already connected to {0}. Do nothing...", relay); - return; - } - - log.log(Level.INFO, "Connecting to {0}... httpUrl = {1}", new Object[]{relay, HttpUrl.parse(relay.getHttpUri())}); - var client = new OkHttpClient(); - var compositeListener = new CompositeListener(Arrays.asList(new OpenListener(relay), new TextListener(relay, context), new CloseListener(relay), new ErrorListener(relay))); - - webSocket = client.newWebSocket((new Request.Builder()).url(HttpUrl.parse(relay.getHttpUri())).build(), compositeListener); - //.connectTimeout(Duration.ofMillis(10000)) // TODO - make this configurable and add to the context. - - connected.set(true); - - log.log(Level.INFO, "Connected to {0}", getRelay()); - } catch (Exception e) { - log.log(Level.SEVERE, "Failed to connect to {0} - {1}", new Object[]{relay, e.getMessage()}); - connected.set(false); - } - } - - @Override - public boolean isConnected() { - return connected.get() && webSocket != null; - } - - @Override - public void send(@NonNull String message) { - if (!isConnected()) { - log.log(Level.WARNING, "FAIL - Not properly connected with Relay: {0}", relay); - return; - } - - log.log(Level.INFO, "Sending message: {0} - Relay: {1}", new Object[]{message, relay}); - webSocket.send(message); - } - - @Override - public void disconnect() { - if (isConnected()) { - log.log(Level.INFO, "Disconnecting from {0}", relay); - // NORMAL_CLOSURE is 1000 - webSocket.close(1000, "bye"); // TODO check return result of close - } - connected.set(false); - } -} \ No newline at end of file diff --git a/nostr-java-connection/src/main/java/nostr/connection/impl/ConnectionPool.java b/nostr-java-connection/src/main/java/nostr/connection/impl/ConnectionPool.java deleted file mode 100644 index 44e144813..000000000 --- a/nostr-java-connection/src/main/java/nostr/connection/impl/ConnectionPool.java +++ /dev/null @@ -1,105 +0,0 @@ -package nostr.connection.impl; - -import lombok.Getter; -import lombok.NonNull; -import lombok.extern.java.Log; -import nostr.base.Relay; -import nostr.connection.Connection; -import nostr.context.Context; -import nostr.context.impl.DefaultRequestContext; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.logging.Level; - -@Getter -@Log -public class ConnectionPool { - - private static class Holder { - private static final ConnectionPool INSTANCE = new ConnectionPool(); - } - - private final Set connections = Collections.synchronizedSet(new HashSet<>()); - - private ConnectionPool() { - // private constructor to prevent instantiation - } - - public static ConnectionPool getInstance(@NonNull Context context) { - if (Holder.INSTANCE.connections.isEmpty() && context instanceof DefaultRequestContext defaultRequestContext) { - var relays = defaultRequestContext.getRelays(); - relays.values().stream().map(Relay::new).forEach(r -> Holder.INSTANCE.addConnection(new ConnectionImpl(r, context))); - } - return Holder.INSTANCE; - } - - public void connect() { - log.log(Level.INFO, "Connecting to relays"); - connections.forEach(c -> { - c.connect(); - }); - } - - public void disconnect() { - log.log(Level.INFO, "Disconnecting from relays"); - connections.forEach(connection -> { - connection.disconnect(); - }); - } - - public void disconnect(@NonNull Relay relay) { - log.log(Level.INFO, "Disconnecting from {0}...", relay); - var connection = getConnection(relay); - if (connection != null) { - connection.disconnect(); - } else { - log.log(Level.WARNING, "No connection found for {0}. Ignoring...", relay); - } - } - - public Connection getConnection(@NonNull Relay relay) { - return connections.stream().filter(c -> c.getRelay().getUri().equalsIgnoreCase(relay.getUri())).findFirst().orElse(null); - } - - public boolean isConnectedTo(@NonNull Relay relay) { - return connections.stream().filter(c -> c.getRelay().equals(relay)).anyMatch(Connection::isConnected); - } - - public int connectionCount() { - return (int) connections.stream().filter(Connection::isConnected).count(); - } - - public void send(@NonNull String message) { - log.log(Level.FINER, "Connected to {0} relay(s)...", connections.size()); - connections.forEach(conn -> { - conn.send(message); - }); - } - - public void send(@NonNull String message, @NonNull Relay relay) { - log.log(Level.FINER, "ConnectionPool.send({0}, {1}) / {2} connection(s)", new Object[]{message, relay, connections.size()}); - var connection = getConnection(relay); - if (connection != null) { - log.log(Level.FINE, "Trying to send {0} to {1}...", new Object[]{message, relay}); - connection.send(message); - } - else { - log.log(Level.WARNING, "No connection found for {0}", relay); - } - } - - private boolean addConnection(@NonNull Connection connection) { - return connections.add(connection); - } - - private boolean removeConnection(@NonNull Connection connection) { - return connections.remove(connection); - } - - private void removeAllConnections() { - connections.clear(); - } - -} \ No newline at end of file diff --git a/nostr-java-connection/src/main/java/nostr/connection/impl/listeners/CloseListener.java b/nostr-java-connection/src/main/java/nostr/connection/impl/listeners/CloseListener.java deleted file mode 100644 index f064bee5c..000000000 --- a/nostr-java-connection/src/main/java/nostr/connection/impl/listeners/CloseListener.java +++ /dev/null @@ -1,40 +0,0 @@ -package nostr.connection.impl.listeners; - -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.ToString; -import lombok.extern.java.Log; -import nostr.base.Relay; -import nostr.event.Response; - -import okhttp3.WebSocket; -import okhttp3.WebSocketListener; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.logging.Level; - -@AllArgsConstructor -@Log -public class CloseListener extends WebSocketListener { - - @Getter - @EqualsAndHashCode.Include - @ToString.Include - private final Relay relay; - - private final Set responses = Collections.synchronizedSet(new HashSet<>()); - - @Override - public void onClosed(WebSocket webSocket, int statusCode, String reason) { - log.log(Level.INFO, "WebSocket connection to {0} closed: {1}, {2}", new Object[]{relay, statusCode, reason}); - responses.clear(); - } - - @Override - public void onClosing(WebSocket webSocket, int statusCode, String reason) { - log.log(Level.INFO, "WebSocket connection to {0} closing: {1}, {2}", new Object[]{relay, statusCode, reason}); - responses.clear(); - } -} diff --git a/nostr-java-connection/src/main/java/nostr/connection/impl/listeners/CompositeListener.java b/nostr-java-connection/src/main/java/nostr/connection/impl/listeners/CompositeListener.java deleted file mode 100644 index a7f696bca..000000000 --- a/nostr-java-connection/src/main/java/nostr/connection/impl/listeners/CompositeListener.java +++ /dev/null @@ -1,46 +0,0 @@ -package nostr.connection.impl.listeners; - -import okhttp3.WebSocket; -import okhttp3.WebSocketListener; -import okhttp3.Response; -import okio.ByteString; - -import java.util.List; - -public class CompositeListener extends WebSocketListener { - private final List listeners; - - public CompositeListener(List listeners) { - this.listeners = listeners; - } - - @Override - public void onOpen(WebSocket webSocket, Response response) { - listeners.forEach(listener -> listener.onOpen(webSocket, response)); - } - - @Override - public void onMessage(WebSocket webSocket, String data) { - listeners.forEach(listener -> listener.onMessage(webSocket, data)); - } - - @Override - public void onMessage(WebSocket webSocket, ByteString bytes) { - listeners.forEach(listener -> listener.onMessage(webSocket, bytes)); - } - - @Override - public void onClosing(WebSocket webSocket, int statusCode, String reason) { - listeners.forEach(listener -> listener.onClosing(webSocket, statusCode, reason)); - } - - @Override - public void onClosed(WebSocket webSocket, int statusCode, String reason) { - listeners.forEach(listener -> listener.onClosed(webSocket, statusCode, reason)); - } - - @Override - public void onFailure(WebSocket webSocket, Throwable error, Response response) { - listeners.forEach(listener -> listener.onFailure(webSocket, error, response)); - } -} diff --git a/nostr-java-connection/src/main/java/nostr/connection/impl/listeners/ErrorListener.java b/nostr-java-connection/src/main/java/nostr/connection/impl/listeners/ErrorListener.java deleted file mode 100644 index 607159813..000000000 --- a/nostr-java-connection/src/main/java/nostr/connection/impl/listeners/ErrorListener.java +++ /dev/null @@ -1,29 +0,0 @@ -package nostr.connection.impl.listeners; - -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.ToString; -import lombok.extern.java.Log; -import nostr.base.Relay; - -import okhttp3.WebSocketListener; -import okhttp3.WebSocket; -import okhttp3.Response; -import java.util.logging.Level; - -@Getter -@Log -@AllArgsConstructor -public class ErrorListener extends WebSocketListener { - - @EqualsAndHashCode.Include - @ToString.Include - private final Relay relay; - - @Override - public void onFailure(WebSocket webSocket, Throwable error, Response response) { - log.log(Level.WARNING, "WebSocket error: {0} - Relay {1}", new Object[]{error, relay}); - error.printStackTrace(); - } -} diff --git a/nostr-java-connection/src/main/java/nostr/connection/impl/listeners/OpenListener.java b/nostr-java-connection/src/main/java/nostr/connection/impl/listeners/OpenListener.java deleted file mode 100644 index fb3e69d74..000000000 --- a/nostr-java-connection/src/main/java/nostr/connection/impl/listeners/OpenListener.java +++ /dev/null @@ -1,29 +0,0 @@ -package nostr.connection.impl.listeners; - -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.ToString; -import lombok.extern.java.Log; -import nostr.base.Relay; - -import okhttp3.WebSocket; -import okhttp3.WebSocketListener; -import okhttp3.Response; -import java.util.logging.Level; - -@Getter -@AllArgsConstructor -@Log -public class OpenListener extends WebSocketListener { - - @Getter - @EqualsAndHashCode.Include - @ToString.Include - private final Relay relay; - - @Override - public void onOpen(WebSocket webSocket, Response response) { - log.log(Level.INFO, "WebSocket opened to {0}", relay); - } -} diff --git a/nostr-java-connection/src/main/java/nostr/connection/impl/listeners/TextListener.java b/nostr-java-connection/src/main/java/nostr/connection/impl/listeners/TextListener.java deleted file mode 100644 index 7695e8a7e..000000000 --- a/nostr-java-connection/src/main/java/nostr/connection/impl/listeners/TextListener.java +++ /dev/null @@ -1,88 +0,0 @@ -package nostr.connection.impl.listeners; - -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NonNull; -import lombok.ToString; -import lombok.extern.java.Log; -import nostr.base.Relay; -import nostr.context.CommandContext; -import nostr.context.Context; -import nostr.context.impl.DefaultCommandContext; -import nostr.context.impl.DefaultRequestContext; -import nostr.controller.impl.ApplicationControllerImpl; -import nostr.event.BaseMessage; -import nostr.event.json.codec.BaseMessageDecoder; -import nostr.event.message.ClosedMessage; -import nostr.event.message.RelayAuthenticationMessage; - -import okhttp3.WebSocketListener; -import okhttp3.WebSocket; -import okio.ByteString; - -import java.util.logging.Level; - -@AllArgsConstructor -@Log -public class TextListener extends WebSocketListener { - - @Getter - @EqualsAndHashCode.Include - @ToString.Include - private final Relay relay; - - private Context context; - - @Override - public void onMessage(WebSocket webSocket, String message) { - // nocheckin - log.log(Level.INFO, "WebSocket received: " + message + " - Relay: {0}", new Object[]{relay}); - handleReceivedText(message); - } - - @Override - public void onMessage(WebSocket webSocket, ByteString bytes) { - String message = bytes.toString(); - log.log(Level.INFO, "WebSocket received: {0} - Relay: {1}", new Object[]{message, relay}); - handleReceivedText(message); - } - - private void handleReceivedText(@NonNull String message) { - log.log(Level.INFO, "Received message {0} from {1}", new Object[]{message, relay}); - - var msg = new BaseMessageDecoder<>().decode(message); - final String strCommand = msg.getCommand(); - - log.log(Level.FINE, "Creating the command context with message {0}", new Object[]{msg}); - Context commandContext = createCommandContext(msg); - var applicationController = new ApplicationControllerImpl(strCommand); - - applicationController.handleRequest(commandContext); - } - - private CommandContext createCommandContext(@NonNull BaseMessage message) { - var commandContext = new DefaultCommandContext(); - - // Pass on the message - commandContext.setMessage(message); - - // Pass on the private key - commandContext.setPrivateKey(((DefaultRequestContext) this.context).getPrivateKey()); - - // Pass on the first relay - commandContext.setRelay(relay); - - // Set the challenge - if (message instanceof RelayAuthenticationMessage authMessage) { - log.log(Level.FINE, "Setting the challenge {0} for the relay {1}", new Object[]{authMessage.getChallenge(), relay}); - commandContext.setChallenge(authMessage.getChallenge()); - } - - if (message instanceof ClosedMessage) { - commandContext.setChallenge(((DefaultRequestContext) this.context).getChallenge(relay)); - } - - return commandContext; - } -} diff --git a/nostr-java-context-interface/pom.xml b/nostr-java-context-interface/pom.xml deleted file mode 100644 index f4393d987..000000000 --- a/nostr-java-context-interface/pom.xml +++ /dev/null @@ -1,22 +0,0 @@ - - 4.0.0 - - xyz.tcheeric - nostr-java - 0.6.3-SNAPSHOT - - - nostr-java-context-interface - jar - - nostr-java-context-interface - http://maven.apache.org - - - UTF-8 - - - - - diff --git a/nostr-java-context-interface/src/main/java/module-info.java b/nostr-java-context-interface/src/main/java/module-info.java deleted file mode 100644 index 739d96fc7..000000000 --- a/nostr-java-context-interface/src/main/java/module-info.java +++ /dev/null @@ -1,4 +0,0 @@ -module nostr.context { - - exports nostr.context; -} \ No newline at end of file diff --git a/nostr-java-context-interface/src/main/java/nostr/context/CommandContext.java b/nostr-java-context-interface/src/main/java/nostr/context/CommandContext.java deleted file mode 100644 index 0a6413171..000000000 --- a/nostr-java-context-interface/src/main/java/nostr/context/CommandContext.java +++ /dev/null @@ -1,4 +0,0 @@ -package nostr.context; - -public interface CommandContext extends Context { -} diff --git a/nostr-java-context-interface/src/main/java/nostr/context/Context.java b/nostr-java-context-interface/src/main/java/nostr/context/Context.java deleted file mode 100644 index 66ca9e9b6..000000000 --- a/nostr-java-context-interface/src/main/java/nostr/context/Context.java +++ /dev/null @@ -1,11 +0,0 @@ -package nostr.context; - -public interface Context { - - void validate(); - - enum Type { - REQUEST, - COMMAND - } -} diff --git a/nostr-java-context-interface/src/main/java/nostr/context/RequestContext.java b/nostr-java-context-interface/src/main/java/nostr/context/RequestContext.java deleted file mode 100644 index 9b3aac647..000000000 --- a/nostr-java-context-interface/src/main/java/nostr/context/RequestContext.java +++ /dev/null @@ -1,4 +0,0 @@ -package nostr.context; - -public interface RequestContext extends Context { -} diff --git a/nostr-java-context/pom.xml b/nostr-java-context/pom.xml deleted file mode 100644 index 6c30861e0..000000000 --- a/nostr-java-context/pom.xml +++ /dev/null @@ -1,42 +0,0 @@ - - 4.0.0 - - xyz.tcheeric - nostr-java - 0.6.3-SNAPSHOT - - - nostr-java-context - jar - - nostr-java-context - http://maven.apache.org - - - UTF-8 - - - - - org.projectlombok - lombok - - - ${project.groupId} - nostr-java-event - ${project.version} - - - ${project.groupId} - nostr-java-base - ${project.version} - - - ${project.groupId} - nostr-java-context-interface - ${project.version} - compile - - - diff --git a/nostr-java-context/src/main/java/module-info.java b/nostr-java-context/src/main/java/module-info.java deleted file mode 100644 index d8d637eb1..000000000 --- a/nostr-java-context/src/main/java/module-info.java +++ /dev/null @@ -1,9 +0,0 @@ -module nostr.context.impl { - requires lombok; - - requires nostr.context; - requires nostr.event; - requires nostr.base; - - exports nostr.context.impl; -} \ No newline at end of file diff --git a/nostr-java-context/src/main/java/nostr/context/impl/DefaultCommandContext.java b/nostr-java-context/src/main/java/nostr/context/impl/DefaultCommandContext.java deleted file mode 100644 index 8949f63b8..000000000 --- a/nostr-java-context/src/main/java/nostr/context/impl/DefaultCommandContext.java +++ /dev/null @@ -1,21 +0,0 @@ -package nostr.context.impl; - -import lombok.Data; -import lombok.NoArgsConstructor; -import nostr.base.Relay; -import nostr.context.CommandContext; -import nostr.event.BaseMessage; - -@Data -@NoArgsConstructor -public class DefaultCommandContext implements CommandContext { - private BaseMessage message; - private String challenge; - private Relay relay; - private byte[] privateKey; - - @Override - public void validate() { - - } -} diff --git a/nostr-java-context/src/main/java/nostr/context/impl/DefaultRequestContext.java b/nostr-java-context/src/main/java/nostr/context/impl/DefaultRequestContext.java deleted file mode 100644 index 5f0a4b0f0..000000000 --- a/nostr-java-context/src/main/java/nostr/context/impl/DefaultRequestContext.java +++ /dev/null @@ -1,39 +0,0 @@ -package nostr.context.impl; - -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import nostr.base.Relay; -import nostr.context.RequestContext; - -import java.util.HashMap; -import java.util.Map; - -@Data -@NoArgsConstructor -public class DefaultRequestContext implements RequestContext { - private byte[] privateKey; - private String subscriptionId; - private Map relays; - private Map challenges = new HashMap<>(); - - @Override - public void validate() { - } - - public String getChallenge(@NonNull Relay relay) { - return challenges.get(relay); - } - - public String getChallenge(String relay) { - return getChallenge(new Relay(relay)); - } - - public void setChallenge(@NonNull Relay relay, @NonNull String challenge) { - challenges.put(relay, challenge); - } - - public void setChallenge(String relay, String challenge) { - setChallenge(new Relay(relay), challenge); - } -} \ No newline at end of file diff --git a/nostr-java-controller/pom.xml b/nostr-java-controller/pom.xml deleted file mode 100644 index c5de9dd59..000000000 --- a/nostr-java-controller/pom.xml +++ /dev/null @@ -1,46 +0,0 @@ - - 4.0.0 - - xyz.tcheeric - nostr-java - 0.6.3-SNAPSHOT - - - nostr-java-controller - jar - - nostr-java-controller-command - http://maven.apache.org - - - UTF-8 - - - - - ${project.groupId} - nostr-java-event - ${project.version} - compile - - - ${project.groupId} - nostr-java-context - ${project.version} - compile - - - ${project.groupId} - nostr-java-command-interface - ${project.version} - compile - - - ${project.groupId} - nostr-java-base - ${project.version} - compile - - - diff --git a/nostr-java-controller/src/main/java/module-info.java b/nostr-java-controller/src/main/java/module-info.java deleted file mode 100644 index 2fa81f3c5..000000000 --- a/nostr-java-controller/src/main/java/module-info.java +++ /dev/null @@ -1,18 +0,0 @@ -import nostr.command.CommandHandler; - -module nostr.controller { - uses CommandHandler; - - requires static lombok; - - requires java.logging; - - requires nostr.context; - requires nostr.context.impl; - requires nostr.base; - requires nostr.util; - requires nostr.command.handler; - - exports nostr.controller.impl; - exports nostr.controller; -} \ No newline at end of file diff --git a/nostr-java-controller/src/main/java/nostr/controller/ApplicationController.java b/nostr-java-controller/src/main/java/nostr/controller/ApplicationController.java deleted file mode 100644 index 3dbb465a4..000000000 --- a/nostr-java-controller/src/main/java/nostr/controller/ApplicationController.java +++ /dev/null @@ -1,4 +0,0 @@ -package nostr.controller; - -public interface ApplicationController extends Controller { -} diff --git a/nostr-java-controller/src/main/java/nostr/controller/Controller.java b/nostr-java-controller/src/main/java/nostr/controller/Controller.java deleted file mode 100644 index 63764c470..000000000 --- a/nostr-java-controller/src/main/java/nostr/controller/Controller.java +++ /dev/null @@ -1,11 +0,0 @@ -package nostr.controller; - -import nostr.context.Context; -import nostr.util.thread.Task; - -public interface Controller extends Task { - - void initialize(); - - void handleRequest(Context requestContext); -} diff --git a/nostr-java-controller/src/main/java/nostr/controller/impl/ApplicationControllerImpl.java b/nostr-java-controller/src/main/java/nostr/controller/impl/ApplicationControllerImpl.java deleted file mode 100644 index 3421c9ff7..000000000 --- a/nostr-java-controller/src/main/java/nostr/controller/impl/ApplicationControllerImpl.java +++ /dev/null @@ -1,84 +0,0 @@ -package nostr.controller.impl; - -import lombok.Getter; -import lombok.NonNull; -import lombok.extern.java.Log; -import nostr.base.Command; -import nostr.base.annotation.CustomHandler; -import nostr.base.annotation.DefaultHandler; -import nostr.command.CommandHandler; -import nostr.context.Context; -import nostr.context.impl.DefaultCommandContext; -import nostr.controller.ApplicationController; -import nostr.util.thread.ThreadUtil; - -import java.util.Optional; -import java.util.ServiceLoader; -import java.util.concurrent.TimeoutException; - -@Getter -@Log -public class ApplicationControllerImpl implements ApplicationController { - - private final String command; - - public ApplicationControllerImpl(@NonNull String command) { - this.command = command; - } - - @Override - public void initialize() { - } - - @Override - public void handleRequest(Context requestContext) { - requestContext.validate(); - if (requestContext instanceof DefaultCommandContext defaultCommandContext) { - try { - ThreadUtil.builder().blocking(true).task(this).build().run(defaultCommandContext); - } catch (TimeoutException e) { - throw new RuntimeException(e); - } - } - } - - @Override - public Void execute(@NonNull Context context) { - if (context instanceof DefaultCommandContext commandContext) { - executeCommand(commandContext); - } - return null; - } - - private void executeCommand(@NonNull DefaultCommandContext defaultCommandContext) { - - var commandHandler = getCommandHandler(this.command); - - log.fine("Executing command: " + this.command); - commandHandler.handle(defaultCommandContext); - } - - private static CommandHandler getCommandHandler(@NonNull String command) { - ServiceLoader loader = ServiceLoader.load(CommandHandler.class); - - Optional customHandler = loader.stream() - .map(ServiceLoader.Provider::get) - .filter(ch -> ch.getClass().isAnnotationPresent(CustomHandler.class) && ch.getClass().getAnnotation(CustomHandler.class).command().equals(Command.valueOf(command.toUpperCase()))) - .findFirst(); - - if (customHandler.isPresent()) { - return customHandler.get(); - } - - Optional defaultHandler = loader.stream() - .map(ServiceLoader.Provider::get) - .filter(ch -> ch.getClass().isAnnotationPresent(DefaultHandler.class) && ch.getClass().getAnnotation(DefaultHandler.class).command().equals(Command.valueOf(command.toUpperCase()))) - .findFirst(); - - if (defaultHandler.isPresent()) { - return defaultHandler.get(); - } - - throw new AssertionError("Could not load the default handler"); - } -} diff --git a/nostr-java-crypto/pom.xml b/nostr-java-crypto/pom.xml index bade534fb..ce4557a33 100644 --- a/nostr-java-crypto/pom.xml +++ b/nostr-java-crypto/pom.xml @@ -6,7 +6,7 @@ xyz.tcheeric nostr-java - 0.6.3-SNAPSHOT + 0.6.4-SNAPSHOT nostr-java-crypto @@ -37,4 +37,4 @@ ${project.version} - \ No newline at end of file + diff --git a/nostr-java-encryption-nip04/pom.xml b/nostr-java-encryption-nip04/pom.xml index c2dcce47a..ced97d6bf 100644 --- a/nostr-java-encryption-nip04/pom.xml +++ b/nostr-java-encryption-nip04/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric nostr-java - 0.6.3-SNAPSHOT + 0.6.4-SNAPSHOT nostr-java-encryption-nip04 diff --git a/nostr-java-encryption-nip44/pom.xml b/nostr-java-encryption-nip44/pom.xml index aa83e53f4..bb95d4948 100644 --- a/nostr-java-encryption-nip44/pom.xml +++ b/nostr-java-encryption-nip44/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric nostr-java - 0.6.3-SNAPSHOT + 0.6.4-SNAPSHOT nostr-java-encryption-nip44 diff --git a/nostr-java-encryption/pom.xml b/nostr-java-encryption/pom.xml index a236f3f18..169f58443 100644 --- a/nostr-java-encryption/pom.xml +++ b/nostr-java-encryption/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric nostr-java - 0.6.3-SNAPSHOT + 0.6.4-SNAPSHOT nostr-java-encryption diff --git a/nostr-java-event/pom.xml b/nostr-java-event/pom.xml index 6e8893ef4..8a7b7c9c5 100644 --- a/nostr-java-event/pom.xml +++ b/nostr-java-event/pom.xml @@ -6,7 +6,7 @@ xyz.tcheeric nostr-java - 0.6.3-SNAPSHOT + 0.6.4-SNAPSHOT nostr-java-event diff --git a/nostr-java-event/src/main/java/module-info.java b/nostr-java-event/src/main/java/module-info.java index 607f39c9e..60e80d4b3 100644 --- a/nostr-java-event/src/main/java/module-info.java +++ b/nostr-java-event/src/main/java/module-info.java @@ -18,4 +18,5 @@ exports nostr.event.json.serializer; exports nostr.event.tag; exports nostr.event.util; + exports nostr.event.filter; } diff --git a/nostr-java-event/src/main/java/nostr/event/Kind.java b/nostr-java-event/src/main/java/nostr/event/Kind.java index 25fae61e5..0f34b990b 100644 --- a/nostr-java-event/src/main/java/nostr/event/Kind.java +++ b/nostr-java-event/src/main/java/nostr/event/Kind.java @@ -29,21 +29,25 @@ public enum Kind { MUTE_USER(44, "mute_user"), ENCRYPTED_PAYLOADS(44, "encrypted_payloads"), OTS_EVENT(1040, "ots_event"), + RESERVED_CASHU_WALLET_TOKENS(7_374, "reserved_cashu_wallet_tokens"), + WALLET_UNSPENT_PROOF(7_375, "wallet_unspent_proof"), + WALLET_TX_HISTORY(7_376, "wallet_tx_history"), ZAP_REQUEST(9734, "zap_request"), ZAP_RECEIPT(9735, "zap_receipt"), REPLACEABLE_EVENT(10_000, "replaceable_event"), + PIN_LIST(10_001, "pin_list"), EPHEMEREAL_EVENT(20_000, "ephemereal_event"), CLIENT_AUTH(22_242, "authentication_of_clients_to_relays"), + STALL_CREATE_OR_UPDATE(30_017, "create_or_update_stall"), PRODUCT_CREATE_OR_UPDATE(30_018, "create_or_update_product"), + PRE_LONG_FORM_CONTENT(30_023, "long_form_content"), CLASSIFIED_LISTING(30_402, "classified_listing_active"), CLASSIFIED_LISTING_INACTIVE(30_403, "classified_listing_inactive"), CLASSIFIED_LISTING_DRAFT(30_403, "classified_listing_draft"), CALENDAR_DATE_BASED_EVENT(31_922, "calendar_date_based_event"), CALENDAR_TIME_BASED_EVENT(31_923, "calendar_time_based_event"), - WALLET(37_375, "wallet"), - WALLET_UNSPENT_PROOF(7_375, "wallet_unspent_proof"), - WALLET_TX_HISTORY(7_376, "wallet_tx_history"), - UNDEFINED(-1, "undefined"); + CALENDAR_RSVP_EVENT(31_925, "calendar_rsvp_event"), + WALLET(37_375, "wallet"); @JsonValue private final int value; @@ -52,7 +56,7 @@ public enum Kind { @JsonCreator public static Kind valueOf(int value) { - if (!ValueRange.of(0, 65535).isValidIntValue(value)) { + if (!ValueRange.of(0, 65_535).isValidIntValue(value)) { throw new IllegalArgumentException(String.format("Kind must be between 0 and 65535 but was [%d]", value)); } for (Kind k : values()) { @@ -61,7 +65,7 @@ public static Kind valueOf(int value) { } } - return UNDEFINED; + return TEXT_NOTE; } @Override diff --git a/nostr-java-event/src/main/java/nostr/event/filter/AddressableTagFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/AddressableTagFilter.java new file mode 100644 index 000000000..b6181320b --- /dev/null +++ b/nostr-java-event/src/main/java/nostr/event/filter/AddressableTagFilter.java @@ -0,0 +1,79 @@ +package nostr.event.filter; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.EqualsAndHashCode; +import lombok.NonNull; +import nostr.base.PublicKey; +import nostr.event.impl.GenericEvent; +import nostr.event.tag.AddressTag; +import nostr.event.tag.IdentifierTag; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@EqualsAndHashCode +public class AddressableTagFilter implements Filterable { + public final static String filterKey = "#a"; + private final T addressableTag; + + public AddressableTagFilter(T addressableTag) { + this.addressableTag = addressableTag; + } + + @Override + public Predicate getPredicate() { + return this::compare; + } + + @Override + public T getFilterCriterion() { + return addressableTag; + } + + @Override + public String getFilterKey() { + return filterKey; + } + + public static AddressTag createAddressTag(@NonNull JsonNode addressableTag) throws IllegalArgumentException { + try { + List list = Arrays.stream(addressableTag.asText().split(":")).toList(); + + AddressTag addressTag = new AddressTag(); + addressTag.setKind(Integer.valueOf(list.getFirst())); + addressTag.setPublicKey(new PublicKey(list.get(1))); + addressTag.setIdentifierTag(new IdentifierTag(list.get(2))); + + return addressTag; + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + String.format("Malformed JsonNode addressable tag: [%s]", addressableTag.asText()), e); + } + } + + @Override + public String getFilterableValue() { + Integer kind = addressableTag.getKind(); + String hexString = addressableTag.getPublicKey().toHexString(); + String id = addressableTag.getIdentifierTag().getId(); + + return Stream.of(kind, hexString, id) + .map(Object::toString) + .collect(Collectors.joining(":")); + } + + private boolean compare(@NonNull GenericEvent genericEvent) { + return + !genericEvent.getPubKey().toHexString().equals( + this.addressableTag.getPublicKey().toHexString()) || + !genericEvent.getKind().equals( + this.addressableTag.getKind()) || + getTypeSpecificTags(IdentifierTag.class, genericEvent).stream() + .anyMatch(identifierTag -> + identifierTag.getId().equals( + this.addressableTag.getIdentifierTag().getId())); + } +} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/AuthorFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/AuthorFilter.java new file mode 100644 index 000000000..dfd0d185a --- /dev/null +++ b/nostr-java-event/src/main/java/nostr/event/filter/AuthorFilter.java @@ -0,0 +1,38 @@ +package nostr.event.filter; + +import lombok.EqualsAndHashCode; +import nostr.base.PublicKey; +import nostr.event.impl.GenericEvent; + +import java.util.function.Predicate; + +@EqualsAndHashCode +public class AuthorFilter implements Filterable { + public final static String filterKey = "authors"; + private final T publicKey; + + public AuthorFilter(T publicKey) { + this.publicKey = publicKey; + } + + @Override + public Predicate getPredicate() { + return (genericEvent) -> + this.publicKey.toHexString().equals(genericEvent.getPubKey().toHexString()); + } + + @Override + public T getFilterCriterion() { + return publicKey; + } + + @Override + public String getFilterKey() { + return filterKey; + } + + @Override + public String getFilterableValue() { + return publicKey.toHexString(); + } +} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/EventFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/EventFilter.java new file mode 100644 index 000000000..c6fae8a50 --- /dev/null +++ b/nostr-java-event/src/main/java/nostr/event/filter/EventFilter.java @@ -0,0 +1,37 @@ +package nostr.event.filter; + +import lombok.EqualsAndHashCode; +import nostr.event.impl.GenericEvent; + +import java.util.function.Predicate; + +@EqualsAndHashCode +public class EventFilter implements Filterable { + public final static String filterKey = "ids"; + private final T event; + + public EventFilter(T event) { + this.event = event; + } + + @Override + public Predicate getPredicate() { + return (genericEvent) -> + this.event.getId().equals(genericEvent.getId()); + } + + @Override + public T getFilterCriterion() { + return event; + } + + @Override + public String getFilterKey() { + return filterKey; + } + + @Override + public String getFilterableValue() { + return event.getId(); + } +} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/Filterable.java b/nostr-java-event/src/main/java/nostr/event/filter/Filterable.java new file mode 100644 index 000000000..002ded0ba --- /dev/null +++ b/nostr-java-event/src/main/java/nostr/event/filter/Filterable.java @@ -0,0 +1,45 @@ +package nostr.event.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import nostr.event.BaseTag; +import nostr.event.impl.GenericEvent; + +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +public interface Filterable { + ObjectMapper mapper = new ObjectMapper(); + + Predicate getPredicate(); + T getFilterCriterion(); + Object getFilterableValue(); + String getFilterKey(); + + default List getTypeSpecificTags(Class tagClass, GenericEvent event) { + return event.getTags().stream() + .filter(tagClass::isInstance) + .map(tagClass::cast) + .toList(); + } + + default ObjectNode toObjectNode(ObjectNode objectNode) { + ArrayNode arrayNode = mapper.createArrayNode(); + + Optional.ofNullable(objectNode.get(getFilterKey())) + .ifPresent(jsonNode -> + jsonNode.elements().forEachRemaining(arrayNode::add)); + + addToArrayNode(arrayNode); + + return objectNode.set(getFilterKey(), arrayNode); + } + + default void addToArrayNode(ArrayNode arrayNode) { + arrayNode.addAll( + mapper.createArrayNode().add( + getFilterableValue().toString())); + } +} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/Filters.java b/nostr-java-event/src/main/java/nostr/event/filter/Filters.java new file mode 100644 index 000000000..65e1a12e5 --- /dev/null +++ b/nostr-java-event/src/main/java/nostr/event/filter/Filters.java @@ -0,0 +1,57 @@ +package nostr.event.filter; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static java.util.stream.Collectors.groupingBy; + +@EqualsAndHashCode +public class Filters { + public static final int DEFAULT_FILTERS_LIMIT = 10; + @Getter + private final Map> filtersMap; + + @Getter + @Setter + private Integer limit = DEFAULT_FILTERS_LIMIT; + + public Filters(@NonNull Filterable... filterablesByDefaultType) { + this(List.of(filterablesByDefaultType)); + } + + public Filters(@NonNull List filterablesByDefaultType) { + this(filterablesByDefaultType.stream().collect(groupingBy(Filterable::getFilterKey))); + } + + private Filters(@NonNull Map> filterablesByCustomType) { + validateFiltersMap(filterablesByCustomType); + this.filtersMap = filterablesByCustomType; + } + + public List getFilterByType(@NonNull String type) { + return filtersMap.get(type); + } + + private static void validateFiltersMap(Map> filtersMap) throws IllegalArgumentException { + if (filtersMap.isEmpty()) { + throw new IllegalArgumentException("Filters cannot be empty."); + } + + filtersMap.values().forEach(filterables -> { + if (filterables.isEmpty()) { + throw new IllegalArgumentException("Filters cannot be empty."); + } + }); + + filtersMap.forEach((key, value) -> { + if (key.isEmpty()) + throw new IllegalArgumentException(String.format("Filter key for filterable [%s] is not defined", value.getFirst().getFilterKey())); + }); + } +} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/GenericTagQueryFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/GenericTagQueryFilter.java new file mode 100644 index 000000000..324ba6557 --- /dev/null +++ b/nostr-java-event/src/main/java/nostr/event/filter/GenericTagQueryFilter.java @@ -0,0 +1,48 @@ +package nostr.event.filter; + +import lombok.EqualsAndHashCode; +import nostr.base.ElementAttribute; +import nostr.base.GenericTagQuery; +import nostr.event.impl.GenericEvent; +import nostr.event.impl.GenericTag; + +import java.util.HashSet; +import java.util.function.Predicate; + +@EqualsAndHashCode +public class GenericTagQueryFilter implements Filterable { + private final T genericTagQuery; + + public GenericTagQueryFilter(T genericTagQuery) { + this.genericTagQuery = genericTagQuery; + } + + @Override + public Predicate getPredicate() { + return (genericEvent) -> + getTypeSpecificTags(GenericTag.class, genericEvent).stream() + .filter(genericTag -> + genericTag.getCode().equals(this.genericTagQuery.getTagName())) + .anyMatch(genericTag -> + new HashSet<>(genericTag + .getAttributes().stream().map( + ElementAttribute::getValue).toList()) + .contains( + this.genericTagQuery.getValue())); + } + + @Override + public T getFilterCriterion() { + return genericTagQuery; + } + + @Override + public String getFilterKey() { + return genericTagQuery.getTagName(); + } + + @Override + public String getFilterableValue() { + return genericTagQuery.getValue(); + } +} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/IdentifierTagFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/IdentifierTagFilter.java new file mode 100644 index 000000000..6efc2b1ff --- /dev/null +++ b/nostr-java-event/src/main/java/nostr/event/filter/IdentifierTagFilter.java @@ -0,0 +1,39 @@ +package nostr.event.filter; + +import lombok.EqualsAndHashCode; +import nostr.event.impl.GenericEvent; +import nostr.event.tag.IdentifierTag; + +import java.util.function.Predicate; + +@EqualsAndHashCode +public class IdentifierTagFilter implements Filterable { + public final static String filterKey = "#d"; + private final T identifierTag; + + public IdentifierTagFilter(T identifierTag) { + this.identifierTag = identifierTag; + } + + @Override + public Predicate getPredicate() { + return (genericEvent) -> + getTypeSpecificTags(IdentifierTag.class, genericEvent).stream().anyMatch(genericEventIdentifiterTag -> + genericEventIdentifiterTag.getId().equals(this.identifierTag.getId())); + } + + @Override + public T getFilterCriterion() { + return identifierTag; + } + + @Override + public String getFilterKey() { + return filterKey; + } + + @Override + public String getFilterableValue() { + return identifierTag.getId(); + } +} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/KindFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/KindFilter.java new file mode 100644 index 000000000..d018a913d --- /dev/null +++ b/nostr-java-event/src/main/java/nostr/event/filter/KindFilter.java @@ -0,0 +1,46 @@ +package nostr.event.filter; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import lombok.EqualsAndHashCode; +import nostr.event.Kind; +import nostr.event.impl.GenericEvent; + +import java.util.function.Predicate; + +@EqualsAndHashCode +public class KindFilter implements Filterable { + public final static String filterKey = "kinds"; + private final T kind; + + public KindFilter(T kind) { + this.kind = kind; + } + + @Override + public Predicate getPredicate() { + return (genericEvent) -> + genericEvent.getKind().equals(this.kind.getValue()); + } + + @Override + public T getFilterCriterion() { + return kind; + } + + @Override + public void addToArrayNode(ArrayNode arrayNode) { + arrayNode.addAll( + mapper.createArrayNode().add( + getFilterableValue())); + } + + @Override + public String getFilterKey() { + return filterKey; + } + + @Override + public Integer getFilterableValue() { + return kind.getValue(); + } +} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/ReferencedEventFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/ReferencedEventFilter.java new file mode 100644 index 000000000..4ac62b0db --- /dev/null +++ b/nostr-java-event/src/main/java/nostr/event/filter/ReferencedEventFilter.java @@ -0,0 +1,40 @@ +package nostr.event.filter; + +import lombok.EqualsAndHashCode; +import nostr.event.impl.GenericEvent; +import nostr.event.tag.EventTag; + +import java.util.function.Predicate; + +@EqualsAndHashCode +public class ReferencedEventFilter implements Filterable { + public final static String filterKey = "#e"; + private final T referencedEvent; + + public ReferencedEventFilter(T referencedEvent) { + this.referencedEvent = referencedEvent; + } + + @Override + public Predicate getPredicate() { + return (genericEvent) -> + getTypeSpecificTags(EventTag.class, genericEvent).stream() + .anyMatch(eventTag -> + eventTag.getIdEvent().equals(referencedEvent.getId())); + } + + @Override + public T getFilterCriterion() { + return referencedEvent; + } + + @Override + public String getFilterKey() { + return filterKey; + } + + @Override + public String getFilterableValue() { + return referencedEvent.getId(); + } +} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/ReferencedPublicKeyFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/ReferencedPublicKeyFilter.java new file mode 100644 index 000000000..4dc4eab03 --- /dev/null +++ b/nostr-java-event/src/main/java/nostr/event/filter/ReferencedPublicKeyFilter.java @@ -0,0 +1,41 @@ +package nostr.event.filter; + +import lombok.EqualsAndHashCode; +import nostr.base.PublicKey; +import nostr.event.impl.GenericEvent; +import nostr.event.tag.PubKeyTag; + +import java.util.function.Predicate; + +@EqualsAndHashCode +public class ReferencedPublicKeyFilter implements Filterable { + public final static String filterKey = "#p"; + private final T referencedPublicKey; + + public ReferencedPublicKeyFilter(T referencedPublicKey) { + this.referencedPublicKey = referencedPublicKey; + } + + @Override + public Predicate getPredicate() { + return (genericEvent) -> + getTypeSpecificTags(PubKeyTag.class, genericEvent).stream() + .anyMatch(pubKeyTag -> + pubKeyTag.getPublicKey().toHexString().equals(this.referencedPublicKey.toHexString())); + } + + @Override + public T getFilterCriterion() { + return referencedPublicKey; + } + + @Override + public String getFilterKey() { + return filterKey; + } + + @Override + public String getFilterableValue() { + return referencedPublicKey.toHexString(); + } +} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/SinceFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/SinceFilter.java new file mode 100644 index 000000000..fb06f049e --- /dev/null +++ b/nostr-java-event/src/main/java/nostr/event/filter/SinceFilter.java @@ -0,0 +1,43 @@ +package nostr.event.filter; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.EqualsAndHashCode; +import nostr.event.impl.GenericEvent; + +import java.util.function.Predicate; + +@EqualsAndHashCode +public class SinceFilter implements Filterable { + public final static String filterKey = "since"; + private final Long since; + + public SinceFilter(Long since) { + this.since = since; + } + + @Override + public Predicate getPredicate() { + return (genericEvent) -> + this.since < genericEvent.getCreatedAt(); + } + + @Override + public Long getFilterCriterion() { + return since; + } + + @Override + public ObjectNode toObjectNode(ObjectNode objectNode) { + return mapper.createObjectNode().put(filterKey, since); + } + + @Override + public String getFilterKey() { + return filterKey; + } + + @Override + public String getFilterableValue() { + return since.toString(); + } +} diff --git a/nostr-java-event/src/main/java/nostr/event/filter/UntilFilter.java b/nostr-java-event/src/main/java/nostr/event/filter/UntilFilter.java new file mode 100644 index 000000000..54c9e2a99 --- /dev/null +++ b/nostr-java-event/src/main/java/nostr/event/filter/UntilFilter.java @@ -0,0 +1,43 @@ +package nostr.event.filter; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.EqualsAndHashCode; +import nostr.event.impl.GenericEvent; + +import java.util.function.Predicate; + +@EqualsAndHashCode +public class UntilFilter implements Filterable { + public final static String filterKey = "until"; + private final Long until; + + public UntilFilter(Long until) { + this.until = until; + } + + @Override + public Predicate getPredicate() { + return (genericEvent) -> + this.until >= genericEvent.getCreatedAt(); + } + + @Override + public Long getFilterCriterion() { + return until; + } + + @Override + public ObjectNode toObjectNode(ObjectNode objectNode) { + return mapper.createObjectNode().put(filterKey, until); + } + + @Override + public String getFilterKey() { + return filterKey; + } + + @Override + public String getFilterableValue() { + return until.toString(); + } +} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/CalendarRsvpContent.java b/nostr-java-event/src/main/java/nostr/event/impl/CalendarRsvpContent.java new file mode 100644 index 000000000..0c0145a3b --- /dev/null +++ b/nostr-java-event/src/main/java/nostr/event/impl/CalendarRsvpContent.java @@ -0,0 +1,42 @@ +package nostr.event.impl; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NonNull; +import nostr.event.AbstractEventContent; +import nostr.event.impl.CalendarContent.CalendarContentBuilder; +import nostr.event.impl.CalendarRsvpContent.CalendarRsvpContentBuilder; +import nostr.event.tag.AddressTag; +import nostr.event.tag.GeohashTag; +import nostr.event.tag.HashtagTag; +import nostr.event.tag.IdentifierTag; +import nostr.event.tag.PubKeyTag; +import nostr.event.tag.ReferenceTag; + +import java.util.List; + +@Data +@Builder +@JsonDeserialize(builder = CalendarRsvpContentBuilder.class) +@EqualsAndHashCode(callSuper = false) +public class CalendarRsvpContent extends AbstractEventContent { + //@JsonProperty + private final String id; + + // below fields mandatory + private final IdentifierTag identifierTag; + private final AddressTag addressTag; + private final String status; + + // below fields optional + private List participantPubKeys; + + public static CalendarRsvpContentBuilder builder(@NonNull IdentifierTag identifierTag, @NonNull AddressTag addressTag, @NonNull String status) { + return new CalendarRsvpContentBuilder() + .identifierTag(identifierTag) + .addressTag(addressTag) + .status(status); + } +} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/CalendarRsvpEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/CalendarRsvpEvent.java new file mode 100644 index 000000000..6243aa523 --- /dev/null +++ b/nostr-java-event/src/main/java/nostr/event/impl/CalendarRsvpEvent.java @@ -0,0 +1,118 @@ +package nostr.event.impl; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.node.ArrayNode; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; +import nostr.base.PublicKey; +import nostr.base.Signature; +import nostr.base.annotation.Event; +import nostr.event.BaseTag; +import nostr.event.Kind; +import nostr.event.NIP52Event; +import nostr.event.impl.CalendarRsvpEvent.CalendarRsvpEventDeserializer; +import nostr.event.impl.CalendarTimeBasedEvent.CalendarTimeBasedEventDeserializer; +import nostr.event.tag.AddressTag; +import nostr.event.tag.IdentifierTag; +import nostr.event.tag.PubKeyTag; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.StreamSupport; + +@EqualsAndHashCode(callSuper = false) +@Event(name = "CalendarRsvpEvent", nip = 52) +@JsonDeserialize(using = CalendarRsvpEventDeserializer.class) +public class CalendarRsvpEvent extends NIP52Event { + @Getter + @JsonIgnore + private CalendarRsvpContent calendarRsvpContent; + + public CalendarRsvpEvent(@NonNull PublicKey sender, @NonNull List baseTags, @NonNull String content, @NonNull CalendarRsvpContent calendarRsvpContent) { + super(sender, Kind.CALENDAR_RSVP_EVENT, baseTags, content); + this.calendarRsvpContent = calendarRsvpContent; + mapCustomTags(); + } + + private void mapCustomTags() { + addStandardTag(calendarRsvpContent.getIdentifierTag()); + addStandardTag(calendarRsvpContent.getAddressTag()); + addGenericTag("status", getNip(), calendarRsvpContent.getStatus()); + addStandardTag(calendarRsvpContent.getParticipantPubKeys()); + } + + public static class CalendarRsvpEventDeserializer extends StdDeserializer { + public CalendarRsvpEventDeserializer() { + super(CalendarRsvpEvent.class); + } + +// TODO: below methods needs comprehensive tags assignment completion + @Override + public CalendarRsvpEvent deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException { + JsonNode calendarTimeBasedEventNode = jsonParser.getCodec().readTree(jsonParser); + ArrayNode tags = (ArrayNode) calendarTimeBasedEventNode.get("tags"); + + List baseTags = StreamSupport.stream( + tags.spliterator(), false).toList().stream() + .map( + JsonNode::elements) + .map(element -> + new ObjectMapper().convertValue(element, BaseTag.class)).toList(); + + List genericTags = baseTags.stream() + .filter(GenericTag.class::isInstance) + .map(GenericTag.class::cast) + .toList(); + + IdentifierTag identifierTag = getBaseTagCastFromString(baseTags, IdentifierTag.class).getFirst(); + + AddressTag addressTag = getBaseTagCastFromString(baseTags, AddressTag.class).getFirst(); + + CalendarRsvpContent calendarRsvpContent = CalendarRsvpContent.builder( + identifierTag, + addressTag, + getTagValueFromString(genericTags, "status")) + .build(); + + calendarRsvpContent.setParticipantPubKeys(getBaseTagCastFromString(baseTags, PubKeyTag.class)); + + Map generalMap = new HashMap<>(); + calendarTimeBasedEventNode.fields().forEachRemaining(generalTag -> + generalMap.put( + generalTag.getKey(), + generalTag.getValue().asText())); + + + CalendarRsvpEvent calendarTimeBasedEvent = new CalendarRsvpEvent( + new PublicKey(generalMap.get("pubkey")), + baseTags, + generalMap.get("content"), + calendarRsvpContent + ); + calendarTimeBasedEvent.setId(generalMap.get("id")); + calendarTimeBasedEvent.setCreatedAt(Long.valueOf(generalMap.get("created_at"))); + calendarTimeBasedEvent.setSignature(Signature.fromString(generalMap.get("sig"))); + + return calendarTimeBasedEvent; + } + + private String getTagValueFromString(List genericTags, String code) { + return genericTags.stream() + .filter(tag -> tag.getCode().equalsIgnoreCase(code)) + .findFirst().get().getAttributes().get(0).getValue().toString(); + } + + private List getBaseTagCastFromString(List baseTags, Class type) { + return baseTags.stream().filter(type::isInstance).map(type::cast).toList(); + } + } +} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/CreateOrUpdateStallEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/CreateOrUpdateStallEvent.java index 752211272..3ab5f9319 100644 --- a/nostr-java-event/src/main/java/nostr/event/impl/CreateOrUpdateStallEvent.java +++ b/nostr-java-event/src/main/java/nostr/event/impl/CreateOrUpdateStallEvent.java @@ -15,6 +15,7 @@ import nostr.base.annotation.Event; import nostr.event.AbstractEventContent; import nostr.event.BaseTag; +import nostr.event.Kind; /** * @@ -26,7 +27,7 @@ public class CreateOrUpdateStallEvent extends NostrMarketplaceEvent { public CreateOrUpdateStallEvent(PublicKey sender, List tags, @NonNull Stall stall) { - super(sender, 30017, tags, stall); + super(sender, Kind.STALL_CREATE_OR_UPDATE.getValue(), tags, stall); } @Getter diff --git a/nostr-java-event/src/main/java/nostr/event/impl/Filters.java b/nostr-java-event/src/main/java/nostr/event/impl/Filters.java deleted file mode 100644 index 4e987ab32..000000000 --- a/nostr-java-event/src/main/java/nostr/event/impl/Filters.java +++ /dev/null @@ -1,94 +0,0 @@ -package nostr.event.impl; - -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonAnySetter; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import lombok.Setter; -import nostr.base.PublicKey; -import nostr.base.annotation.Key; -import nostr.event.Kind; -import nostr.event.json.serializer.CustomIdEventListSerializer; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -/** - * - * @author squirrel - */ -@Builder -@Data -@EqualsAndHashCode(callSuper = false) -@NoArgsConstructor -@AllArgsConstructor -public class Filters { - - @Key - @JsonProperty("ids") - @JsonSerialize(using=CustomIdEventListSerializer.class) - private List events; - - @Key - @JsonProperty("authors") - private List authors; - - @Key - private List kinds; - - @Key - @JsonProperty("#e") - @JsonSerialize(using=CustomIdEventListSerializer.class) - private List referencedEvents; - - @Key - @JsonProperty("#p") - private List referencePubKeys; - - @Key - private Long since; - - @Key - private Long until; - - @Key - private Integer limit; - - @Key(nip = 12) - @Setter(AccessLevel.NONE) - private Map> genericTagQuery; - - public void setUntil(@NonNull Long until) { - if (until < 0) { - throw new IllegalArgumentException("'until' filter cannot be negative."); - } - this.until = until; - } - - public void setSince(@NonNull Long since) { - if (since < 0) { - throw new IllegalArgumentException("'since' filter cannot be negative."); - } - this.since = since; - } - - @JsonAnyGetter - public Map> getGenericTagQuery() { - return genericTagQuery; - } - - @JsonAnySetter - public void setGenericTagQuery(String key, List value) { - this.genericTagQuery = Optional.ofNullable(genericTagQuery).orElse(new HashMap<>()); - this.genericTagQuery.put(key, value); - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/impl/GenericEvent.java b/nostr-java-event/src/main/java/nostr/event/impl/GenericEvent.java index d53764347..e31c423e6 100644 --- a/nostr-java-event/src/main/java/nostr/event/impl/GenericEvent.java +++ b/nostr-java-event/src/main/java/nostr/event/impl/GenericEvent.java @@ -124,7 +124,7 @@ public GenericEvent(@NonNull PublicKey pubKey, @NonNull Kind kind, @NonNull List public GenericEvent(@NonNull PublicKey pubKey, @NonNull Integer kind, @NonNull List tags, @NonNull String content) { this.pubKey = pubKey; - this.kind = kind; + this.kind = Kind.valueOf(kind).getValue(); this.tags = tags; this.content = content; this.attributes = new ArrayList<>(); diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/BaseMessageDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/BaseMessageDecoder.java index 40dadbbb7..385d7f461 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/BaseMessageDecoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/BaseMessageDecoder.java @@ -1,9 +1,10 @@ package nostr.event.json.codec; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.NonNull; -import lombok.SneakyThrows; import nostr.base.IDecoder; import nostr.event.BaseMessage; import nostr.event.impl.GenericMessage; @@ -16,37 +17,66 @@ import nostr.event.message.RelayAuthenticationMessage; import nostr.event.message.ReqMessage; +import java.util.List; import java.util.Map; /** * @author eric */ public class BaseMessageDecoder implements IDecoder { - private final ObjectMapper mapper; + public static final int MAX_JSON_NODE_THRESHOLD = 99; + private final ObjectMapper mapper = new ObjectMapper(); public BaseMessageDecoder() { - mapper = new ObjectMapper(); mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); } - @SneakyThrows @Override - public T decode(@NonNull String jsonString) { - Object[] msgArr = mapper.readValue(jsonString, Object[].class); - final String strCmd = msgArr[0].toString(); - final Object arg = msgArr[1]; + public T decode(@NonNull String jsonString) throws JsonProcessingException { + ValidJsonNodeFirstPair validJsonNodeFirstPair = jsonFirstPair(jsonString); + String command = validJsonNodeFirstPair.formerly_strCmd(); + Object subscriptionId = validJsonNodeFirstPair.formerly_arg(); // subscriptionId - return switch (strCmd) { - case "AUTH" -> arg instanceof Map map ? + Object[] msgArr = mapper.readValue(jsonString, Object[].class); // TODO: replace with jsonNode after ReqMessage.decode() is finished + + return switch (command) { + case "AUTH" -> subscriptionId instanceof Map map ? CanonicalAuthenticationMessage.decode(map, mapper) : - RelayAuthenticationMessage.decode(arg); - case "CLOSE" -> CloseMessage.decode(arg); - case "EOSE" -> EoseMessage.decode(arg); + RelayAuthenticationMessage.decode(subscriptionId); + case "CLOSE" -> CloseMessage.decode(subscriptionId); + case "EOSE" -> EoseMessage.decode(subscriptionId); case "EVENT" -> EventMessage.decode(msgArr, mapper); - case "NOTICE" -> NoticeMessage.decode(arg); + case "NOTICE" -> NoticeMessage.decode(subscriptionId); case "OK" -> OkMessage.decode(msgArr); - case "REQ" -> ReqMessage.decode(msgArr, mapper); + case "REQ" -> { + String filtersJson = jsonSecondPair(jsonString).formerly_msgArr(); // filters + yield ReqMessage.decode(subscriptionId, List.of(filtersJson)); + } default -> GenericMessage.decode(msgArr); }; } + + private ValidJsonNodeFirstPair jsonFirstPair(@NonNull String jsonString) throws JsonProcessingException { + final JsonNode jsonNode = mapper.readTree(jsonString); + + return new ValidJsonNodeFirstPair( + jsonNode.get(0).asText(), + jsonNode.get(1).asText()); + } + + private ValidJsonNodeSecondPair jsonSecondPair(@NonNull String jsonString) throws JsonProcessingException { + final JsonNode jsonNode = mapper.readTree(jsonString); + + return new ValidJsonNodeSecondPair( + jsonNode.get(2).toString()); + } + + private record ValidJsonNodeFirstPair( + @NonNull String formerly_strCmd, + @NonNull Object formerly_arg) { + } + + private record ValidJsonNodeSecondPair( + @NonNull String formerly_msgArr) { + } } diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/FilterableProvider.java b/nostr-java-event/src/main/java/nostr/event/json/codec/FilterableProvider.java new file mode 100644 index 000000000..507d24f6c --- /dev/null +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/FilterableProvider.java @@ -0,0 +1,45 @@ +package nostr.event.json.codec; + +import com.fasterxml.jackson.databind.JsonNode; +import nostr.base.GenericTagQuery; +import nostr.base.PublicKey; +import nostr.event.Kind; +import nostr.event.filter.AddressableTagFilter; +import nostr.event.filter.AuthorFilter; +import nostr.event.filter.EventFilter; +import nostr.event.filter.Filterable; +import nostr.event.filter.GenericTagQueryFilter; +import nostr.event.filter.IdentifierTagFilter; +import nostr.event.filter.KindFilter; +import nostr.event.filter.ReferencedEventFilter; +import nostr.event.filter.ReferencedPublicKeyFilter; +import nostr.event.filter.SinceFilter; +import nostr.event.filter.UntilFilter; +import nostr.event.impl.GenericEvent; +import nostr.event.tag.IdentifierTag; + +import java.util.List; +import java.util.function.Function; +import java.util.stream.StreamSupport; + +class FilterableProvider { + protected static List getFilterable(String type, JsonNode node) { + return switch (type) { + case ReferencedPublicKeyFilter.filterKey -> getFilterable(node, referencedPubKey -> new ReferencedPublicKeyFilter<>(new PublicKey(referencedPubKey.asText()))); + case ReferencedEventFilter.filterKey -> getFilterable(node, referencedEvent -> new ReferencedEventFilter<>(new GenericEvent(referencedEvent.asText()))); + case AddressableTagFilter.filterKey -> getFilterable(node, addressableTag -> new AddressableTagFilter<>(AddressableTagFilter.createAddressTag(addressableTag))); + case IdentifierTagFilter.filterKey -> getFilterable(node, identifierTag -> new IdentifierTagFilter<>(new IdentifierTag(identifierTag.asText()))); + case AuthorFilter.filterKey -> getFilterable(node, author -> new AuthorFilter<>(new PublicKey(author.asText()))); + case EventFilter.filterKey -> getFilterable(node, event -> new EventFilter<>(new GenericEvent(event.asText()))); + case KindFilter.filterKey -> getFilterable(node, kindNode -> new KindFilter<>(Kind.valueOf(kindNode.asInt()))); + case SinceFilter.filterKey -> List.of(new SinceFilter(node.asLong())); + case UntilFilter.filterKey -> List.of(new UntilFilter(node.asLong())); + default -> + getFilterable(node, genericNode -> new GenericTagQueryFilter<>(new GenericTagQuery(type, genericNode.asText()))); + }; + } + + private static List getFilterable(JsonNode jsonNode, Function filterFunction) { + return StreamSupport.stream(jsonNode.spliterator(), false).map(filterFunction).toList(); + } +} diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersDecoder.java index b957d1c00..71659c7c5 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersDecoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersDecoder.java @@ -1,26 +1,32 @@ package nostr.event.json.codec; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.Data; import lombok.NonNull; -import nostr.event.impl.Filters; +import lombok.SneakyThrows; +import nostr.event.filter.Filterable; +import nostr.event.filter.Filters; + +import java.util.ArrayList; +import java.util.List; /** - * * @author eric */ @Data public class FiltersDecoder implements FDecoder { - private final Class clazz = (Class)Filters.class; + private final static ObjectMapper mapper = new ObjectMapper(); + + @SneakyThrows + public T decode(@NonNull String jsonFiltersList) { + final List filterables = new ArrayList<>(); + + mapper.readTree(jsonFiltersList).fields().forEachRemaining(field -> + filterables.addAll( + FilterableProvider.getFilterable( + field.getKey(), + field.getValue()))); - @Override - public T decode(@NonNull String jsonString) { - try { - ObjectMapper mapper = new ObjectMapper(); - return mapper.readValue(jsonString, clazz); - } catch (JsonProcessingException ex) { - throw new RuntimeException(ex); - } - } + return (T) new Filters(filterables); + } } diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersEncoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersEncoder.java index 73f620606..5675cd752 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersEncoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersEncoder.java @@ -1,22 +1,11 @@ package nostr.event.json.codec; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Data; import lombok.EqualsAndHashCode; import nostr.base.FEncoder; -import nostr.event.impl.Filters; -import nostr.util.NostrException; +import nostr.event.filter.Filters; -import java.util.Spliterator; -import java.util.Spliterators; -import java.util.stream.StreamSupport; - -/** - * @author guilhermegps - */ @Data @EqualsAndHashCode(callSuper = false) public class FiltersEncoder implements FEncoder { @@ -26,45 +15,21 @@ public FiltersEncoder(Filters filters) { this.filters = filters; } - protected String toJson() throws NostrException { - try { - JsonNode node = MAPPER.valueToTree(filters); - ObjectNode objNode = (ObjectNode) node; - //var arrayNode = (ArrayNode) node.get("genericTagQuery"); - if (objNode != null && !objNode.isNull()) { - for (JsonNode jn : objNode) { - StreamSupport.stream( - Spliterators.spliteratorUnknownSize(jn.fields(), Spliterator.ORDERED), false) - .forEach(f -> { - if ("genericTagQuery".equals(f.getKey())) { - var mapper = new ObjectMapper(); - try { - mapper.readTree(f.getValue().toString()) - .fields() - .forEachRemaining(g -> objNode.set(g.getKey(), g.getValue())); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } else { - objNode.set(f.getKey(), f.getValue()); - } - }); - } - } - objNode.remove("genericTagQuery"); - - return MAPPER.writeValueAsString(objNode); - } catch (JsonProcessingException | IllegalArgumentException e) { - throw new NostrException(e); - } - } - @Override public String encode() { - try { - return toJson(); - } catch (NostrException ex) { - throw new RuntimeException(ex); - } + ObjectNode root = MAPPER.createObjectNode(); + + filters.getFiltersMap().forEach((key, filterableList) -> { + final ObjectNode objectNode = MAPPER.createObjectNode(); + root.setAll( + filterableList + .stream() + .map(filterable -> + filterable.toObjectNode(objectNode)) + .toList() + .getFirst()); + }); + + return root.toString(); } } diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersListEncoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersListEncoder.java deleted file mode 100644 index 8a43f46d2..000000000 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/FiltersListEncoder.java +++ /dev/null @@ -1,58 +0,0 @@ -package nostr.event.json.codec; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.Data; -import nostr.base.FEncoder; -import nostr.event.impl.Filters; -import nostr.util.NostrException; - -import java.util.List; - -@Data -public class FiltersListEncoder implements FEncoder { - - private final List filtersList; - - public FiltersListEncoder(List filtersList) { - this.filtersList = filtersList; - } - - @Override - public String encode() { - try { - return toJson(); - } catch (NostrException ex) { - throw new RuntimeException(ex); - } - } - - protected String toJson() throws NostrException { - return toJsonCommaSeparated(); - } - - private String toJsonArray() throws NostrException { - try { - StringBuilder sb = new StringBuilder(); - for (Object filter : getFiltersList()) { - if (!sb.isEmpty()) { - sb.append(","); - } - sb.append(MAPPER.writeValueAsString(filter)); - } - return sb.toString(); - } catch (JsonProcessingException | IllegalArgumentException e) { - throw new NostrException(e); - } - } - - private String toJsonCommaSeparated() throws NostrException { - JsonNode node = MAPPER.valueToTree(getFiltersList()); - try { - return MAPPER.writeValueAsString(node); - } catch (JsonProcessingException e) { - throw new NostrException(e); - } - - } -} diff --git a/nostr-java-event/src/main/java/nostr/event/json/codec/GenericEventDecoder.java b/nostr-java-event/src/main/java/nostr/event/json/codec/GenericEventDecoder.java index b97ffeca4..533d61e19 100644 --- a/nostr-java-event/src/main/java/nostr/event/json/codec/GenericEventDecoder.java +++ b/nostr-java-event/src/main/java/nostr/event/json/codec/GenericEventDecoder.java @@ -25,13 +25,9 @@ public GenericEventDecoder(Class clazz) { } @Override - public T decode(String jsonEvent) { - try { - var mapper = new ObjectMapper(); - mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); - return mapper.readValue(jsonEvent, clazz); - } catch (JsonProcessingException ex) { - throw new RuntimeException(ex); - } + public T decode(String jsonEvent) throws JsonProcessingException { + var mapper = new ObjectMapper(); + mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); + return mapper.readValue(jsonEvent, clazz); } } diff --git a/nostr-java-event/src/main/java/nostr/event/message/EventMessage.java b/nostr-java-event/src/main/java/nostr/event/message/EventMessage.java index 9c81a7fd2..7caf29d7d 100644 --- a/nostr-java-event/src/main/java/nostr/event/message/EventMessage.java +++ b/nostr-java-event/src/main/java/nostr/event/message/EventMessage.java @@ -51,15 +51,25 @@ public String encode() throws JsonProcessingException { public static T decode(@NonNull Object[] msgArr, ObjectMapper mapper) { var arg = msgArr[1]; if (msgArr.length == 2 && arg instanceof Map map) { - var event = mapper.convertValue(map, new TypeReference() {}); - return (T) new EventMessage(event); - } else if (msgArr.length == 3 && arg instanceof String) { - var subId = arg.toString(); + return (T) new EventMessage( + convertValue(mapper, map) + ); + } + + if (msgArr.length == 3 && arg instanceof String) { if (msgArr[2] instanceof Map map) { - var event = mapper.convertValue(map, new TypeReference() {}); - return (T) new EventMessage(event, subId); + return (T) new EventMessage( + convertValue(mapper, map), + arg.toString() + ); } } + throw new AssertionError("Invalid argument: " + arg); } + + private static GenericEvent convertValue(ObjectMapper mapper, Map map) { + return mapper.convertValue(map, new TypeReference() { + }); + } } diff --git a/nostr-java-event/src/main/java/nostr/event/message/ReqMessage.java b/nostr-java-event/src/main/java/nostr/event/message/ReqMessage.java index 17fadf42d..7e6a9c547 100644 --- a/nostr-java-event/src/main/java/nostr/event/message/ReqMessage.java +++ b/nostr-java-event/src/main/java/nostr/event/message/ReqMessage.java @@ -2,8 +2,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.JsonNode; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NonNull; @@ -11,66 +10,74 @@ import nostr.base.Command; import nostr.base.IEncoder; import nostr.event.BaseMessage; -import nostr.event.impl.Filters; +import nostr.event.filter.Filters; +import nostr.event.json.codec.FiltersDecoder; import nostr.event.json.codec.FiltersEncoder; import java.time.temporal.ValueRange; -import java.util.ArrayList; import java.util.List; /** - * * @author squirrel */ @Getter @EqualsAndHashCode(callSuper = false) @ToString(callSuper = true) public class ReqMessage extends BaseMessage { - @JsonProperty private final String subscriptionId; @JsonProperty private final List filtersList; - public ReqMessage(@NonNull String subscriptionId, Filters filters) { - this(subscriptionId, List.of(filters)); + public ReqMessage(@NonNull String subscriptionId, Filters... filtersList) { +// TODO: complete logic for a list of filters + this(subscriptionId, List.of(filtersList)); } - public ReqMessage(@NonNull String subscriptionId, List incomingFiltersList) { + public ReqMessage(@NonNull String subscriptionId, List filtersList) { super(Command.REQ.name()); - if (!ValueRange.of(1, 64).isValidIntValue(subscriptionId.length())) { - throw new IllegalArgumentException(String.format("subscriptionId length must be between 1 and 64 characters but was [%d]", subscriptionId.length())); - } + validateSubscriptionId(subscriptionId); this.subscriptionId = subscriptionId; - this.filtersList = new ArrayList<>(); - this.filtersList.addAll(incomingFiltersList); + this.filtersList = filtersList; } @Override public String encode() throws JsonProcessingException { getArrayNode() - .add(getCommand()) - .add(getSubscriptionId()); - List localFiltersList = getFiltersList(); - for (Filters f : localFiltersList) { - try { - FiltersEncoder filtersEncoder = new FiltersEncoder(f); - var filterNode = IEncoder.MAPPER.readTree(filtersEncoder.encode()); - getArrayNode().add(filterNode); - } catch (Exception e) { - throw new RuntimeException(e); - } - } + .add(getCommand()) + .add(getSubscriptionId()); + + filtersList.stream() + .map(FiltersEncoder::new) + .map(FiltersEncoder::encode) + .map(ReqMessage::createJsonNode) + .forEach(jsonNode -> + getArrayNode().add(jsonNode)); + return IEncoder.MAPPER.writeValueAsString(getArrayNode()); } - public static T decode(@NonNull Object[] msgArr, ObjectMapper mapper) { - var len = msgArr.length - 2; - var filtersArr = new Object[len]; - System.arraycopy(msgArr, 2, filtersArr, 0, len); - var filtersList = mapper.convertValue(filtersArr, new TypeReference>() { - }); - return (T) new ReqMessage(msgArr[1].toString(), filtersList); + public static T decode(@NonNull Object subscriptionId, @NonNull List jsonFiltersList) { + validateSubscriptionId(subscriptionId.toString()); + ReqMessage reqMessage = new ReqMessage( + subscriptionId.toString(), + jsonFiltersList.stream().map(filtersList -> + new FiltersDecoder<>().decode(filtersList)).toList()); + return (T) reqMessage; + } + + private static void validateSubscriptionId(String subscriptionId) { + if (!ValueRange.of(1, 64).isValidIntValue(subscriptionId.length())) { + throw new IllegalArgumentException(String.format("SubscriptionId length must be between 1 and 64 characters but was [%d]", subscriptionId.length())); + } + } + + private static JsonNode createJsonNode(String jsonNode) { + try { + return IEncoder.MAPPER.readTree(jsonNode); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException(String.format("Malformed encoding ReqMessage json: [%s]", jsonNode), e); + } } } diff --git a/nostr-java-examples/pom.xml b/nostr-java-examples/pom.xml index a3a9b808f..ad1dfcf4d 100644 --- a/nostr-java-examples/pom.xml +++ b/nostr-java-examples/pom.xml @@ -6,7 +6,7 @@ xyz.tcheeric nostr-java - 0.6.3-SNAPSHOT + 0.6.4-SNAPSHOT nostr-java-examples diff --git a/nostr-java-examples/src/main/java/nostr/examples/NostrApiExamples.java b/nostr-java-examples/src/main/java/nostr/examples/NostrApiExamples.java index 0a3117e6a..b2316ca45 100644 --- a/nostr-java-examples/src/main/java/nostr/examples/NostrApiExamples.java +++ b/nostr-java-examples/src/main/java/nostr/examples/NostrApiExamples.java @@ -15,12 +15,15 @@ import nostr.event.BaseTag; import nostr.event.Kind; import nostr.event.Reaction; +import nostr.event.filter.AuthorFilter; +import nostr.event.filter.Filters; +import nostr.event.filter.KindFilter; +import nostr.event.filter.SinceFilter; import nostr.event.impl.ChannelCreateEvent; import nostr.event.impl.ChannelMessageEvent; import nostr.event.impl.DeletionEvent; import nostr.event.impl.DirectMessageEvent; import nostr.event.impl.EphemeralEvent; -import nostr.event.impl.Filters; import nostr.event.impl.GenericEvent; import nostr.event.impl.HideMessageEvent; import nostr.event.impl.InternetIdentifierMetadataEvent; @@ -243,7 +246,7 @@ private static void ephemerealEvent() { logHeader("ephemeralEvent"); var nip01 = new NIP01(SENDER); - nip01.createEphemeralEvent(21000, "An ephemeral event") + nip01.createEphemeralEvent(Kind.EPHEMEREAL_EVENT.getValue(), "An ephemeral event") .sign() .send(RELAYS); } @@ -260,7 +263,7 @@ private static void reactionEvent() { var reactionEvent = nip25.createReactionEvent(event.getEvent(), Reaction.LIKE); reactionEvent.signAndSend(RELAYS); nip25.createReactionEvent(event.getEvent(), "💩").signAndSend(); -// Using Custom Emoji as reaction +// Using Custom Emoji as reaction nip25.createReactionEvent( event.getEvent(), NIP30.createCustomEmojiTag( @@ -276,7 +279,7 @@ private static void replaceableEvent() { var event = nip01.createTextNoteEvent("Hello Astral, Please replace me!"); event.signAndSend(RELAYS); - nip01.createReplaceableEvent(List.of(new EventTag(event.getEvent().getId())), 15_000, "New content").signAndSend(); + nip01.createReplaceableEvent(List.of(new EventTag(event.getEvent().getId())), Kind.REPLACEABLE_EVENT.getValue(), "New content").signAndSend(); } private static void internetIdMetadata() { @@ -296,20 +299,18 @@ private static void internetIdMetadata() { public static void filters() throws InterruptedException { logHeader("filters"); - var kinds = List.of(Kind.EPHEMEREAL_EVENT, Kind.TEXT_NOTE); - var authors = new ArrayList<>(List.of(new PublicKey("21ef0d8541375ae4bca85285097fba370f7e540b5a30e5e75670c16679f9d144"))); - var date = Calendar.getInstance(); var subId = "subId" + date.getTimeInMillis(); date.add(Calendar.DAY_OF_MONTH, -5); - Filters filters = Filters.builder() - .authors(authors) - .kinds(kinds) - .since(date.getTimeInMillis()/1000) - .build(); var nip01 = NIP01.getInstance(); - nip01.setRelays(RELAYS).send(filters, subId); + nip01.setRelays(RELAYS).sendRequest( + new Filters( + new KindFilter<>(Kind.EPHEMEREAL_EVENT), + new KindFilter<>(Kind.TEXT_NOTE), + new AuthorFilter<>(new PublicKey("21ef0d8541375ae4bca85285097fba370f7e540b5a30e5e75670c16679f9d144")), + new SinceFilter(date.getTimeInMillis()/1000)), subId); + Thread.sleep(5000); } diff --git a/nostr-java-id/pom.xml b/nostr-java-id/pom.xml index f31c7bd08..3b9796f47 100644 --- a/nostr-java-id/pom.xml +++ b/nostr-java-id/pom.xml @@ -6,7 +6,7 @@ xyz.tcheeric nostr-java - 0.6.3-SNAPSHOT + 0.6.4-SNAPSHOT nostr-java-id diff --git a/nostr-java-test/pom.xml b/nostr-java-test/pom.xml index 15e049822..25ff07bd8 100644 --- a/nostr-java-test/pom.xml +++ b/nostr-java-test/pom.xml @@ -6,7 +6,7 @@ xyz.tcheeric nostr-java - 0.6.3-SNAPSHOT + 0.6.4-SNAPSHOT nostr-java-test @@ -58,13 +58,6 @@ ${project.version} test - - ${project.groupId} - nostr-java-command-provider - ${project.version} - test - - org.springframework @@ -193,4 +186,4 @@ - \ No newline at end of file + diff --git a/nostr-java-test/src/main/java/nostr/test/EntityFactory.java b/nostr-java-test/src/main/java/nostr/test/EntityFactory.java index 67cfca4d6..6afb38149 100644 --- a/nostr-java-test/src/main/java/nostr/test/EntityFactory.java +++ b/nostr-java-test/src/main/java/nostr/test/EntityFactory.java @@ -9,9 +9,9 @@ import nostr.event.BaseTag; import nostr.event.Kind; import nostr.event.Reaction; +import nostr.event.filter.Filters; import nostr.event.impl.DirectMessageEvent; import nostr.event.impl.EphemeralEvent; -import nostr.event.impl.Filters; import nostr.event.impl.GenericEvent; import nostr.event.impl.GenericTag; import nostr.event.impl.InternetIdentifierMetadataEvent; @@ -29,7 +29,6 @@ import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Random; /** @@ -59,10 +58,6 @@ public static DirectMessageEvent createDirectMessageEvent(PublicKey senderPublic return event; } - public static Filters createFilters(List authors, List kindList, Long since) { - return Filters.builder().authors(authors).kinds(kindList).since(since).build(); - } - public static InternetIdentifierMetadataEvent createInternetIdentifierMetadataEvent(UserProfile profile) { final PublicKey publicKey = profile.getPublicKey(); InternetIdentifierMetadataEvent event = new InternetIdentifierMetadataEvent(publicKey, profile); @@ -139,26 +134,7 @@ public static GenericTag createGenericTag(PublicKey publicKey, IEvent event, Int return tag; } - public static Filters createFilters(PublicKey publicKey) { - List eventList = new ArrayList<>(); - eventList.add(createTextNoteEvent(publicKey)); - eventList.add(createEphemeralEvent(publicKey)); - - List refEvents = new ArrayList<>(); - refEvents.add(createTextNoteEvent(publicKey)); - - GenericTagQuery genericTagQuery = createGenericTagQuery(); - return Filters.builder() - .events(eventList) - .referencedEvents(refEvents) - .genericTagQuery( - Map.of( - genericTagQuery.getTagName(), - genericTagQuery.getValue())) - .build(); - } - - public static GenericTagQuery createGenericTagQuery() { + public static List createGenericTagQuery() { Character c = generateRamdomAlpha(1).charAt(0); String v1 = generateRamdomAlpha(5); String v2 = generateRamdomAlpha(6); @@ -169,10 +145,12 @@ public static GenericTagQuery createGenericTagQuery() { list.add(v2); list.add(v1); - var result = new GenericTagQuery(); - result.setTagName(c.toString()); - result.setValue(list); - return result; + return list.stream().map(item -> { + var result = new GenericTagQuery(); + result.setTagName(c.toString()); + result.setValue(item); + return result; + }).toList(); } } diff --git a/nostr-java-test/src/test/java/nostr/test/client/ClientTest.java b/nostr-java-test/src/test/java/nostr/test/client/ClientTest.java deleted file mode 100644 index 3fdc5a491..000000000 --- a/nostr-java-test/src/test/java/nostr/test/client/ClientTest.java +++ /dev/null @@ -1,145 +0,0 @@ -package nostr.test.client; - -import lombok.extern.java.Log; -import nostr.base.PrivateKey; -import nostr.client.Client; -import nostr.context.impl.DefaultRequestContext; -import nostr.event.BaseMessage; -import nostr.event.message.EventMessage; -import nostr.id.Identity; -import nostr.test.EntityFactory; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; -import java.util.Set; -import java.util.concurrent.TimeoutException; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * @author squirrel - */ -@Log -class ClientTest { - - private Client client; - private Identity identity; - - public ClientTest() { - } - - @BeforeEach - public void init() { - log.info("init"); - identity = Identity.create(PrivateKey.generateRandomPrivKey()); - - DefaultRequestContext requestContext = new DefaultRequestContext(); - requestContext.setPrivateKey(identity.getPrivateKey().getRawData()); - //requestContext.setRelays(Map.of("My local test relay", "ws://localhost:5555")); - Properties loadedProperties = getRelayProperties(); - requestContext.setRelays(convertPropertiesToMap(loadedProperties)); - try { - client = Client.getInstance().connect(requestContext); - } catch (TimeoutException e) { - throw new RuntimeException(e); - } - } - - //@AfterEach - public void dispose() { - log.info("dispose"); - try { - this.client.disconnect(); - } catch (TimeoutException e) { - throw new RuntimeException(e); - } - this.client = null; - this.identity = null; - } - - @Test - public void testSend() { - log.info("testSend"); - var event = EntityFactory.Events.createTextNoteEvent(identity.getPublicKey()); - identity.sign(event); - BaseMessage msg = new EventMessage(event); - assertDoesNotThrow(() -> { - client.send(msg); - }); - assertTrue(true); - } - - @Test - public void disconnect() { - log.info("disconnect"); - - int relayCount = getRelayCount(); - assertEquals(relayCount, client.getOpenConnectionsCount()); - assertDoesNotThrow(() -> { - client.disconnect(); - }); - assertEquals(0, client.getOpenConnectionsCount()); - - assertDoesNotThrow(() -> { - client.disconnect(); // if all connections are closed, trying to disconnect again wont throw error - }); - } - -/* - @Test - public void testNip42() { - System.out.println("testNip42"); - - var rcpt = Identity.generateRandomIdentity().getPublicKey(); - var sender = identity.getPublicKey(); - var event = EntityFactory.Events.createDirectMessageEvent(sender, rcpt, "Hello, World!"); - identity.sign(event); - BaseMessage msg = new EventMessage(event); - assertDoesNotThrow(() -> { - client.send(msg); - - var filters = Filters.builder().kinds(new KindList(4)).authors(new PublicKeyList(sender)).build(); - msg = new ReqMessage("testNip42_" + sender.toString(), filters); - client.send(msg); - }); - } -*/ - - private Properties getRelayProperties() { - Properties properties = new Properties(); - try (InputStream is = getClass().getClassLoader().getResourceAsStream("relays.properties")) { - if (is != null) { - properties.load(is); - } else { - throw new IOException("Cannot find relays.properties on the classpath."); - } - } catch (IOException e) { - log.severe(e.getMessage()); - } - return properties; - } - - private int getRelayCount() { - return getRelayProperties().size(); - } - - public static Map convertPropertiesToMap(Properties properties) { - Map map = new HashMap<>(); - - // Iterate over the properties and put each entry into the map - Set propertyNames = properties.stringPropertyNames(); - for (String name : propertyNames) { - map.put(name, properties.getProperty(name)); - } - - return map; - } - -} diff --git a/nostr-java-test/src/test/java/nostr/test/event/APINIP09EventTest.java b/nostr-java-test/src/test/java/nostr/test/event/APINIP09EventTest.java index 0c11089a6..f44849bbf 100644 --- a/nostr-java-test/src/test/java/nostr/test/event/APINIP09EventTest.java +++ b/nostr-java-test/src/test/java/nostr/test/event/APINIP09EventTest.java @@ -3,11 +3,12 @@ import nostr.api.NIP01; import nostr.api.NIP09; import nostr.base.Relay; -import nostr.client.springwebsocket.SpringWebSocketClient; import nostr.event.BaseMessage; import nostr.event.BaseTag; import nostr.event.Kind; -import nostr.event.impl.Filters; +import nostr.event.filter.AuthorFilter; +import nostr.event.filter.Filters; +import nostr.event.filter.KindFilter; import nostr.event.impl.GenericEvent; import nostr.event.impl.ReplaceableEvent; import nostr.event.impl.TextNoteEvent; @@ -32,11 +33,6 @@ public class APINIP09EventTest { private static final String RELAY_URI = "ws://localhost:5555"; - private final SpringWebSocketClient springWebSocketClient; - - public APINIP09EventTest() { - springWebSocketClient = new SpringWebSocketClient(RELAY_URI); - } @Test public void deleteEvent() throws IOException { @@ -47,21 +43,18 @@ public void deleteEvent() throws IOException { NIP01 nip01 = new NIP01<>(identity); nip01.createTextNoteEvent("Delete me!").signAndSend(Map.of("local", RELAY_URI)); + Filters filters = new Filters( + new KindFilter<>(Kind.TEXT_NOTE), + new AuthorFilter<>(identity.getPublicKey())); - Filters filters = Filters - .builder() - .kinds(List.of(Kind.TEXT_NOTE)) - .authors(List.of(identity.getPublicKey())) - .build(); - - List result = nip01.send(filters, UUID.randomUUID().toString()); + List result = nip01.sendRequest(filters, UUID.randomUUID().toString()); assertFalse(result.isEmpty()); assertEquals(2, result.size()); nip09.createDeletionEvent(nip01.getEvent()).signAndSend(Map.of("local", RELAY_URI)); - result = nip01.send(filters, UUID.randomUUID().toString()); + result = nip01.sendRequest(filters, UUID.randomUUID().toString()); assertFalse(result.isEmpty()); assertEquals(1, result.size()); diff --git a/nostr-java-test/src/test/java/nostr/test/event/ApiEventTest.java b/nostr-java-test/src/test/java/nostr/test/event/ApiEventTest.java index 26a5a6b35..26e88c8a1 100644 --- a/nostr-java-test/src/test/java/nostr/test/event/ApiEventTest.java +++ b/nostr-java-test/src/test/java/nostr/test/event/ApiEventTest.java @@ -17,13 +17,11 @@ import nostr.crypto.bech32.Bech32; import nostr.crypto.bech32.Bech32Prefix; import nostr.event.BaseTag; -import nostr.event.Kind; import nostr.event.impl.CalendarContent; import nostr.event.impl.CreateOrUpdateStallEvent; import nostr.event.impl.CreateOrUpdateStallEvent.Stall; import nostr.event.impl.DirectMessageEvent; import nostr.event.impl.EncryptedPayloadEvent; -import nostr.event.impl.Filters; import nostr.event.impl.NostrMarketplaceEvent; import nostr.event.impl.NostrMarketplaceEvent.Product.Spec; import nostr.event.impl.TextNoteEvent; @@ -43,7 +41,6 @@ import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/nostr-java-test/src/test/java/nostr/test/event/ApiNIP52RequestTest.java b/nostr-java-test/src/test/java/nostr/test/event/ApiNIP52RequestTest.java index 8e78c7e19..6baba40fa 100644 --- a/nostr-java-test/src/test/java/nostr/test/event/ApiNIP52RequestTest.java +++ b/nostr-java-test/src/test/java/nostr/test/event/ApiNIP52RequestTest.java @@ -1,17 +1,6 @@ package nostr.test.event; -import java.io.IOException; -import java.net.URI; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; - import com.fasterxml.jackson.databind.ObjectMapper; - import nostr.api.NIP52; import nostr.base.PublicKey; import nostr.client.springwebsocket.SpringWebSocketClient; @@ -19,9 +8,6 @@ import nostr.event.impl.CalendarContent; import nostr.event.impl.GenericEvent; import nostr.event.impl.GenericTag; -import nostr.event.json.codec.BaseEventEncoder; -import nostr.event.json.codec.BaseMessageDecoder; -import nostr.event.json.codec.GenericEventDecoder; import nostr.event.message.EventMessage; import nostr.event.tag.EventTag; import nostr.event.tag.GeohashTag; @@ -30,7 +16,15 @@ import nostr.event.tag.PubKeyTag; import nostr.event.tag.ReferenceTag; import nostr.id.Identity; -import nostr.test.util.JsonComparator; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; class ApiNIP52RequestTest { private static final String PRV_KEY_VALUE = "23c011c4c02de9aa98d48c3646c70bb0e7ae30bdae1dfed4d251cbceadaeeb7b"; @@ -103,9 +97,9 @@ void testNIP99CalendarContentPreRequest() throws IOException { tags.add(R_TAG); CalendarContent calendarContent = CalendarContent.builder( - new IdentifierTag(UUID_CALENDAR_TIME_BASED_EVENT_TEST), - TITLE, - Long.valueOf(START)) + new IdentifierTag(UUID_CALENDAR_TIME_BASED_EVENT_TEST), + TITLE, + Long.valueOf(START)) .build(); var nip52 = new NIP52<>(Identity.create(PRV_KEY_VALUE)); @@ -152,18 +146,6 @@ void testNIP99CalendarContentPreRequest() throws IOException { */ } - private T mapJsonToEvent(List reqResponse, Class clazz) { - Optional first = reqResponse - .stream() - .map(baseMessage -> new BaseMessageDecoder().decode(baseMessage)) - .map(eventMessage -> ((GenericEvent) eventMessage.getEvent())) - .map(event -> new BaseEventEncoder<>(event).encode()) - .map(encode -> new GenericEventDecoder<>(clazz).decode(encode)) - .findFirst(); - T t = first.get(); - return t; - } - private String expectedEventResponseJson(String subscriptionId) { return "[\"OK\",\"" + subscriptionId + "\",true,\"success: request processed\"]"; } diff --git a/nostr-java-test/src/test/java/nostr/test/event/ApiNIP99RequestTest.java b/nostr-java-test/src/test/java/nostr/test/event/ApiNIP99RequestTest.java index 26e8881c9..10b9017b0 100644 --- a/nostr-java-test/src/test/java/nostr/test/event/ApiNIP99RequestTest.java +++ b/nostr-java-test/src/test/java/nostr/test/event/ApiNIP99RequestTest.java @@ -1,18 +1,7 @@ package nostr.test.event; -import java.io.IOException; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; - import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; - import nostr.api.NIP99; import nostr.base.PublicKey; import nostr.client.springwebsocket.SpringWebSocketClient; @@ -20,9 +9,6 @@ import nostr.event.impl.ClassifiedListing; import nostr.event.impl.GenericEvent; import nostr.event.impl.GenericTag; -import nostr.event.json.codec.BaseEventEncoder; -import nostr.event.json.codec.BaseMessageDecoder; -import nostr.event.json.codec.GenericEventDecoder; import nostr.event.message.EventMessage; import nostr.event.tag.EventTag; import nostr.event.tag.GeohashTag; @@ -31,8 +17,17 @@ import nostr.event.tag.PubKeyTag; import nostr.event.tag.SubjectTag; import nostr.id.Identity; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + import static nostr.test.event.ClassifiedListingEventTest.LOCATION_CODE; import static nostr.test.event.ClassifiedListingEventTest.PUBLISHED_AT_CODE; +import static org.junit.jupiter.api.Assertions.assertTrue; class ApiNIP99RequestTest { private static final String PRV_KEY_VALUE = "23c011c4c02de9aa98d48c3646c70bb0e7ae30bdae1dfed4d251cbceadaeeb7b"; @@ -85,9 +80,9 @@ void testNIP99ClassifiedListingPreRequest() throws IOException { PriceTag priceTag = new PriceTag(NUMBER, CURRENCY, FREQUENCY); ClassifiedListing classifiedListing = ClassifiedListing.builder( - TITLE, - SUMMARY, - priceTag) + TITLE, + SUMMARY, + priceTag) .build(); var nip99 = new NIP99<>(Identity.create(PRV_KEY_VALUE)); @@ -148,18 +143,6 @@ void testNIP99ClassifiedListingPreRequest() throws IOException { */ } - private T mapJsonToEvent(List reqResponse, Class clazz) { - Optional first = reqResponse - .stream() - .map(baseMessage -> new BaseMessageDecoder().decode(baseMessage)) - .map(eventMessage -> ((GenericEvent) eventMessage.getEvent())) - .map(event -> new BaseEventEncoder<>(event).encode()) - .map(encode -> new GenericEventDecoder<>(clazz).decode(encode)) - .findFirst(); - T t = first.get(); - return t; - } - private String expectedEventResponseJson(String subscriptionId) { return "[\"OK\",\"" + subscriptionId + "\",true,\"success: request processed\"]"; } diff --git a/nostr-java-test/src/test/java/nostr/test/event/BaseMessageCommandMapperTest.java b/nostr-java-test/src/test/java/nostr/test/event/BaseMessageCommandMapperTest.java new file mode 100644 index 000000000..94000b102 --- /dev/null +++ b/nostr-java-test/src/test/java/nostr/test/event/BaseMessageCommandMapperTest.java @@ -0,0 +1,72 @@ +package nostr.test.event; + +import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.extern.java.Log; +import nostr.event.BaseMessage; +import nostr.event.json.codec.BaseMessageDecoder; +import nostr.event.message.EoseMessage; +import nostr.event.message.ReqMessage; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Log +public class BaseMessageCommandMapperTest { +// TODO: flesh out remaining commands + public final static String REQ_JSON = + "[\"REQ\", " + + "\"npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh\", " + + "{\"kinds\": [1], " + + "\"authors\": [\"f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75\"]," + + "\"#e\": [\"fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712\"]}]"; + + @Test + public void testReqMessageDecoder() throws JsonProcessingException { + log.info("testReqMessageDecoder"); + + BaseMessage decode = new BaseMessageDecoder<>().decode(REQ_JSON); + assertInstanceOf(ReqMessage.class, decode); + } + + @Test + public void testReqMessageDecoderType() { + log.info("testReqMessageDecoderType"); + + assertDoesNotThrow(() -> { + new BaseMessageDecoder().decode(REQ_JSON); + }); + + assertDoesNotThrow(() -> { + ReqMessage reqMessage = new BaseMessageDecoder().decode(REQ_JSON); + }); + } + + @Test + public void testReqMessageDecoderThrows() { + log.info("testReqMessageDecoderThrows"); + + assertThrows(ClassCastException.class, () -> { + EoseMessage decode = new BaseMessageDecoder().decode(REQ_JSON); + }); + } + + @Test + public void testReqMessageDecoderDoesNotThrow() { + log.info("testReqMessageDecoderDoesNotThrow"); + + assertDoesNotThrow(() -> { + new BaseMessageDecoder().decode(REQ_JSON); + }); + } + + @Test + public void testReqMessageDecoderThrows3() { + log.info("testReqMessageDecoderThrows"); + + assertThrows(ClassCastException.class, () -> { + EoseMessage decode = new BaseMessageDecoder().decode(REQ_JSON); + }); + } +} diff --git a/nostr-java-test/src/test/java/nostr/test/event/CalendarContentDecodeTest.java b/nostr-java-test/src/test/java/nostr/test/event/CalendarContentDecodeTest.java index b375a85cd..7e2dae22b 100644 --- a/nostr-java-test/src/test/java/nostr/test/event/CalendarContentDecodeTest.java +++ b/nostr-java-test/src/test/java/nostr/test/event/CalendarContentDecodeTest.java @@ -82,25 +82,19 @@ public class CalendarContentDecodeTest { @Test void testCalendarContentMinimalJsonDecoding() { - assertDoesNotThrow(() -> { - CalendarTimeBasedEvent decode = new GenericEventDecoder<>(CalendarTimeBasedEvent.class).decode(eventMinimalJson); - return decode; - }); + assertDoesNotThrow(() -> + new GenericEventDecoder<>(CalendarTimeBasedEvent.class).decode(eventMinimalJson)); } @Test void testCalendarContentFullJsonDecoding() { - assertDoesNotThrow(() -> { - CalendarTimeBasedEvent decode = new GenericEventDecoder<>(CalendarTimeBasedEvent.class).decode(eventFullJson); - return decode; - }); + assertDoesNotThrow(() -> + new GenericEventDecoder<>(CalendarTimeBasedEvent.class).decode(eventFullJson)); } @Test void testCalendarContentProblemBarchettaJsonDecoding() { - assertDoesNotThrow(() -> { - CalendarTimeBasedEvent decoded = new GenericEventDecoder<>(CalendarTimeBasedEvent.class).decode(eventFullJson); - return decoded; - }); + assertDoesNotThrow(() -> + new GenericEventDecoder<>(CalendarTimeBasedEvent.class).decode(eventFullJson)); } } diff --git a/nostr-java-test/src/test/java/nostr/test/event/DecodeTest.java b/nostr-java-test/src/test/java/nostr/test/event/DecodeTest.java index b75ecdd49..1850e4d01 100644 --- a/nostr-java-test/src/test/java/nostr/test/event/DecodeTest.java +++ b/nostr-java-test/src/test/java/nostr/test/event/DecodeTest.java @@ -1,84 +1,90 @@ -package nostr.test.event; - -import nostr.base.PublicKey; -import nostr.event.BaseMessage; -import nostr.event.BaseTag; -import nostr.event.Marker; -import nostr.event.impl.GenericEvent; -import nostr.event.json.codec.BaseMessageDecoder; -import nostr.event.message.EventMessage; -import nostr.event.tag.EventTag; -import nostr.event.tag.PubKeyTag; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.List; - -public class DecodeTest { - - @Test - public void decodeTest() { - - String json = "[" - + "\"EVENT\"," - + "\"temp20230627\"," - + "{" - + "\"id\":\"28f2fc030e335d061f0b9d03ce0e2c7d1253e6fadb15d89bd47379a96b2c861a\"," - + "\"kind\":1," - + "\"pubkey\":\"2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984\"," - + "\"created_at\":1687765220," - + "\"content\":\"手順書が間違ってたら作業者は無理だな\"," - + "\"tags\":[" - + "[\"e\",\"494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4346\",\"\",\"root\"]," - + "[\"p\",\"2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984\"]" - + "]," - + "\"sig\":\"86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546\"" - + "}]"; - - BaseMessage message = new BaseMessageDecoder<>().decode(json); - - Assertions.assertEquals("EVENT", message.getCommand()); - EventMessage eventMessage = (EventMessage) message; - - Assertions.assertEquals("temp20230627", eventMessage.getSubscriptionId()); - GenericEvent eventImpl = (GenericEvent) eventMessage.getEvent(); - - Assertions.assertEquals("28f2fc030e335d061f0b9d03ce0e2c7d1253e6fadb15d89bd47379a96b2c861a", eventImpl.getId()); - Assertions.assertEquals(1, eventImpl.getKind()); - Assertions.assertEquals("2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984", eventImpl.getPubKey().toString()); - Assertions.assertEquals(1687765220, eventImpl.getCreatedAt()); - Assertions.assertEquals("手順書が間違ってたら作業者は無理だな", eventImpl.getContent()); - Assertions.assertEquals("86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546", - eventImpl.getSignature().toString()); - - List expectedTags = new ArrayList<>(); - EventTag eventTag = new EventTag("494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4346"); - eventTag.setRecommendedRelayUrl(""); - eventTag.setMarker(Marker.ROOT); - expectedTags.add(eventTag); - PubKeyTag pubKeyTag = new PubKeyTag(); - pubKeyTag.setPublicKey(new PublicKey("2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984")); - expectedTags.add(pubKeyTag); - - List actualTags = eventImpl.getTags(); - - for (int i = 0; i < expectedTags.size(); i++) { - BaseTag expected = expectedTags.get(i); - if (expected instanceof EventTag expetedEventTag) { - EventTag actualEventTag = (EventTag) actualTags.get(i); - Assertions.assertEquals(expetedEventTag.getIdEvent(), actualEventTag.getIdEvent()); - Assertions.assertEquals(expetedEventTag.getRecommendedRelayUrl(), actualEventTag.getRecommendedRelayUrl()); - Assertions.assertEquals(expetedEventTag.getMarker(), actualEventTag.getMarker()); - } else if (expected instanceof PubKeyTag expectedPublicKeyTag) { - PubKeyTag actualPublicKeyTag = (PubKeyTag) actualTags.get(i); - Assertions.assertEquals(expectedPublicKeyTag.getPublicKey().toString(), actualPublicKeyTag.getPublicKey().toString()); - } else { - Assertions.fail(); - } - - } - - } - -} +package nostr.test.event; + +import com.fasterxml.jackson.core.JsonProcessingException; +import nostr.base.PublicKey; +import nostr.event.BaseMessage; +import nostr.event.BaseTag; +import nostr.event.Marker; +import nostr.event.impl.GenericEvent; +import nostr.event.json.codec.BaseMessageDecoder; +import nostr.event.message.EventMessage; +import nostr.event.tag.EventTag; +import nostr.event.tag.PubKeyTag; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.fail; + +public class DecodeTest { + + @Test + public void decodeTest() throws JsonProcessingException { + + String json = "[" + + "\"EVENT\"," + + "\"temp20230627\"," + + "{" + + "\"id\":\"28f2fc030e335d061f0b9d03ce0e2c7d1253e6fadb15d89bd47379a96b2c861a\"," + + "\"kind\":1," + + "\"pubkey\":\"2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984\"," + + "\"created_at\":1687765220," + + "\"content\":\"手順書が間違ってたら作業者は無理だな\"," + + "\"tags\":[" + + "[\"e\",\"494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4346\",\"\",\"root\"]," + + "[\"p\",\"2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984\"]" + + "]," + + "\"sig\":\"86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546\"" + + "}]"; + + BaseMessage message = new BaseMessageDecoder<>().decode(json); + + assertEquals("EVENT", message.getCommand()); + assertInstanceOf(EventMessage.class, message); + + EventMessage eventMessage = (EventMessage) message; + + assertEquals("temp20230627", eventMessage.getSubscriptionId()); + GenericEvent eventImpl = (GenericEvent) eventMessage.getEvent(); + + assertEquals("28f2fc030e335d061f0b9d03ce0e2c7d1253e6fadb15d89bd47379a96b2c861a", eventImpl.getId()); + assertEquals(1, eventImpl.getKind()); + assertEquals("2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984", eventImpl.getPubKey().toString()); + assertEquals(1687765220, eventImpl.getCreatedAt()); + assertEquals("手順書が間違ってたら作業者は無理だな", eventImpl.getContent()); + assertEquals("86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546", + eventImpl.getSignature().toString()); + + List expectedTags = new ArrayList<>(); + EventTag eventTag = new EventTag("494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4346"); + eventTag.setRecommendedRelayUrl(""); + eventTag.setMarker(Marker.ROOT); + expectedTags.add(eventTag); + PubKeyTag pubKeyTag = new PubKeyTag(); + pubKeyTag.setPublicKey(new PublicKey("2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984")); + expectedTags.add(pubKeyTag); + + List actualTags = eventImpl.getTags(); + + for (int i = 0; i < expectedTags.size(); i++) { + BaseTag expected = expectedTags.get(i); + if (expected instanceof EventTag expetedEventTag) { + EventTag actualEventTag = (EventTag) actualTags.get(i); + assertEquals(expetedEventTag.getIdEvent(), actualEventTag.getIdEvent()); + assertEquals(expetedEventTag.getRecommendedRelayUrl(), actualEventTag.getRecommendedRelayUrl()); + assertEquals(expetedEventTag.getMarker(), actualEventTag.getMarker()); + } else if (expected instanceof PubKeyTag expectedPublicKeyTag) { + PubKeyTag actualPublicKeyTag = (PubKeyTag) actualTags.get(i); + assertEquals(expectedPublicKeyTag.getPublicKey().toString(), actualPublicKeyTag.getPublicKey().toString()); + } else { + fail(); + } + + } + + } + +} diff --git a/nostr-java-test/src/test/java/nostr/test/event/KindMappingTest.java b/nostr-java-test/src/test/java/nostr/test/event/KindMappingTest.java index 48b1f5283..cbb4b44b4 100644 --- a/nostr-java-test/src/test/java/nostr/test/event/KindMappingTest.java +++ b/nostr-java-test/src/test/java/nostr/test/event/KindMappingTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; public class KindMappingTest { @Test @@ -15,4 +16,9 @@ void testKindValueOf() { void testKindName() { assertEquals("text_note", Kind.valueOf(1).getName()); } + + @Test + void testKindUndefinedName() { + assertThrows(IllegalArgumentException.class, () -> Kind.valueOf(9999999)); + } } diff --git a/nostr-java-test/src/test/java/nostr/test/filters/FiltersDecoderTest.java b/nostr-java-test/src/test/java/nostr/test/filters/FiltersDecoderTest.java new file mode 100644 index 000000000..85c8bdeca --- /dev/null +++ b/nostr-java-test/src/test/java/nostr/test/filters/FiltersDecoderTest.java @@ -0,0 +1,324 @@ +package nostr.test.filters; + +import lombok.extern.java.Log; +import nostr.base.GenericTagQuery; +import nostr.base.PublicKey; +import nostr.event.Kind; +import nostr.event.filter.AddressableTagFilter; +import nostr.event.filter.EventFilter; +import nostr.event.filter.Filters; +import nostr.event.filter.GenericTagQueryFilter; +import nostr.event.filter.IdentifierTagFilter; +import nostr.event.filter.KindFilter; +import nostr.event.filter.ReferencedEventFilter; +import nostr.event.filter.ReferencedPublicKeyFilter; +import nostr.event.filter.SinceFilter; +import nostr.event.filter.UntilFilter; +import nostr.event.impl.GenericEvent; +import nostr.event.json.codec.FiltersDecoder; +import nostr.event.tag.AddressTag; +import nostr.event.tag.IdentifierTag; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Log +public class FiltersDecoderTest { + + @Test + public void testEventFiltersDecoder() { + log.info("testEventFiltersDecoder"); + + String filterKey = "ids"; + String eventId = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + + String expected = "{\"" + filterKey + "\":[\"" + eventId + "\"]}"; + Filters decodedFilters = new FiltersDecoder<>().decode(expected); + + assertEquals( + new Filters( + new EventFilter<>(new GenericEvent(eventId))), + decodedFilters); + } + + @Test + public void testMultipleEventFiltersDecoder() { + log.info("testMultipleEventFiltersDecoder"); + + String filterKey = "ids"; + String eventId1 = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + String eventId2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + String joined = String.join("\",\"", eventId1, eventId2); + + String expected = "{\"" + filterKey + "\":[\"" + joined + "\"]}"; + Filters decodedFilters = new FiltersDecoder<>().decode(expected); + + assertEquals( + new Filters( + new EventFilter<>(new GenericEvent(eventId1)), + new EventFilter<>(new GenericEvent(eventId2))), + decodedFilters); + } + + + @Test + public void testAddressableTagFiltersDecoder() { + log.info("testAddressableTagFiltersDecoder"); + + Integer kind = 1; + String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + String uuidValue1 = "UUID-1"; + + String joined = String.join(":", String.valueOf(kind), author, uuidValue1); + + AddressTag addressTag = new AddressTag(); + addressTag.setKind(kind); + addressTag.setPublicKey(new PublicKey(author)); + addressTag.setIdentifierTag(new IdentifierTag(uuidValue1)); + + String expected = "{\"#a\":[\"" + joined + "\"]}"; + Filters decodedFilters = new FiltersDecoder<>().decode(expected); + + assertEquals( + new Filters( + new AddressableTagFilter<>(addressTag)), + decodedFilters); + } + + @Test + public void testMultipleAddressableTagFiltersDecoder() { + log.info("testMultipleAddressableTagFiltersDecoder"); + + Integer kind1 = 1; + String author1 = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + String uuidValue1 = "UUID-1"; + + Integer kind2 = 1; + String author2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + String uuidValue2 = "UUID-2"; + + AddressTag addressTag1 = new AddressTag(); + addressTag1.setKind(kind1); + addressTag1.setPublicKey(new PublicKey(author1)); + addressTag1.setIdentifierTag(new IdentifierTag(uuidValue1)); + + AddressTag addressTag2 = new AddressTag(); + addressTag2.setKind(kind2); + addressTag2.setPublicKey(new PublicKey(author2)); + addressTag2.setIdentifierTag(new IdentifierTag(uuidValue2)); + + String joined1 = String.join(":", String.valueOf(kind1), author1, uuidValue1); + String joined2 = String.join(":", String.valueOf(kind2), author2, uuidValue2); + + String joined3 = String.join("\",\"", joined1, joined2); + + String expected = "{\"#a\":[\"" + joined3 + "\"]}"; + Filters decodedFilters = new FiltersDecoder<>().decode(expected); + + assertEquals( + new Filters( + new AddressableTagFilter<>(addressTag1), + new AddressableTagFilter<>(addressTag2)), + decodedFilters); + } + + @Test + public void testKindFiltersDecoder() { + log.info("testKindFiltersDecoder"); + + String filterKey = KindFilter.filterKey; + Kind kind = Kind.valueOf(1); + + String expected = "{\"" + filterKey + "\":[" + kind.toString() + "]}"; + Filters decodedFilters = new FiltersDecoder<>().decode(expected); + + assertEquals(new Filters(new KindFilter<>(kind)), decodedFilters); + } + + @Test + public void testMultipleKindFiltersDecoder() { + log.info("testMultipleKindFiltersDecoder"); + + String filterKey = KindFilter.filterKey; + Kind kind1 = Kind.valueOf(1); + Kind kind2 = Kind.valueOf(2); + + String join = String.join(",", kind1.toString(), kind2.toString()); + + String expected = "{\"" + filterKey + "\":[" + join + "]}"; + Filters decodedFilters = new FiltersDecoder<>().decode(expected); + + assertEquals( + new Filters( + new KindFilter<>(kind1), + new KindFilter<>(kind2)), + decodedFilters); + } + + @Test + public void testIdentifierTagFilterDecoder() { + log.info("testIdentifierTagFilterDecoder"); + + String uuidValue1 = "UUID-1"; + + String expected = "{\"#d\":[\"" + uuidValue1 + "\"]}"; + Filters decodedFilters = new FiltersDecoder<>().decode(expected); + + + assertEquals(new Filters(new IdentifierTagFilter<>(new IdentifierTag(uuidValue1))), decodedFilters); + } + + @Test + public void testMultipleIdentifierTagFilterDecoder() { + log.info("testMultipleIdentifierTagFilterDecoder"); + + String uuidValue1 = "UUID-1"; + String uuidValue2 = "UUID-2"; + + String joined = String.join("\",\"", uuidValue1, uuidValue2); + String expected = "{\"#d\":[\"" + joined + "\"]}"; + + Filters decodedFilters = new FiltersDecoder<>().decode(expected); + + assertEquals( + new Filters( + new IdentifierTagFilter<>(new IdentifierTag(uuidValue1)), + new IdentifierTagFilter<>(new IdentifierTag(uuidValue2))), + decodedFilters); + } + + @Test + public void testReferencedEventFilterDecoder() { + log.info("testReferencedEventFilterDecoder"); + + String eventId = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + + String expected = "{\"#e\":[\"" + eventId + "\"]}"; + Filters decodedFilters = new FiltersDecoder<>().decode(expected); + + assertEquals(new Filters(new ReferencedEventFilter<>(new GenericEvent(eventId))), decodedFilters); + } + + @Test + public void testMultipleReferencedEventFilterDecoder() { + log.info("testMultipleReferencedEventFilterDecoder"); + + String eventId1 = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + String eventId2 = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + + String joined = String.join("\",\"", eventId1, eventId2); + String expected = "{\"#e\":[\"" + joined + "\"]}"; + Filters decodedFilters = new FiltersDecoder<>().decode(expected); + + assertEquals( + new Filters( + new ReferencedEventFilter<>(new GenericEvent(eventId1)), + new ReferencedEventFilter<>(new GenericEvent(eventId2))), + decodedFilters); + } + + @Test + public void testReferencedPublicKeyFilterDecoder() { + log.info("testReferencedPublicKeyFilterDecoder"); + + String pubkeyString = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + + String expected = "{\"#p\":[\"" + pubkeyString + "\"]}"; + Filters decodedFilters = new FiltersDecoder<>().decode(expected); + + assertEquals(new Filters(new ReferencedPublicKeyFilter<>(new PublicKey(pubkeyString))), decodedFilters); + } + + @Test + public void testMultipleReferencedPublicKeyFilterDecoder() { + log.info("testMultipleReferencedPublicKeyFilterDecoder"); + + String pubkeyString1 = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + String pubkeyString2 = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + + String joined = String.join("\",\"", pubkeyString1, pubkeyString2); + String expected = "{\"#p\":[\"" + joined + "\"]}"; + + Filters decodedFilters = new FiltersDecoder<>().decode(expected); + + assertEquals( + new Filters( + new ReferencedPublicKeyFilter<>(new PublicKey(pubkeyString1)), + new ReferencedPublicKeyFilter<>(new PublicKey(pubkeyString2))), + decodedFilters); + } + + @Test + public void testGenericTagFiltersDecoder() { + log.info("testGenericTagFiltersDecoder"); + + String geohashKey = "#g"; + String geohashValue = "2vghde"; + String reqJsonWithCustomTagQueryFilterToDecode = "{\"" + geohashKey + "\":[\"" + geohashValue + "\"]}"; + + Filters decodedFilters = new FiltersDecoder<>().decode(reqJsonWithCustomTagQueryFilterToDecode); + + assertEquals(new Filters(new GenericTagQueryFilter<>(new GenericTagQuery(geohashKey, geohashValue))), decodedFilters); + } + + @Test + public void testMultipleGenericTagFiltersDecoder() { + log.info("testMultipleGenericTagFiltersDecoder"); + + String geohashKey = "#g"; + String geohashValue1 = "2vghde"; + String geohashValue2 = "3abcde"; + String reqJsonWithCustomTagQueryFilterToDecode = "{\"" + geohashKey + "\":[\"" + geohashValue1 + "\",\"" + geohashValue2 + "\"]}"; + + Filters decodedFilters = new FiltersDecoder<>().decode(reqJsonWithCustomTagQueryFilterToDecode); + + assertEquals( + new Filters( + new GenericTagQueryFilter<>(new GenericTagQuery(geohashKey, geohashValue1)), + new GenericTagQueryFilter<>(new GenericTagQuery(geohashKey, geohashValue2))), + decodedFilters); + } + + @Test + public void testSinceFiltersDecoder() { + log.info("testSinceFiltersDecoder"); + + Long since = Date.from(Instant.now()).getTime(); + + String expected = "{\"since\":" + since + "}"; + Filters decodedFilters = new FiltersDecoder<>().decode(expected); + + assertEquals(new Filters(new SinceFilter(since)), decodedFilters); + } + + @Test + public void testUntilFiltersDecoder() { + log.info("testUntilFiltersDecoder"); + + Long until = Date.from(Instant.now()).getTime(); + + String expected = "{\"until\":" + until + "}"; + Filters decodedFilters = new FiltersDecoder<>().decode(expected); + + assertEquals(new Filters(new UntilFilter(until)), decodedFilters); + } + + @Test + public void testFailedAddressableTagMalformedSeparator() { + log.info("testFailedAddressableTagMalformedSeparator"); + + Integer kind = 1; + String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + String uuidValue1 = "UUID-1"; + + String malformedJoin = String.join(",", String.valueOf(kind), author, uuidValue1); + String expected = "{\"#a\":[\"" + malformedJoin + "\"]}"; + + assertThrows(IllegalArgumentException.class, () -> new FiltersDecoder<>().decode(expected)); + } +} diff --git a/nostr-java-test/src/test/java/nostr/test/filters/FiltersEncoderTest.java b/nostr-java-test/src/test/java/nostr/test/filters/FiltersEncoderTest.java new file mode 100644 index 000000000..3331c8106 --- /dev/null +++ b/nostr-java-test/src/test/java/nostr/test/filters/FiltersEncoderTest.java @@ -0,0 +1,335 @@ +package nostr.test.filters; + +import lombok.extern.java.Log; +import nostr.base.GenericTagQuery; +import nostr.base.PublicKey; +import nostr.event.Kind; +import nostr.event.filter.AddressableTagFilter; +import nostr.event.filter.AuthorFilter; +import nostr.event.filter.EventFilter; +import nostr.event.filter.Filterable; +import nostr.event.filter.Filters; +import nostr.event.filter.GenericTagQueryFilter; +import nostr.event.filter.IdentifierTagFilter; +import nostr.event.filter.KindFilter; +import nostr.event.filter.ReferencedEventFilter; +import nostr.event.filter.ReferencedPublicKeyFilter; +import nostr.event.filter.SinceFilter; +import nostr.event.filter.UntilFilter; +import nostr.event.impl.GenericEvent; +import nostr.event.json.codec.FiltersEncoder; +import nostr.event.message.ReqMessage; +import nostr.event.tag.AddressTag; +import nostr.event.tag.IdentifierTag; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Log +public class FiltersEncoderTest { + + @Test + public void testEventFilterEncoderUsingVarArgs() { + log.info("testEventFilterEncoderUsingVarArgs"); + + String eventId = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + + FiltersEncoder encoder = new FiltersEncoder(new Filters(new EventFilter<>(new GenericEvent(eventId)))); + + String encodedFilters = encoder.encode(); + assertEquals("{\"ids\":[\"" + eventId + "\"]}", encodedFilters); + } + + @Test + public void testEventFilterEncoderUsingList() { + log.info("testEventFilterEncoderUsingList"); + + List expectedFilters = new ArrayList<>(); + + String eventId = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + expectedFilters.add(new EventFilter<>(new GenericEvent(eventId))); + + FiltersEncoder encoder = new FiltersEncoder(new Filters(expectedFilters)); + String encodedFilters = encoder.encode(); + assertEquals("{\"ids\":[\"" + eventId + "\"]}", encodedFilters); + } + + @Test + public void testEventFilterEncoderByMap() { + log.info("testEventFilterEncoderByMap"); + + String eventId = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + + FiltersEncoder encoder = new FiltersEncoder(new Filters(new EventFilter<>(new GenericEvent(eventId)))); + String encodedFilters = encoder.encode(); + assertEquals("{\"ids\":[\"" + eventId + "\"]}", encodedFilters); + } + + @Test + public void testKindFiltersEncoder() { + log.info("testKindFiltersEncoder"); + + Kind kind = Kind.valueOf(1); + FiltersEncoder encoder = new FiltersEncoder(new Filters(new KindFilter<>(kind))); + + String encodedFilters = encoder.encode(); + assertEquals("{\"kinds\":[" + kind.toString() + "]}", encodedFilters); + } + + @Test + public void testAuthorFilterEncoder() { + log.info("testAuthorFilterEncoder"); + + String pubKeyString = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + FiltersEncoder encoder = new FiltersEncoder(new Filters(new AuthorFilter<>(new PublicKey(pubKeyString)))); + + String encodedFilters = encoder.encode(); + assertEquals("{\"authors\":[\"" + pubKeyString + "\"]}", encodedFilters); + } + + @Test + public void testMultipleAuthorFilterEncoder() { + log.info("testMultipleAuthorFilterEncoder"); + + String pubKeyString1 = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + String pubKeyString2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + FiltersEncoder encoder = new FiltersEncoder(new Filters( + List.of( + new AuthorFilter<>(new PublicKey(pubKeyString1)), + new AuthorFilter<>(new PublicKey(pubKeyString2))))); + + String encodedFilters = encoder.encode(); + String authorPubKeys = String.join("\",\"", pubKeyString1, pubKeyString2); + + assertEquals("{\"authors\":[\"" + authorPubKeys + "\"]}", encodedFilters); + } + + @Test + public void testMultipleKindFiltersEncoder() { + log.info("testMultipleKindFiltersEncoder"); + + Kind kind1 = Kind.valueOf(1); + Kind kind2 = Kind.valueOf(2); + + FiltersEncoder encoder = new FiltersEncoder(new Filters( + List.of( + new KindFilter<>(kind1), + new KindFilter<>(kind2)))); + + String encodedFilters = encoder.encode(); + String kinds = String.join(",", kind1.toString(), kind2.toString()); + assertEquals("{\"kinds\":[" + kinds + "]}", encodedFilters); + } + + @Test + public void testAddressableTagFilterEncoder() { + log.info("testAddressableTagFilterEncoder"); + + Integer kind = 1; + String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + String uuidValue1 = "UUID-1"; + + AddressTag addressTag = new AddressTag(); + addressTag.setKind(kind); + addressTag.setPublicKey(new PublicKey(author)); + addressTag.setIdentifierTag(new IdentifierTag(uuidValue1)); + + FiltersEncoder encoder = new FiltersEncoder(new Filters(new AddressableTagFilter<>(addressTag))); + String encodedFilters = encoder.encode(); + String addressableTag = String.join(":", String.valueOf(kind), author, uuidValue1); + + assertEquals("{\"#a\":[\"" + addressableTag + "\"]}", encodedFilters); + } + + @Test + public void testIdentifierTagFilterEncoder() { + log.info("testIdentifierTagFilterEncoder"); + + String uuidValue1 = "UUID-1"; + + FiltersEncoder encoder = new FiltersEncoder(new Filters(new IdentifierTagFilter<>(new IdentifierTag(uuidValue1)))); + String encodedFilters = encoder.encode(); + assertEquals("{\"#d\":[\"" + uuidValue1 + "\"]}", encodedFilters); + } + + @Test + public void testMultipleIdentifierTagFilterEncoder() { + log.info("testMultipleIdentifierTagFilterEncoder"); + + String uuidValue1 = "UUID-1"; + String uuidValue2 = "UUID-2"; + + FiltersEncoder encoder = new FiltersEncoder(new Filters( + List.of( + new IdentifierTagFilter<>(new IdentifierTag(uuidValue1)), + new IdentifierTagFilter<>(new IdentifierTag(uuidValue2))))); + + String encodedFilters = encoder.encode(); + String dTags = String.join("\",\"", uuidValue1, uuidValue2); + assertEquals("{\"#d\":[\"" + dTags + "\"]}", encodedFilters); + } + + @Test + public void testReferencedEventFilterEncoder() { + log.info("testReferencedEventFilterEncoder"); + + String eventId = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + + FiltersEncoder encoder = new FiltersEncoder(new Filters(new ReferencedEventFilter<>(new GenericEvent(eventId)))); + String encodedFilters = encoder.encode(); + assertEquals("{\"#e\":[\"" + eventId + "\"]}", encodedFilters); + } + + @Test + public void testMultipleReferencedEventFilterEncoder() { + log.info("testMultipleReferencedEventFilterEncoder"); + + String eventId1 = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + String eventId2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + FiltersEncoder encoder = new FiltersEncoder(new Filters( + List.of( + new ReferencedEventFilter<>(new GenericEvent(eventId1)), + new ReferencedEventFilter<>(new GenericEvent(eventId2))))); + + String encodedFilters = encoder.encode(); + String eventIds = String.join("\",\"", eventId1, eventId2); + assertEquals("{\"#e\":[\"" + eventIds + "\"]}", encodedFilters); + } + + @Test + public void testSingleGenericTagQueryFiltersEncoder() { + log.info("testSingleGenericTagQueryFiltersEncoder"); + + String geohashKey = "#g"; + String new_geohash = "2vghde"; + + FiltersEncoder encoder = new FiltersEncoder( + new Filters(new GenericTagQueryFilter<>(new GenericTagQuery(geohashKey, new_geohash)))); + + String encodedFilters = encoder.encode(); + assertEquals("{\"#g\":[\"2vghde\"]}", encodedFilters); + } + + @Test + public void testReferencedPublicKeyFilterEncoder() { + log.info("testReferencedPublicKeyFilterEncoder"); + + String pubKeyString = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + + FiltersEncoder encoder = new FiltersEncoder(new Filters(new ReferencedPublicKeyFilter<>(new PublicKey(pubKeyString)))); + + String encodedFilters = encoder.encode(); + assertEquals("{\"#p\":[\"" + pubKeyString + "\"]}", encodedFilters); + } + + @Test + public void testMultipleReferencedPublicKeyFilterEncoder() { + log.info("testMultipleReferencedPublicKeyFilterEncoder"); + + String pubKeyString1 = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + String pubKeyString2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + FiltersEncoder encoder = new FiltersEncoder(new Filters( + new ReferencedPublicKeyFilter<>(new PublicKey(pubKeyString1)), + new ReferencedPublicKeyFilter<>(new PublicKey(pubKeyString2)))); + + String encodedFilters = encoder.encode(); + String pubKeyTags = String.join("\",\"", pubKeyString1, pubKeyString2); + assertEquals("{\"#p\":[\"" + pubKeyTags + "\"]}", encodedFilters); + } + + @Test + public void testMultipleGenericTagQueryFiltersEncoder() { + log.info("testMultipleGenericTagQueryFiltersEncoder"); + + String geohashKey = "#g"; + String geohashValue1 = "2vghde"; + String geohashValue2 = "3abcde"; + + FiltersEncoder encoder = new FiltersEncoder(new Filters( + new GenericTagQueryFilter<>(new GenericTagQuery(geohashKey, geohashValue1)), + new GenericTagQueryFilter<>(new GenericTagQuery(geohashKey, geohashValue2)))); + + String encodedFilters = encoder.encode(); + assertEquals("{\"#g\":[\"2vghde\",\"3abcde\"]}", encodedFilters); + } + + @Test + public void testMultipleAddressableTagFilterEncoder() { + log.info("testMultipleAddressableTagFilterEncoder"); + + Integer kind = 1; + String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + String uuidValue1 = "UUID-1"; + String uuidValue2 = "UUID-2"; + + String addressableTag1 = String.join(":", String.valueOf(kind), author, uuidValue1); + String addressableTag2 = String.join(":", String.valueOf(kind), author, uuidValue2); + + AddressTag addressTag1 = new AddressTag(); + addressTag1.setKind(kind); + addressTag1.setPublicKey(new PublicKey(author)); + addressTag1.setIdentifierTag(new IdentifierTag(uuidValue1)); + + AddressTag addressTag2 = new AddressTag(); + addressTag2.setKind(kind); + addressTag2.setPublicKey(new PublicKey(author)); + addressTag2.setIdentifierTag(new IdentifierTag(uuidValue2)); + + FiltersEncoder encoder = new FiltersEncoder(new Filters( + new AddressableTagFilter<>(addressTag1), + new AddressableTagFilter<>(addressTag2))); + + String encoded = encoder.encode(); + String addressableTags = String.join("\",\"", addressableTag1, addressableTag2); + assertEquals("{\"#a\":[\"" + addressableTags + "\"]}", encoded); + } + + @Test + public void testSinceFiltersEncoder() { + log.info("testSinceFiltersEncoder"); + + Long since = Date.from(Instant.now()).getTime(); + + FiltersEncoder encoder = new FiltersEncoder(new Filters(new SinceFilter(since))); + String encodedFilters = encoder.encode(); + assertEquals("{\"since\":" + since + "}", encodedFilters); + } + + @Test + public void testUntilFiltersEncoder() { + log.info("testUntilFiltersEncoder"); + + Long until = Date.from(Instant.now()).getTime(); + + FiltersEncoder encoder = new FiltersEncoder(new Filters(new UntilFilter(until))); + String encodedFilters = encoder.encode(); + assertEquals("{\"until\":" + until + "}", encodedFilters); + } + + @Test + public void testReqMessageEmptyFilters() { + log.info("testReqMessageEmptyFilters"); + String subscriptionId = "npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9"; + + assertThrows(IllegalArgumentException.class, () -> new ReqMessage(subscriptionId, new Filters(List.of()))); + } + + @Test + public void testReqMessageCustomGenericTagFilter() { + log.info("testReqMessageEmptyFilterKey"); + String subscriptionId = "npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9"; + + assertDoesNotThrow(() -> + new ReqMessage(subscriptionId, new Filters( + new GenericTagQueryFilter<>(new GenericTagQuery("some-tag", "some-value"))))); + } +} diff --git a/nostr-java-test/src/test/java/nostr/test/json/JsonParseTest.java b/nostr-java-test/src/test/java/nostr/test/json/JsonParseTest.java index c17855037..65a7aed69 100644 --- a/nostr-java-test/src/test/java/nostr/test/json/JsonParseTest.java +++ b/nostr-java-test/src/test/java/nostr/test/json/JsonParseTest.java @@ -1,10 +1,12 @@ package nostr.test.json; -import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.java.Log; import nostr.api.NIP01; import nostr.base.Command; import nostr.base.ElementAttribute; +import nostr.base.GenericTagQuery; import nostr.base.PublicKey; import nostr.crypto.bech32.Bech32; import nostr.event.BaseEvent; @@ -12,30 +14,37 @@ import nostr.event.BaseTag; import nostr.event.Kind; import nostr.event.Marker; -import nostr.event.impl.Filters; +import nostr.event.filter.AddressableTagFilter; +import nostr.event.filter.AuthorFilter; +import nostr.event.filter.EventFilter; +import nostr.event.filter.Filterable; +import nostr.event.filter.Filters; +import nostr.event.filter.GenericTagQueryFilter; +import nostr.event.filter.IdentifierTagFilter; +import nostr.event.filter.KindFilter; +import nostr.event.filter.ReferencedEventFilter; +import nostr.event.filter.ReferencedPublicKeyFilter; import nostr.event.impl.GenericEvent; import nostr.event.impl.GenericTag; import nostr.event.json.codec.BaseEventEncoder; import nostr.event.json.codec.BaseMessageDecoder; import nostr.event.json.codec.BaseTagDecoder; -import nostr.event.json.codec.FiltersDecoder; -import nostr.event.json.codec.FiltersEncoder; import nostr.event.json.codec.GenericEventDecoder; import nostr.event.json.codec.GenericTagDecoder; import nostr.event.message.EventMessage; import nostr.event.message.ReqMessage; +import nostr.event.tag.AddressTag; import nostr.event.tag.EventTag; +import nostr.event.tag.IdentifierTag; import nostr.event.tag.PriceTag; import nostr.event.tag.PubKeyTag; import nostr.id.Identity; -import nostr.util.NostrUtil; +import nostr.test.util.JsonComparator; +import nostr.util.NostrException; import org.junit.jupiter.api.Test; import java.math.BigDecimal; -import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.function.Function; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -49,570 +58,619 @@ */ @Log public class JsonParseTest { + ObjectMapper mapper = new ObjectMapper(); - @Test - public void testBaseMessageDecoder() { - log.info("testBaseMessageDecoder"); + @Test + public void testBaseMessageDecoderEventFilter() throws JsonProcessingException { + log.info("testBaseMessageDecoderEventFilter"); - final String parseTarget = - "[\"REQ\", " + - "\"npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh\", " + - "{\"kinds\": [1], " + - "\"authors\": [\"f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75\"]," + - "\"#e\": [\"fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712\"]}]"; - - final var message = new BaseMessageDecoder<>().decode(parseTarget); - - assertEquals(Command.REQ.toString(), message.getCommand()); - assertEquals("npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh", ((ReqMessage) message).getSubscriptionId()); - assertEquals(1, ((ReqMessage) message).getFiltersList().size()); - - var filters = ((ReqMessage) message).getFiltersList().getFirst(); - - assertEquals(1, filters.getKinds().size()); - assertEquals(Kind.TEXT_NOTE, filters.getKinds().getFirst()); - - assertEquals(1, filters.getAuthors().size()); - assertEquals("npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh", filters.getAuthors().getFirst().toBech32String()); - - assertEquals(1, filters.getReferencedEvents().size()); - assertEquals("fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712", (filters.getReferencedEvents().getFirst().getId())); - } - - @Test - public void testBaseReqMessageDecoder() { - log.info("testBaseReqMessageDecoder"); - - final var filtersList = new ArrayList(); - var publicKey = Identity.generateRandomIdentity().getPublicKey(); - filtersList.add(Filters.builder().authors(new ArrayList<>(List.of(publicKey))).kinds(new ArrayList<>(List.of(Kind.CONTACT_LIST, Kind.DELETION))).build()); - filtersList.add(Filters.builder().kinds(new ArrayList<>(List.of(Kind.SET_METADATA, Kind.TEXT_NOTE))).build()); - final var reqMessage = new ReqMessage(publicKey.toString(), filtersList); - - assertDoesNotThrow(() -> { - String jsonMessage = reqMessage.encode(); - - String jsonMsg = jsonMessage.substring(1, jsonMessage.length() - 1); - String[] parts = jsonMsg.split(","); - assertEquals("\"REQ\"", parts[0]); - assertEquals("\"" + publicKey.toString() + "\"", parts[1]); - assertFalse(parts[2].startsWith("[")); - assertFalse(parts[parts.length - 1].endsWith("]")); - - BaseMessage message = new BaseMessageDecoder<>().decode(jsonMessage); - - assertEquals(reqMessage, message); - }); - } - - @Test - public void testBaseEventMessageDecoder() { - log.info("testBaseEventMessageDecoder"); - - final String parseTarget - = "[\"EVENT\"," - + "{" - + "\"content\":\"直んないわ。まあええか\"," - + "\"created_at\":1686199583," - + "\"id\":\"fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712\"," - + "\"kind\":1," - + "\"pubkey\":\"8c59239319637f97e007dad0d681e65ce35b1ace333b629e2d33f9465c132608\"," - + "\"sig\":\"9584afd231c52fcbcec6ce668a2cc4b6dc9b4d9da20510dcb9005c6844679b4844edb7a2e1e0591958b0295241567c774dbf7d39a73932877542de1a5f963f4b\"," - + "\"tags\":[]" - + "}]"; - - final var message = new BaseMessageDecoder<>().decode(parseTarget); - - assertEquals(Command.EVENT.toString(), message.getCommand()); - - final var event = (GenericEvent) (((EventMessage) message).getEvent()); - assertEquals(1, event.getKind().intValue()); - assertEquals(1686199583, event.getCreatedAt().longValue()); - assertEquals("fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712", event.getId()); - } - - @Test - public void testBaseEventMessageMarkerDecoder() { - log.info("testBaseEventMessageMarkerDecoder"); - - final String json = "[" - + "\"EVENT\"," - + "{" - + "\"id\":\"28f2fc030e335d061f0b9d03ce0e2c7d1253e6fadb15d89bd47379a96b2c861a\"," - + "\"kind\":1," - + "\"pubkey\":\"2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984\"," - + "\"created_at\":1687765220," - + "\"content\":\"手順書が間違ってたら作業者は無理だな\"," - + "\"tags\":[" - + "[\"e\",\"494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4346\",\"\",\"root\"]," - + "[\"p\",\"2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984\"]" - + "]," - + "\"sig\":\"86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546\"" - + "}]"; - - BaseMessage message = new BaseMessageDecoder<>().decode(json); - - final var event = (GenericEvent) (((EventMessage) message).getEvent()); - var tags = event.getTags(); - for (BaseTag t : tags) { - if (t.getCode().equalsIgnoreCase("e")) { - EventTag et = (EventTag) t; - assertEquals(Marker.ROOT, et.getMarker()); - } - } - } - - @Test - public void testGenericTagDecoder() { - log.info("testGenericTagDecoder"); - final String jsonString = "[\"saturn\", \"jetpack\", false]"; - - var tag = new GenericTagDecoder<>().decode(jsonString); - - assertEquals("saturn", tag.getCode()); - assertEquals(2, tag.getAttributes().size()); - assertEquals("jetpack", ((ElementAttribute) (tag.getAttributes().toArray())[0]).getValue()); - assertEquals(false, Boolean.valueOf(((ElementAttribute) (tag.getAttributes().toArray())[1]).getValue().toString())); - } - - @Test - public void testClassifiedListingTagSerializer() { - log.info("testClassifiedListingSerializer"); - final String classifiedListingEventJson = "{" - + "\"id\":\"28f2fc030e335d061f0b9d03ce0e2c7d1253e6fadb15d89bd47379a96b2c861a\"," - + "\"kind\":30402," - + "\"content\":\"content ipsum\"," - + "\"pubkey\":\"ec0762fe78b0f0b763d1324452d973a38bef576d1d76662722d2b8ff948af1de\"," - + "\"created_at\":1687765220," - + "\"tags\":[" - + "[\"p\",\"ec0762fe78b0f0b763d1324452d973a38bef576d1d76662722d2b8ff948af1de\"]," - + "[\"title\",\"title ipsum\"]," - + "[\"summary\",\"summary ipsum\"]," - + "[\"published_at\",\"1687765220\"]," - + "[\"location\",\"location ipsum\"]," - + "[\"price\",\"11111\",\"BTC\",\"1\"]]," - + "\"sig\":\"86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546\"" - + "}]"; - - GenericEvent event = new GenericEventDecoder<>().decode(classifiedListingEventJson); - EventMessage message = NIP01.createEventMessage(event, "1"); - assertEquals(1, message.getNip()); - String encoded = new BaseEventEncoder<>((BaseEvent) message.getEvent()).encode(); - assertEquals("{\"id\":\"28f2fc030e335d061f0b9d03ce0e2c7d1253e6fadb15d89bd47379a96b2c861a\",\"kind\":30402,\"content\":\"content ipsum\",\"pubkey\":\"ec0762fe78b0f0b763d1324452d973a38bef576d1d76662722d2b8ff948af1de\",\"created_at\":1687765220,\"tags\":[[\"p\",\"ec0762fe78b0f0b763d1324452d973a38bef576d1d76662722d2b8ff948af1de\"],[\"title\",\"title ipsum\"],[\"summary\",\"summary ipsum\"],[\"published_at\",\"1687765220\"],[\"location\",\"location ipsum\"],[\"price\",\"11111\",\"BTC\",\"1\"]],\"sig\":\"86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546\"}", encoded); - - assertEquals("28f2fc030e335d061f0b9d03ce0e2c7d1253e6fadb15d89bd47379a96b2c861a", event.getId()); - assertEquals(30402, event.getKind()); - assertEquals("content ipsum", event.getContent()); - assertEquals("ec0762fe78b0f0b763d1324452d973a38bef576d1d76662722d2b8ff948af1de", event.getPubKey().toString()); - assertEquals(1687765220L, event.getCreatedAt()); - assertEquals("86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546", event.getSignature().toString()); - - assertEquals(new BigDecimal("11111"), event.getTags().stream().filter(baseTag -> - baseTag.getCode().equalsIgnoreCase("price")) - .filter(PriceTag.class::isInstance) - .map(PriceTag.class::cast) - .map(PriceTag::getNumber).findFirst().orElseThrow()); - - assertEquals("BTC", event.getTags().stream().filter(baseTag -> - baseTag.getCode().equalsIgnoreCase("price")) - .filter(PriceTag.class::isInstance) - .map(PriceTag.class::cast) - .map(PriceTag::getCurrency).findFirst().orElseThrow()); - - assertEquals("1", event.getTags().stream().filter(baseTag -> - baseTag.getCode().equalsIgnoreCase("price")) - .filter(PriceTag.class::isInstance) - .map(PriceTag.class::cast) - .map(PriceTag::getFrequency).findFirst().orElseThrow()); - - List genericTags = event.getTags().stream() - .filter(GenericTag.class::isInstance) - .map(GenericTag.class::cast).toList(); - - assertEquals("title ipsum", genericTags.stream() - .filter(tag -> tag.getCode().equalsIgnoreCase("title")).map(GenericTag::getAttributes).toList().getFirst().getFirst().getValue()); - - assertEquals("summary ipsum", genericTags.stream() - .filter(tag -> tag.getCode().equalsIgnoreCase("summary")).map(GenericTag::getAttributes).toList().getFirst().getFirst().getValue()); - - assertEquals("1687765220", genericTags.stream() - .filter(tag -> tag.getCode().equalsIgnoreCase("published_at")).map(GenericTag::getAttributes).toList().getFirst().getFirst().getValue()); - - assertEquals("location ipsum", genericTags.stream() - .filter(tag -> tag.getCode().equalsIgnoreCase("location")).map(GenericTag::getAttributes).toList().getFirst().getFirst().getValue()); - } - - @Test - public void testDeserializeTag() { - log.info("testDeserializeTag"); - - assertDoesNotThrow(() -> { - String npubHex = new PublicKey(NostrUtil.hexToBytes(Bech32.fromBech32("npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9"))).toString(); - - final String jsonString = "[\"p\", \"" + npubHex + "\", \"wss://nostr.java\", \"alice\"]"; - var tag = new BaseTagDecoder<>().decode(jsonString); - - assertInstanceOf(PubKeyTag.class, tag); - - PubKeyTag pTag = (PubKeyTag) tag; - assertEquals("wss://nostr.java", pTag.getMainRelayUrl()); - assertEquals(npubHex, pTag.getPublicKey().toString()); - assertEquals("alice", pTag.getPetName()); - }); - } - - @Test - public void testDeserializeGenericTag() { - log.info("testDeserializeGenericTag"); - assertDoesNotThrow(() -> { - String npubHex = new PublicKey(Bech32.decode("npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9").data).toString(); - final String jsonString = "[\"gt\", \"" + npubHex + "\", \"wss://nostr.java\", \"alice\"]"; - var tag = new BaseTagDecoder<>().decode(jsonString); - - assertInstanceOf(GenericTag.class, tag); - - GenericTag gTag = (GenericTag) tag; - assertEquals("gt", gTag.getCode()); - }); - } - - @Test - public void testFiltersEncoder() { - log.info("testFiltersEncoder"); - - String new_geohash = "2vghde"; - List geohashList = new ArrayList<>(); - geohashList.add(new_geohash); - Filters filters = Filters.builder().genericTagQuery(Map.of("#g", geohashList)).build(); - - FiltersEncoder encoder = new FiltersEncoder(filters); - String jsonMessage = encoder.encode(); - assertEquals("{\"#g\":[\"2vghde\"]}", jsonMessage); - } - - @Test - public void testReqMessageFilterListSerializer() { - log.info("testReqMessageFilterListSerializer"); - - String new_geohash = "2vghde"; - String second_geohash = "3abcde"; - List geohashList = new ArrayList<>(); - geohashList.add(new_geohash); - geohashList.add(second_geohash); - Filters filters = Filters.builder().genericTagQuery(Map.of("#g", geohashList)).build(); - - ReqMessage reqMessage = new ReqMessage("npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9", new ArrayList<>(List.of(filters))); - assertDoesNotThrow(() -> { - String jsonMessage = reqMessage.encode(); - - assertEquals("[\"REQ\",\"npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9\",{\"#g\":[\"2vghde\",\"3abcde\"]}]", jsonMessage); - }); - } - - @Test - public void testReqMessageFiltersDecoder() { - log.info("testReqMessageFiltersDecoder"); - - String geohashKey = "#g"; - String geohashValue = "2vghde"; - String reqJsonWithCustomTagQueryFilterToDecode = "{\"" + geohashKey + "\":[\"" + geohashValue + "\"]}"; - - Filters decodedFilters = new FiltersDecoder<>().decode(reqJsonWithCustomTagQueryFilterToDecode); - - Filters expectedFilters = new Filters(); - List expectedGeohashValueList = List.of(geohashValue); - expectedFilters.setGenericTagQuery(geohashKey, expectedGeohashValueList); - - assertEquals(expectedFilters, decodedFilters); - } - - @Test - public void testReqMessageFiltersListDecoder() { - log.info("testReqMessageFiltersListDecoder"); - - String geohashKey = "#g"; - String geohashValue1 = "2vghde"; - String geohashValue2 = "3abcde"; - String reqJsonWithCustomTagQueryFilterToDecode = "{\"" + geohashKey + "\":[\"" + geohashValue1 + "\",\"" + geohashValue2 + "\"]}"; - - Filters decodedFilters = new FiltersDecoder<>().decode(reqJsonWithCustomTagQueryFilterToDecode); - - Filters expectedFilters = new Filters(); - List expectedGeohashValuesList = List.of(geohashValue1, geohashValue2); - expectedFilters.setGenericTagQuery(geohashKey, expectedGeohashValuesList); - - assertEquals(expectedFilters, decodedFilters); - } - - @Test - public void testReqMessageDeserializer() { - log.info("testReqMessageDeserializer"); - - String subscriptionId = "npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9"; - String geohashKey = "#g"; - String geohashValue = "2vghde"; - String reqJsonWithCustomTagQueryFilterToDecode = "[\"REQ\",\"" + subscriptionId + "\",{\"" + geohashKey + "\":[\"" + geohashValue + "\"]}]"; - - ReqMessage decodedReqMessage = new BaseMessageDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); + String eventId = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + final String parseTarget = + "[\"REQ\", " + + "\"npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh\", " + + "{\"kinds\": [1], " + + "\"ids\": [\"" + eventId + "\"]," + + "\"#p\": [\"fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712\"]}]"; - Filters expectedFilters = new Filters(); - List expectedGeohashValuesList = List.of(geohashValue); - expectedFilters.setGenericTagQuery(geohashKey, expectedGeohashValuesList); + final var message = new BaseMessageDecoder<>().decode(parseTarget); - ReqMessage expectedReqMessage = new ReqMessage(subscriptionId, expectedFilters); - assertEquals(expectedReqMessage, decodedReqMessage); - } + assertEquals(Command.REQ.toString(), message.getCommand()); + assertEquals("npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh", ((ReqMessage) message).getSubscriptionId()); + assertEquals(1, ((ReqMessage) message).getFiltersList().size()); - @Test - public void testReqMessageFilterListDecoder() { - log.info("testReqMessageFilterListDecoder"); + Filters filters = ((ReqMessage) message).getFiltersList().getFirst(); - String subscriptionId = "npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9"; - String geohashKey = "#g"; - String geohashValue1 = "2vghde"; - String geohashValue2 = "3abcde"; - String reqJsonWithCustomTagQueryFiltersToDecode = "[\"REQ\",\"" + subscriptionId + "\",{\"" + geohashKey + "\":[\"" + geohashValue1 + "\",\"" + geohashValue2 + "\"]}]"; + List kindFilters = filters.getFilterByType(KindFilter.filterKey); + assertEquals(1, kindFilters.size()); + assertEquals(new KindFilter<>(Kind.TEXT_NOTE), kindFilters.getFirst()); - ReqMessage decodedReqMessage = new BaseMessageDecoder().decode(reqJsonWithCustomTagQueryFiltersToDecode); + List eventFilter = filters.getFilterByType(EventFilter.filterKey); + assertEquals(1, eventFilter.size()); + assertEquals(new EventFilter<>(new GenericEvent("f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75")), eventFilter.getFirst()); - Filters expectedFilters = new Filters(); - List expectedGeohashValuesList = List.of(geohashValue1, geohashValue2); - expectedFilters.setGenericTagQuery(geohashKey, expectedGeohashValuesList); + List referencedPublicKeyfilter = filters.getFilterByType(ReferencedPublicKeyFilter.filterKey); + assertEquals(1, referencedPublicKeyfilter.size()); + assertEquals(new ReferencedPublicKeyFilter<>(new PublicKey("fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712")), referencedPublicKeyfilter.getFirst()); + } - ReqMessage expectedReqMessage = new ReqMessage(subscriptionId, expectedFilters); - assertEquals(expectedReqMessage, decodedReqMessage); - } - - @Test - public void testReqMessagePopulatedFilterDecoder() { - log.info("testReqMessagePopulatedFilterDecoder"); - - String subscriptionId = "npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh"; - String kind = "1"; - String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - String geohashKey = "#g"; - String geohashValue1 = "2vghde"; - String geohashValue2 = "3abcde"; - String referencedEventId = "fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712"; - String reqJsonWithCustomTagQueryFilterToDecode = - "[\"REQ\", " + - "\"" + subscriptionId + "\", " + - "{\"kinds\": [" + kind + "], " + - "\"authors\": [\"" + author + "\"]," + - "\"" + geohashKey + "\": [\"" + geohashValue1 + "\",\"" + geohashValue2 + "\"]," + - "\"#e\": [\"" + referencedEventId + "\"]}]"; - - ReqMessage decodedReqMessage = new BaseMessageDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); - - Filters expectedFilters = new Filters(); - expectedFilters.setKinds(List.of(Kind.TEXT_NOTE)); - expectedFilters.setAuthors(List.of(new PublicKey(author))); - expectedFilters.setReferencedEvents(List.of(new GenericEvent(referencedEventId))); - List expectedGeohashValuesList = List.of(geohashValue1, geohashValue2); - expectedFilters.setGenericTagQuery(geohashKey, expectedGeohashValuesList); - - ReqMessage expectedReqMessage = new ReqMessage(subscriptionId, expectedFilters); - assertEquals(expectedReqMessage, decodedReqMessage); - } + @Test + public void testBaseMessageDecoderKindsAuthorsReferencedPublicKey() throws JsonProcessingException { + log.info("testBaseMessageDecoderKindsAuthorsReferencedPublicKey"); - @Test - public void testReqMessagePopulatedListOfFiltersListDecoder() { - log.info("testReqMessagePopulatedListOfFiltersListDecoder"); - - String subscriptionId = "npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh"; - String kind = "1"; - String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; - String geohashKey = "#g"; - String geohashValue1 = "2vghde"; - String geohashValue2 = "3abcde"; - String referencedEventId = "fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712"; - String uuidKey = "#d"; - String uuidValue1 = "UUID-1"; - String uuidValue2 = "UUID-2"; - String reqJsonWithCustomTagQueryFilterToDecode = - "[\"REQ\", " + - "\"" + subscriptionId + "\", " + - "{\"kinds\": [" + kind + "], " + - "\"authors\": [\"" + author + "\"]," + - "\"" + geohashKey + "\": [\"" + geohashValue1 + "\",\"" + geohashValue2 + "\"]," + - "\"" + uuidKey + "\": [\"" + uuidValue1 + "\",\"" + uuidValue2 + "\"]," + - "\"#e\": [\"" + referencedEventId + "\"]}]"; - - ReqMessage decodedReqMessage = new BaseMessageDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); - - Filters expectedFilters = new Filters(); - expectedFilters.setKinds(List.of(Kind.TEXT_NOTE)); - expectedFilters.setAuthors(List.of(new PublicKey(author))); - expectedFilters.setReferencedEvents(List.of(new GenericEvent(referencedEventId))); - List expectedGeohashValuesList = List.of(geohashValue1, geohashValue2); - expectedFilters.setGenericTagQuery(geohashKey, expectedGeohashValuesList); - List expectedIdentityTagValuesList = List.of(uuidValue1, uuidValue2); - expectedFilters.setGenericTagQuery(uuidKey, expectedIdentityTagValuesList); - ReqMessage expectedReqMessage = new ReqMessage(subscriptionId, expectedFilters); - assertEquals(expectedReqMessage, decodedReqMessage); - } - - @Test - public void testReqMessageSubscriptionIdLength() { - log.info("testReqMessageSubscriptionIdLength"); - String id65Chars = "npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9ab"; - assertTrue( - assertThrows(IllegalArgumentException.class, () -> new ReqMessage(id65Chars, new Filters())) - .getMessage().contains("subscriptionId length must be between 1 and 64 characters but was [65]")); - } + final String parseTarget = + "[\"REQ\", " + + "\"npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh\", " + + "{\"kinds\": [1], " + + "\"authors\": [\"f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75\"]," + + "\"#p\": [\"fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712\"]}]"; - @Test - public void testReqMessageFilterIdLength() { - log.info("testReqMessageFilterIdLength"); - String id64chars = "fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712"; - String reqJsonId64Chars = - "[\"REQ\",\"" + "subscriber_id" + "\",{\"ids\":[\"" + id64chars + "\"]}]"; - assertDoesNotThrow(() -> new BaseMessageDecoder().decode(reqJsonId64Chars)); - - String id65chars = "fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a7123"; - String reqJsonId65Chars = - "[\"REQ\",\"" + "subscriber_id" + "\",{\"ids\":[\"" + id65chars + "\"]}]"; - assertTrue( - assertThrows(IllegalArgumentException.class, () -> new BaseMessageDecoder().decode(reqJsonId65Chars)) - .getMessage().contains("[fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a7123], length: [65], target length: [64]")); - - String id63chars = "fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a71"; - String reqJsonId63chars = - "[\"REQ\",\"" + "subscriber_id" + "\",{\"ids\":[\"" + id63chars + "\"]}]"; - assertTrue( - assertThrows(IllegalArgumentException.class, () -> new BaseMessageDecoder().decode(reqJsonId63chars)) - .getMessage().contains("[fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a71], length: [63], target length: [64]")); - } + final var message = new BaseMessageDecoder<>().decode(parseTarget); - @Test - public void testReqMessageDecoderKind() { - log.info("testReqMessageDecoderKind"); + assertEquals(Command.REQ.toString(), message.getCommand()); + assertEquals("npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh", ((ReqMessage) message).getSubscriptionId()); + assertEquals(1, ((ReqMessage) message).getFiltersList().size()); - Function kindTarget = kind -> "[\"REQ\", " + - "\"npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh\", " + - "{\"kinds\": [" + kind + "]" + - "}]"; + Filters filters = ((ReqMessage) message).getFiltersList().getFirst(); - assertDoesNotThrow(() -> new BaseMessageDecoder<>().decode(kindTarget.apply(0))); - assertDoesNotThrow(() -> new BaseMessageDecoder<>().decode(kindTarget.apply(65535))); + List kindFilters = filters.getFilterByType(KindFilter.filterKey); + assertEquals(1, kindFilters.size()); + assertEquals(new KindFilter<>(Kind.TEXT_NOTE), kindFilters.getFirst()); - assertTrue( - assertThrows(IllegalArgumentException.class, () -> new BaseMessageDecoder<>().decode(kindTarget.apply(-1))) - .getMessage().contains("Kind must be between 0 and 65535 but was [-1]")); - assertTrue( - assertThrows(IllegalArgumentException.class, () -> new BaseMessageDecoder<>().decode(kindTarget.apply(65536))) - .getMessage().contains("Kind must be between 0 and 65535 but was [65536]")); - } + List authorFilters = filters.getFilterByType(AuthorFilter.filterKey); + assertEquals(1, authorFilters.size()); + assertEquals(new AuthorFilter<>(new PublicKey("f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75")), authorFilters.getFirst()); - @Test - public void testReqMessageDecoderETag() { - log.info("testReqMessageDecoderETag"); + List referencedPublicKeyfilter = filters.getFilterByType(ReferencedPublicKeyFilter.filterKey); + assertEquals(1, referencedPublicKeyfilter.size()); + assertEquals(new ReferencedPublicKeyFilter<>(new PublicKey("fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712")), referencedPublicKeyfilter.getFirst()); + } - String VALID_EVENTID = "56adf01ca1aa9d6f1c35953833bbe6d99a0c85b73af222e6bd305b51f2749f6f"; - String VALID_EVENTID_ALL_ZEROS = "0000000000000000000000000000000000000000000000000000000000000000"; - String VALID_EVENTID_ALL_FF = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; - String INVALID_EVENTID_NON_HEX_DIGITS = "XYZdf01ca1aa9d6f1c35953833bbe6d99a0c85b73af222e6bd305b51f2749f6f"; - String INVALID_EVENTID_LENGTH_TOO_SHORT = "56adf01ca1aa9d6f1c35953833bbe6d99a0c85b73af222e6bd305b51f2749f6"; - String INVALID_EVENTID_LENGTH_TOO_LONG = "56adf01ca1aa9d6f1c35953833bbe6d99a0c85b73af222e6bd305b51f2749f666"; - String INVALID_EVENTID_HAS_MULTIPLE_UPPERCASE = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; - String INVALID_EVENTID_HAS_SINGLE_UPPERCASE = "56adf01ca1aa9d6f1c35953833bbe6d99a0c85b73af222e6bd305b51f2749f6F"; + @Test + public void testBaseMessageDecoderKindsAuthorsReferencedEvents() throws JsonProcessingException { + log.info("testBaseMessageDecoderKindsAuthorsReferencedEvents"); - Function eTagTarget = eTag -> "[\"REQ\", " + + final String parseTarget = + "[\"REQ\", " + "\"npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh\", " + - "{\"#e\": [\"" + eTag + "\"]}]"; - - assertDoesNotThrow(() -> new BaseMessageDecoder<>().decode(eTagTarget.apply(VALID_EVENTID))); - assertDoesNotThrow(() -> new BaseMessageDecoder<>().decode(eTagTarget.apply(VALID_EVENTID_ALL_ZEROS))); - assertDoesNotThrow(() -> new BaseMessageDecoder<>().decode(eTagTarget.apply(VALID_EVENTID_ALL_FF))); - - assertTrue( - assertThrows(IllegalArgumentException.class, () -> new BaseMessageDecoder<>().decode(eTagTarget.apply(INVALID_EVENTID_NON_HEX_DIGITS))) - .getMessage().contains("has non-hex characters")); - assertTrue( - assertThrows(IllegalArgumentException.class, () -> new BaseMessageDecoder<>().decode(eTagTarget.apply(INVALID_EVENTID_LENGTH_TOO_SHORT))) - .getMessage().contains("target length")); - assertTrue( - assertThrows(IllegalArgumentException.class, () -> new BaseMessageDecoder<>().decode(eTagTarget.apply(INVALID_EVENTID_LENGTH_TOO_LONG))) - .getMessage().contains("target length")); - assertTrue( - assertThrows(IllegalArgumentException.class, () -> new BaseMessageDecoder<>().decode(eTagTarget.apply(INVALID_EVENTID_HAS_MULTIPLE_UPPERCASE))) - .getMessage().contains("has uppcase characters")); - assertTrue( - assertThrows(IllegalArgumentException.class, () -> new BaseMessageDecoder<>().decode(eTagTarget.apply(INVALID_EVENTID_HAS_SINGLE_UPPERCASE))) - .getMessage().contains("has uppcase characters")); + "{\"kinds\": [1], " + + "\"authors\": [\"f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75\"]," + + "\"#e\": [\"fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712\"]}]"; + + final var message = new BaseMessageDecoder<>().decode(parseTarget); + + assertEquals(Command.REQ.toString(), message.getCommand()); + assertEquals("npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh", ((ReqMessage) message).getSubscriptionId()); + assertEquals(1, ((ReqMessage) message).getFiltersList().size()); + + Filters filters = ((ReqMessage) message).getFiltersList().getFirst(); + + List kindFilters = filters.getFilterByType(KindFilter.filterKey); + assertEquals(1, kindFilters.size()); + assertEquals(new KindFilter<>(Kind.TEXT_NOTE), kindFilters.getFirst()); + + List authorFilters = filters.getFilterByType(AuthorFilter.filterKey); + assertEquals(1, authorFilters.size()); + assertEquals(new AuthorFilter<>(new PublicKey("f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75")), authorFilters.getFirst()); + + List referencedEventFilters = filters.getFilterByType(ReferencedEventFilter.filterKey); + assertEquals(1, referencedEventFilters.size()); + assertEquals(new ReferencedEventFilter<>(new GenericEvent("fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712")), referencedEventFilters.getFirst()); + } + + @Test + public void testBaseReqMessageDecoder() throws JsonProcessingException { + log.info("testBaseReqMessageDecoder"); + + var publicKey = Identity.generateRandomIdentity().getPublicKey(); + + final var expectedReqMessage = new ReqMessage(publicKey.toString(), + new Filters( + new KindFilter<>(Kind.SET_METADATA), + new KindFilter<>(Kind.TEXT_NOTE), + new KindFilter<>(Kind.CONTACT_LIST), + new KindFilter<>(Kind.DELETION), + new AuthorFilter<>(publicKey))); + + String jsonMessage = expectedReqMessage.encode(); + + String jsonMsg = jsonMessage.substring(1, jsonMessage.length() - 1); + + System.out.println(jsonMessage); + + String[] parts = jsonMsg.split(","); + assertEquals("\"REQ\"", parts[0]); + assertEquals("\"" + publicKey.toHexString() + "\"", parts[1]); + assertFalse(parts[2].startsWith("[")); + assertFalse(parts[parts.length - 1].endsWith("]")); + + ReqMessage actualMessage = new BaseMessageDecoder().decode(jsonMessage); + + assertEquals(jsonMessage, actualMessage.encode()); + assertEquals(expectedReqMessage, actualMessage); + } + + @Test + public void testBaseEventMessageDecoder() throws JsonProcessingException { + log.info("testBaseEventMessageDecoder"); + + final String parseTarget + = "[\"EVENT\"," + + "\"npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh\"," + + "{" + + "\"content\":\"直んないわ。まあええか\"," + + "\"created_at\":1686199583," + + "\"id\":\"fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712\"," + + "\"kind\":1," + + "\"pubkey\":\"8c59239319637f97e007dad0d681e65ce35b1ace333b629e2d33f9465c132608\"," + + "\"sig\":\"9584afd231c52fcbcec6ce668a2cc4b6dc9b4d9da20510dcb9005c6844679b4844edb7a2e1e0591958b0295241567c774dbf7d39a73932877542de1a5f963f4b\"," + + "\"tags\":[]" + + "}]"; + + final var message = new BaseMessageDecoder<>().decode(parseTarget); + + assertEquals(Command.EVENT.toString(), message.getCommand()); + + final var event = (GenericEvent) (((EventMessage) message).getEvent()); + assertEquals("npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh", ((EventMessage) message).getSubscriptionId()); + assertEquals(1, event.getKind().intValue()); + assertEquals(1686199583, event.getCreatedAt().longValue()); + assertEquals("fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712", event.getId()); + } + + @Test + public void testBaseEventMessageMarkerDecoder() throws JsonProcessingException { + log.info("testBaseEventMessageMarkerDecoder"); + + final String json = "[" + + "\"EVENT\"," + + "\"temp20230627\"," + + "{" + + "\"id\":\"28f2fc030e335d061f0b9d03ce0e2c7d1253e6fadb15d89bd47379a96b2c861a\"," + + "\"kind\":1," + + "\"pubkey\":\"2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984\"," + + "\"created_at\":1687765220," + + "\"content\":\"手順書が間違ってたら作業者は無理だな\"," + + "\"tags\":[" + + "[\"e\",\"494001ac0c8af2a10f60f23538e5b35d3cdacb8e1cc956fe7a16dfa5cbfc4346\",\"\",\"root\"]," + + "[\"p\",\"2bed79f81439ff794cf5ac5f7bff9121e257f399829e472c7a14d3e86fe76984\"]" + + "]," + + "\"sig\":\"86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546\"" + + "}]"; + + BaseMessage message = new BaseMessageDecoder<>().decode(json); + + final var event = (GenericEvent) (((EventMessage) message).getEvent()); + var tags = event.getTags(); + for (BaseTag t : tags) { + if (t.getCode().equalsIgnoreCase("e")) { + EventTag et = (EventTag) t; + assertEquals(Marker.ROOT, et.getMarker()); + } } + } + + @Test + public void testGenericTagDecoder() { + log.info("testGenericTagDecoder"); + final String jsonString = "[\"saturn\", \"jetpack\", false]"; + + var tag = new GenericTagDecoder<>().decode(jsonString); + + assertEquals("saturn", tag.getCode()); + assertEquals(2, tag.getAttributes().size()); + assertEquals("jetpack", ((ElementAttribute) (tag.getAttributes().toArray())[0]).getValue()); + assertEquals(false, Boolean.valueOf(((ElementAttribute) (tag.getAttributes().toArray())[1]).getValue().toString())); + } + + @Test + public void testClassifiedListingTagSerializer() throws JsonProcessingException { + log.info("testClassifiedListingSerializer"); + final String classifiedListingEventJson = "{" + + "\"id\":\"28f2fc030e335d061f0b9d03ce0e2c7d1253e6fadb15d89bd47379a96b2c861a\"," + + "\"kind\":30402," + + "\"content\":\"content ipsum\"," + + "\"pubkey\":\"ec0762fe78b0f0b763d1324452d973a38bef576d1d76662722d2b8ff948af1de\"," + + "\"created_at\":1687765220," + + "\"tags\":[" + + "[\"p\",\"ec0762fe78b0f0b763d1324452d973a38bef576d1d76662722d2b8ff948af1de\"]," + + "[\"title\",\"title ipsum\"]," + + "[\"summary\",\"summary ipsum\"]," + + "[\"published_at\",\"1687765220\"]," + + "[\"location\",\"location ipsum\"]," + + "[\"price\",\"11111\",\"BTC\",\"1\"]]," + + "\"sig\":\"86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546\"" + + "}]"; + + GenericEvent event = new GenericEventDecoder<>().decode(classifiedListingEventJson); + EventMessage message = NIP01.createEventMessage(event, "1"); + assertEquals(1, message.getNip()); + String encoded = new BaseEventEncoder<>((BaseEvent) message.getEvent()).encode(); + assertEquals("{\"id\":\"28f2fc030e335d061f0b9d03ce0e2c7d1253e6fadb15d89bd47379a96b2c861a\",\"kind\":30402,\"content\":\"content ipsum\",\"pubkey\":\"ec0762fe78b0f0b763d1324452d973a38bef576d1d76662722d2b8ff948af1de\",\"created_at\":1687765220,\"tags\":[[\"p\",\"ec0762fe78b0f0b763d1324452d973a38bef576d1d76662722d2b8ff948af1de\"],[\"title\",\"title ipsum\"],[\"summary\",\"summary ipsum\"],[\"published_at\",\"1687765220\"],[\"location\",\"location ipsum\"],[\"price\",\"11111\",\"BTC\",\"1\"]],\"sig\":\"86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546\"}", encoded); + + assertEquals("28f2fc030e335d061f0b9d03ce0e2c7d1253e6fadb15d89bd47379a96b2c861a", event.getId()); + assertEquals(30402, event.getKind()); + assertEquals("content ipsum", event.getContent()); + assertEquals("ec0762fe78b0f0b763d1324452d973a38bef576d1d76662722d2b8ff948af1de", event.getPubKey().toString()); + assertEquals(1687765220L, event.getCreatedAt()); + assertEquals("86f25c161fec51b9e441bdb2c09095d5f8b92fdce66cb80d9ef09fad6ce53eaa14c5e16787c42f5404905536e43ebec0e463aee819378a4acbe412c533e60546", event.getSignature().toString()); + + assertEquals(new BigDecimal("11111"), event.getTags().stream().filter(baseTag -> + baseTag.getCode().equalsIgnoreCase("price")) + .filter(PriceTag.class::isInstance) + .map(PriceTag.class::cast) + .map(PriceTag::getNumber).findFirst().orElseThrow()); + + assertEquals("BTC", event.getTags().stream().filter(baseTag -> + baseTag.getCode().equalsIgnoreCase("price")) + .filter(PriceTag.class::isInstance) + .map(PriceTag.class::cast) + .map(PriceTag::getCurrency).findFirst().orElseThrow()); + + assertEquals("1", event.getTags().stream().filter(baseTag -> + baseTag.getCode().equalsIgnoreCase("price")) + .filter(PriceTag.class::isInstance) + .map(PriceTag.class::cast) + .map(PriceTag::getFrequency).findFirst().orElseThrow()); + + List genericTags = event.getTags().stream() + .filter(GenericTag.class::isInstance) + .map(GenericTag.class::cast).toList(); + + assertEquals("title ipsum", genericTags.stream() + .filter(tag -> tag.getCode().equalsIgnoreCase("title")).map(GenericTag::getAttributes).toList().getFirst().getFirst().getValue()); + + assertEquals("summary ipsum", genericTags.stream() + .filter(tag -> tag.getCode().equalsIgnoreCase("summary")).map(GenericTag::getAttributes).toList().getFirst().getFirst().getValue()); + + assertEquals("1687765220", genericTags.stream() + .filter(tag -> tag.getCode().equalsIgnoreCase("published_at")).map(GenericTag::getAttributes).toList().getFirst().getFirst().getValue()); + + assertEquals("location ipsum", genericTags.stream() + .filter(tag -> tag.getCode().equalsIgnoreCase("location")).map(GenericTag::getAttributes).toList().getFirst().getFirst().getValue()); + } + + @Test + public void testDeserializeTag() throws NostrException { + log.info("testDeserializeTag"); + + String npubHex = new PublicKey(Bech32.fromBech32("npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9")).toString(); + final String jsonString = "[\"p\", \"" + npubHex + "\", \"wss://nostr.java\", \"alice\"]"; + var tag = new BaseTagDecoder<>().decode(jsonString); + + assertInstanceOf(PubKeyTag.class, tag); + + PubKeyTag pTag = (PubKeyTag) tag; + assertEquals("wss://nostr.java", pTag.getMainRelayUrl()); + assertEquals(npubHex, pTag.getPublicKey().toString()); + assertEquals("alice", pTag.getPetName()); + } + + @Test + public void testDeserializeGenericTag() throws NostrException { + log.info("testDeserializeGenericTag"); + String npubHex = new PublicKey(Bech32.fromBech32("npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9")).toString(); + final String jsonString = "[\"gt\", \"" + npubHex + "\", \"wss://nostr.java\", \"alice\"]"; + var tag = new BaseTagDecoder<>().decode(jsonString); + + assertInstanceOf(GenericTag.class, tag); + + GenericTag gTag = (GenericTag) tag; + assertEquals("gt", gTag.getCode()); + } + + @Test + public void testReqMessageFilterListSerializer() { + log.info("testReqMessageFilterListSerializer"); + + String new_geohash = "2vghde"; + String second_geohash = "3abcde"; + + ReqMessage reqMessage = new ReqMessage("npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9", + new Filters( + new GenericTagQueryFilter<>(new GenericTagQuery("#g", new_geohash)), + new GenericTagQueryFilter<>(new GenericTagQuery("#g", second_geohash)))); + + assertDoesNotThrow(() -> { + String jsonMessage = reqMessage.encode(); + String expected = "[\"REQ\",\"npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9\",{\"#g\":[\"2vghde\",\"3abcde\"]}]"; + assertEquals(expected, jsonMessage); + }); + } + + @Test + public void testReqMessageDeserializer() throws JsonProcessingException { + log.info("testReqMessageDeserializer"); + + String subscriptionId = "npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9"; + String geohashKey = "#g"; + String geohashValue = "2vghde"; + String reqJsonWithCustomTagQueryFilterToDecode = "[\"REQ\",\"" + subscriptionId + "\",{\"" + geohashKey + "\":[\"" + geohashValue + "\"]}]"; + + ReqMessage decodedReqMessage = new BaseMessageDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); + + ReqMessage expectedReqMessage = new ReqMessage(subscriptionId, + new Filters( + new GenericTagQueryFilter<>(new GenericTagQuery("#g", geohashValue)))); + + assertEquals(expectedReqMessage, decodedReqMessage); + } + + @Test + public void testReqMessageFilterListDecoder() { + log.info("testReqMessageFilterListDecoder"); + + String subscriptionId = "npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9"; + String geohashKey = "#g"; + String geohashValue1 = "2vghde"; + String geohashValue2 = "3abcde"; + String reqJsonWithCustomTagQueryFiltersToDecode = "[\"REQ\",\"" + subscriptionId + "\",{\"" + geohashKey + "\":[\"" + geohashValue1 + "\",\"" + geohashValue2 + "\"]}]"; + + assertDoesNotThrow(() -> { + ReqMessage decodedReqMessage = new BaseMessageDecoder().decode(reqJsonWithCustomTagQueryFiltersToDecode); + + ReqMessage expectedReqMessage = new ReqMessage(subscriptionId, + new Filters( + new GenericTagQueryFilter<>(new GenericTagQuery(geohashKey, geohashValue1)), + new GenericTagQueryFilter<>(new GenericTagQuery(geohashKey, geohashValue2)))); + + assertEquals(reqJsonWithCustomTagQueryFiltersToDecode, decodedReqMessage.encode()); + assertEquals(expectedReqMessage, decodedReqMessage); + }); + } + + @Test + public void testReqMessagePopulatedFilterDecoder() { + log.info("testReqMessagePopulatedFilterDecoder"); + + String subscriptionId = "npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh"; + String kind = "1"; + String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + String geohashKey = "#g"; + String geohashValue1 = "2vghde"; + String geohashValue2 = "3abcde"; + String referencedEventId = "fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712"; + String reqJsonWithCustomTagQueryFilterToDecode = + "[\"REQ\", " + + "\"" + subscriptionId + "\", " + + "{\"kinds\": [" + kind + "], " + + "\"authors\": [\"" + author + "\"]," + + "\"" + geohashKey + "\": [\"" + geohashValue1 + "\",\"" + geohashValue2 + "\"]," + + "\"#e\": [\"" + referencedEventId + "\"]," + + "\"#p\": [\"" + author + "\"]" + + "}]"; - @Test - public void testReqMessageDecoderPTag() { - log.info("testReqMessageDecoderPTag"); - - String VALID_HEXPUBKEY = "56adf01ca1aa9d6f1c35953833bbe6d99a0c85b73af222e6bd305b51f2749f6f"; - String VALID_HEXPUBKEY_ALL_ZEROS = "0000000000000000000000000000000000000000000000000000000000000000"; - String VALID_HEXPUBKEY_ALL_FF = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; - String INVALID_HEXPUBKEY_NON_HEX_DIGITS = "XYZdf01ca1aa9d6f1c35953833bbe6d99a0c85b73af222e6bd305b51f2749f6f"; - String INVALID_HEXPUBKEY_LENGTH_TOO_SHORT = "56adf01ca1aa9d6f1c35953833bbe6d99a0c85b73af222e6bd305b51f2749f6"; - String INVALID_HEXPUBKEY_LENGTH_TOO_LONG = "56adf01ca1aa9d6f1c35953833bbe6d99a0c85b73af222e6bd305b51f2749f666"; - String INVALID_HEXPUBKEY_HAS_MULTIPLE_UPPERCASE = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; - String INVALID_HEXPUBKEY_HAS_SINGLE_UPPERCASE = "56adf01ca1aa9d6f1c35953833bbe6d99a0c85b73af222e6bd305b51f2749f6F"; + assertDoesNotThrow(() -> { + ReqMessage decodedReqMessage = new BaseMessageDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); + + ReqMessage expectedReqMessage = new ReqMessage(subscriptionId, + new Filters( + new GenericTagQueryFilter<>(new GenericTagQuery(geohashKey, geohashValue1)), + new GenericTagQueryFilter<>(new GenericTagQuery(geohashKey, geohashValue2)), + new ReferencedPublicKeyFilter<>(new PublicKey(author)), + new KindFilter<>(Kind.TEXT_NOTE), + new AuthorFilter<>(new PublicKey(author)), + new ReferencedEventFilter<>(new GenericEvent(referencedEventId)))); + + assertEquals(expectedReqMessage, decodedReqMessage); + }); + } + + + @Test + public void testReqMessagePopulatedListOfFiltersWithIdentityDecoder() throws JsonProcessingException { + log.info("testReqMessagePopulatedListOfFiltersWithIdentityDecoder"); + + String subscriptionId = "npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh"; + String kind = "1"; + String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + String geohashKey = "#g"; + String geohashValue1 = "2vghde"; + String geohashValue2 = "3abcde"; + String referencedEventId = "fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712"; + String uuidKey = "#d"; + String uuidValue1 = "UUID-1"; + String uuidValue2 = "UUID-2"; + String reqJsonWithCustomTagQueryFilterToDecode = + "[\"REQ\", " + + "\"" + subscriptionId + "\", " + + "{\"kinds\": [" + kind + "], " + + "\"authors\": [\"" + author + "\"]," + + "\"" + geohashKey + "\": [\"" + geohashValue1 + "\",\"" + geohashValue2 + "\"]," + + "\"" + uuidKey + "\": [\"" + uuidValue1 + "\",\"" + uuidValue2 + "\"]," + + "\"#e\": [\"" + referencedEventId + "\"]}]"; + + ReqMessage decodedReqMessage = new BaseMessageDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); + + ReqMessage expectedReqMessage = new ReqMessage(subscriptionId, + new Filters( + new KindFilter<>(Kind.TEXT_NOTE), + new AuthorFilter<>(new PublicKey(author)), + new ReferencedEventFilter<>(new GenericEvent(referencedEventId)), + new GenericTagQueryFilter<>(new GenericTagQuery(geohashKey, geohashValue1)), + new GenericTagQueryFilter<>(new GenericTagQuery(geohashKey, geohashValue2)), + new IdentifierTagFilter<>(new IdentifierTag(uuidValue1)), + new IdentifierTagFilter<>(new IdentifierTag(uuidValue2)))); + + assertEquals(expectedReqMessage, decodedReqMessage); + } + + @Test + public void testReqMessagePopulatedListOfFiltersListDecoder() throws JsonProcessingException { + log.info("testReqMessagePopulatedListOfFiltersListDecoder"); + + String subscriptionId = "npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh"; + Integer kind = 1; + String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + String referencedEventId = "fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712"; + String uuidValue1 = "UUID-1"; + + String addressableTag = String.join(":", String.valueOf(kind), author, uuidValue1); + + String reqJsonWithCustomTagQueryFilterToDecode = + "[\"REQ\", " + + "\"" + subscriptionId + "\", " + + "{\"kinds\": [" + kind + "], " + + "\"authors\": [\"" + author + "\"]," + + "\"#e\": [\"" + referencedEventId + "\"]," + + "\"#a\": [\"" + addressableTag + "\"]," + + "\"#p\": [\"" + author + "\"]" + + "}]"; - Function eTagTarget = pTag -> "[\"REQ\", " + - "\"npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh\", " + - "{\"#p\": [\"" + pTag + "\"]}]"; - - assertDoesNotThrow(() -> new BaseMessageDecoder<>().decode(eTagTarget.apply(VALID_HEXPUBKEY))); - assertDoesNotThrow(() -> new BaseMessageDecoder<>().decode(eTagTarget.apply(VALID_HEXPUBKEY_ALL_ZEROS))); - assertDoesNotThrow(() -> new BaseMessageDecoder<>().decode(eTagTarget.apply(VALID_HEXPUBKEY_ALL_FF))); - - assertTrue( - assertThrows(IllegalArgumentException.class, () -> new BaseMessageDecoder<>().decode(eTagTarget.apply(INVALID_HEXPUBKEY_NON_HEX_DIGITS))) - .getMessage().contains("has non-hex characters")); - assertTrue( - assertThrows(IllegalArgumentException.class, () -> new BaseMessageDecoder<>().decode(eTagTarget.apply(INVALID_HEXPUBKEY_LENGTH_TOO_SHORT))) - .getMessage().contains("target length")); - assertTrue( - assertThrows(IllegalArgumentException.class, () -> new BaseMessageDecoder<>().decode(eTagTarget.apply(INVALID_HEXPUBKEY_LENGTH_TOO_LONG))) - .getMessage().contains("target length")); - assertTrue( - assertThrows(IllegalArgumentException.class, () -> new BaseMessageDecoder<>().decode(eTagTarget.apply(INVALID_HEXPUBKEY_HAS_MULTIPLE_UPPERCASE))) - .getMessage().contains("has uppcase characters")); - assertTrue( - assertThrows(IllegalArgumentException.class, () -> new BaseMessageDecoder<>().decode(eTagTarget.apply(INVALID_HEXPUBKEY_HAS_SINGLE_UPPERCASE))) - .getMessage().contains("has uppcase characters")); - } + ReqMessage decodedReqMessage = new BaseMessageDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); + + AddressTag addressTag1 = new AddressTag(); + addressTag1.setKind(kind); + addressTag1.setPublicKey(new PublicKey(author)); + addressTag1.setIdentifierTag(new IdentifierTag(uuidValue1)); + + ReqMessage expectedReqMessage = new ReqMessage(subscriptionId, + new Filters( + new KindFilter<>(Kind.TEXT_NOTE), + new AuthorFilter<>(new PublicKey(author)), + new ReferencedEventFilter<>(new GenericEvent(referencedEventId)), + new ReferencedPublicKeyFilter<>(new PublicKey(author)), + new AddressableTagFilter<>(addressTag1))); + + assertEquals(expectedReqMessage.encode(), decodedReqMessage.encode()); + assertEquals(expectedReqMessage, decodedReqMessage); + } + + @Test + public void testReqMessagePopulatedListOfMultipleTypeFiltersListDecoder() throws JsonProcessingException { + log.info("testReqMessagePopulatedListOfFiltersListDecoder"); + + String subscriptionId = "npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh"; + String kind = "1"; + String kind2 = "2"; + String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + String author2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + String referencedEventId = "fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712"; + String reqJsonWithCustomTagQueryFilterToDecode = + "[\"REQ\", " + + "\"" + subscriptionId + "\", " + + "{\"kinds\": [" + kind + ", " + kind2 + "], " + + "\"authors\": [\"" + author + "\",\"" + author2 + "\"]," + + "\"#e\": [\"" + referencedEventId + "\"]" + + "}]"; - @Test - public void testReqMessageFilterSince() { - log.info("testReqMessageFilterSince"); - Function sinceTarget = since -> "[\"REQ\", " + - "\"npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh\", " + - "{\"since\": " + since + "}]"; - - assertTrue( - assertThrows(IllegalArgumentException.class, () -> new BaseMessageDecoder<>().decode(sinceTarget.apply(null))) - .getMessage().contains("since is marked non-null but is null")); - assertTrue( - assertThrows(IllegalArgumentException.class, () -> new BaseMessageDecoder<>().decode(sinceTarget.apply("-1"))) - .getMessage().contains("'since' filter cannot be negative")); - assertTrue( - assertThrows(JsonMappingException.class, () -> new BaseMessageDecoder<>().decode(sinceTarget.apply("a"))) - .getMessage().contains("Unrecognized token 'a'")); - } + ReqMessage decodedReqMessage = new BaseMessageDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); + + ReqMessage expectedReqMessage = new ReqMessage(subscriptionId, + new Filters( + new KindFilter<>(Kind.TEXT_NOTE), + new KindFilter<>(Kind.RECOMMEND_SERVER), + new AuthorFilter<>(new PublicKey(author)), + new AuthorFilter<>(new PublicKey(author2)), + new ReferencedEventFilter<>(new GenericEvent(referencedEventId)))); + + assertEquals(expectedReqMessage.encode(), decodedReqMessage.encode()); + assertEquals(expectedReqMessage, decodedReqMessage); + } + + @Test + public void testGenericTagQueryListDecoder() throws JsonProcessingException { + log.info("testReqMessagePopulatedListOfFiltersListDecoder"); + + String subscriptionId = "npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh"; + String kind = "1"; + String kind2 = "2"; + String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + String author2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + String geohashKey = "#g"; + String geohashValue1 = "2vghde"; + String geohashValue2 = "3abcde"; + String referencedEventId = "fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712"; + String uuidKey = "#d"; + String uuidValue1 = "UUID-1"; + String uuidValue2 = "UUID-2"; + String reqJsonWithCustomTagQueryFilterToDecode = + "[\"REQ\", " + + "\"" + subscriptionId + "\", " + + "{\"kinds\": [" + kind + ", " + kind2 + "], " + + "\"authors\": [\"" + author + "\",\"" + author2 + "\"]," + + "\"" + geohashKey + "\": [\"" + geohashValue1 + "\",\"" + geohashValue2 + "\"]," + + "\"" + uuidKey + "\": [\"" + uuidValue1 + "\",\"" + uuidValue2 + "\"]," + + "\"#e\": [\"" + referencedEventId + "\"]" + + "}]"; - @Test - public void testReqMessageFilterUntil() { - log.info("testReqMessageFilterUntil"); - Function untilTarget = until -> "[\"REQ\", " + - "\"npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujh\", " + - "{\"until\": " + until + "}]"; - - assertTrue( - assertThrows(IllegalArgumentException.class, () -> new BaseMessageDecoder<>().decode(untilTarget.apply(null))) - .getMessage().contains("until is marked non-null but is null")); - assertTrue( - assertThrows(IllegalArgumentException.class, () -> new BaseMessageDecoder<>().decode(untilTarget.apply("-1"))) - .getMessage().contains("'until' filter cannot be negative")); - assertTrue( - assertThrows(JsonMappingException.class, () -> new BaseMessageDecoder<>().decode(untilTarget.apply("a"))) - .getMessage().contains("Unrecognized token 'a'")); - } + ReqMessage decodedReqMessage = new BaseMessageDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); + + ReqMessage expectedReqMessage = new ReqMessage(subscriptionId, + new Filters( + new KindFilter<>(Kind.TEXT_NOTE), + new KindFilter<>(Kind.RECOMMEND_SERVER), + new AuthorFilter<>(new PublicKey(author)), + new AuthorFilter<>(new PublicKey(author2)), + new ReferencedEventFilter<>(new GenericEvent(referencedEventId)), + new GenericTagQueryFilter<>(new GenericTagQuery(geohashKey, geohashValue1)), + new GenericTagQueryFilter<>(new GenericTagQuery(geohashKey, geohashValue2)), + new IdentifierTagFilter<>(new IdentifierTag(uuidValue1)), + new IdentifierTagFilter<>(new IdentifierTag(uuidValue2)))); + + assertTrue(JsonComparator.isEquivalentJson( + mapper.createArrayNode() + .add(mapper.readTree( + expectedReqMessage.encode())), + mapper.createArrayNode() + .add(mapper.readTree(decodedReqMessage.encode())))); + assertEquals(expectedReqMessage, decodedReqMessage); + } + + @Test + public void testReqMessageAddressableTagDeserializer() throws JsonProcessingException { + log.info("testReqMessageAddressableTagDeserializer"); + + Integer kind = 1; + String subscriptionId = "npub1clk6vc9xhjp8q5cws262wuf2eh4zuvwupft03hy4ttqqnm7e0jrq3upup9"; + String author = "f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75"; + String uuidKey = "#a"; + String uuidValue1 = "UUID-1"; + + String joined1 = String.join(":", String.valueOf(kind), author, uuidValue1); + + String reqJsonWithCustomTagQueryFilterToDecode = "[\"REQ\",\"" + subscriptionId + "\",{\"" + uuidKey + "\":[\"" + joined1 + "\"]}]"; + + ReqMessage decodedReqMessage = new BaseMessageDecoder().decode(reqJsonWithCustomTagQueryFilterToDecode); + + AddressTag addressTag1 = new AddressTag(); + addressTag1.setKind(kind); + addressTag1.setPublicKey(new PublicKey(author)); + addressTag1.setIdentifierTag(new IdentifierTag(uuidValue1)); + + ReqMessage expectedReqMessage = new ReqMessage(subscriptionId, new Filters(new AddressableTagFilter<>(addressTag1))); + + assertEquals(expectedReqMessage.encode(), decodedReqMessage.encode()); + assertEquals(expectedReqMessage, decodedReqMessage); + } + + @Test + public void testReqMessageSubscriptionIdTooLong() { + log.info("testReqMessageSubscriptionIdTooLong"); + + String malformedSubscriptionId = "npub17x6pn22ukq3n5yw5x9prksdyyu6ww9jle2ckpqwdprh3ey8qhe6stnpujhaa"; + final String parseTarget = + "[\"REQ\", " + + "\"" + malformedSubscriptionId + "\", " + + "{\"kinds\": [1], " + + "\"authors\": [\"f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75\"]," + + "\"#p\": [\"fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712\"]}]"; + + assertThrows(IllegalArgumentException.class, () -> new BaseMessageDecoder<>().decode(parseTarget)); + } + + @Test + public void testReqMessageSubscriptionIdTooShort() { + log.info("testReqMessageSubscriptionIdTooShort"); + + String malformedSubscriptionId = ""; + final String parseTarget = + "[\"REQ\", " + + "\"" + malformedSubscriptionId + "\", " + + "{\"kinds\": [1], " + + "\"authors\": [\"f1b419a95cb0233a11d431423b41a42734e7165fcab16081cd08ef1c90e0be75\"]," + + "\"#p\": [\"fc7f200c5bed175702bd06c7ca5dba90d3497e827350b42fc99c3a4fa276a712\"]}]"; + + assertThrows(IllegalArgumentException.class, () -> new BaseMessageDecoder<>().decode(parseTarget)); + } } + diff --git a/nostr-java-util/pom.xml b/nostr-java-util/pom.xml index 96b8fe48e..055036fd6 100644 --- a/nostr-java-util/pom.xml +++ b/nostr-java-util/pom.xml @@ -6,7 +6,7 @@ xyz.tcheeric nostr-java - 0.6.3-SNAPSHOT + 0.6.4-SNAPSHOT nostr-java-util diff --git a/nostr-java-util/src/main/java/nostr/util/thread/Task.java b/nostr-java-util/src/main/java/nostr/util/thread/Task.java deleted file mode 100644 index a82403264..000000000 --- a/nostr-java-util/src/main/java/nostr/util/thread/Task.java +++ /dev/null @@ -1,9 +0,0 @@ -package nostr.util.thread; - -import lombok.NonNull; -import nostr.context.Context; - -public interface Task { - - T execute(@NonNull Context context); -} diff --git a/nostr-java-util/src/main/java/nostr/util/thread/ThreadUtil.java b/nostr-java-util/src/main/java/nostr/util/thread/ThreadUtil.java deleted file mode 100644 index efed426b8..000000000 --- a/nostr-java-util/src/main/java/nostr/util/thread/ThreadUtil.java +++ /dev/null @@ -1,73 +0,0 @@ -package nostr.util.thread; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.NonNull; -import lombok.extern.java.Log; -import nostr.context.Context; - -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.locks.ReentrantLock; -import java.util.logging.Level; - -@AllArgsConstructor -@Builder -@Log -public class ThreadUtil { - - private final T task; - - @Builder.Default - private int timeoutSeconds = TIMEOUT_SECONDS; - - @Builder.Default - private boolean blocking = false; - - @Builder.Default - private boolean lock = false; - - public static final int TIMEOUT_SECONDS = 5; - - public static final ReentrantLock LOCK = new ReentrantLock(); - - public ThreadUtil(@NonNull T task) { - this.task = task; - } - - public void run(@NonNull Context context) throws TimeoutException { - log.log(Level.FINE, "Executing thread on {0}...", task); - ExecutorService threadPool = Executors.newCachedThreadPool(); - Future futureTask = threadPool.submit(() -> { - if (lock) { - LOCK.lock(); - } - try { - task.execute(context); - } catch (Exception e) { - log.log(Level.WARNING, "Failed to execute task: {0}", e.getMessage()); - } finally { - if (lock) { - LOCK.unlock(); - } - } - }); - - if (blocking) { - try { - log.log(Level.FINE, "Waiting for thread to complete... "); - futureTask.get(timeoutSeconds, TimeUnit.SECONDS); // Wait for the thread to complete - log.log(Level.FINE, "Thread execution completed!"); - } catch (InterruptedException | ExecutionException e) { - log.log(Level.WARNING, "Failed to execute task: {0}", e.getMessage()); - throw new RuntimeException(e); - } finally { - threadPool.shutdown(); - } - } - } -} diff --git a/pom.xml b/pom.xml index b243a293f..6db953756 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ xyz.tcheeric nostr-java - 0.6.3-SNAPSHOT + 0.6.4-SNAPSHOT pom ${project.artifactId} @@ -21,7 +21,7 @@ - @tcheeric + @tcheeric @avlo @@ -68,24 +68,19 @@ nostr-java-id nostr-java-test nostr-java-util - nostr-java-connection - nostr-java-command-interface - nostr-java-command-provider nostr-java-client nostr-java-api nostr-java-encryption nostr-java-encryption-nip04 nostr-java-encryption-nip44 - nostr-java-context - nostr-java-context-interface - nostr-java-controller + ${version} UTF-8 - - 21 - 21 + 21 + ${java.version} + ${java.version} 4.9.3 2.8.0 @@ -97,6 +92,7 @@ 1.78 2.17.2 + 3.17.0 5.10.2