See #1
This commit is contained in:
@@ -11,12 +11,17 @@ import org.java_websocket.client.WebSocketClient;
|
||||
import org.java_websocket.handshake.ServerHandshake;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -24,12 +29,26 @@ public class BukkitstrPlugin extends JavaPlugin implements Listener {
|
||||
|
||||
private NostrWebSocketClient webSocketClient;
|
||||
private final Set<Player> authenticatedPlayers = new HashSet<>();
|
||||
private final Map<UUID, PendingAuth> pendingAuths = new ConcurrentHashMap<>();
|
||||
|
||||
// Class to hold pending authentication data
|
||||
private static class PendingAuth {
|
||||
final Player player;
|
||||
final String pubkey;
|
||||
String minecraftUsername;
|
||||
boolean metadataVerified = false;
|
||||
|
||||
PendingAuth(Player player, String pubkey) {
|
||||
this.player = player;
|
||||
this.pubkey = pubkey;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
getLogger().info("BukkitstrPlugin enabled!");
|
||||
try {
|
||||
URI uri = new URI("wss://"); // Replace with your Nostr relay URL
|
||||
URI uri = new URI("ws://192.168.1.14:8888"); // Replace with your Nostr relay URL
|
||||
webSocketClient = new NostrWebSocketClient(uri, getLogger());
|
||||
webSocketClient.connect();
|
||||
} catch (URISyntaxException e) {
|
||||
@@ -48,23 +67,139 @@ public class BukkitstrPlugin extends JavaPlugin implements Listener {
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (sender instanceof Player) {
|
||||
Player player = (Player) sender;
|
||||
player.sendMessage("Waiting for Nostr event...");
|
||||
if (!(sender instanceof Player)) {
|
||||
sender.sendMessage("This command can only be used by players");
|
||||
return true;
|
||||
}
|
||||
|
||||
webSocketClient.waitForEvent(content -> handleSpecificEvent(content, player));
|
||||
Player player = (Player) sender;
|
||||
|
||||
if (command.getName().equalsIgnoreCase("nostrauth")) {
|
||||
if (args.length < 1) {
|
||||
player.sendMessage("Usage: /nostrauth <pubkey>");
|
||||
return true;
|
||||
}
|
||||
|
||||
String pubkey = args[0];
|
||||
player.sendMessage("Starting Nostr authentication for pubkey: " + pubkey);
|
||||
|
||||
// Create a unique ID for this authentication attempt
|
||||
UUID authId = UUID.randomUUID();
|
||||
String metadataSubId = "meta_" + authId.toString().replace("-", "");
|
||||
String authSubId = "auth_" + authId.toString().replace("-", "");
|
||||
|
||||
// Store the pending authentication
|
||||
PendingAuth pendingAuth = new PendingAuth(player, pubkey);
|
||||
pendingAuths.put(authId, pendingAuth);
|
||||
|
||||
// First step: Check for metadata (kind 30078)
|
||||
player.sendMessage("§6Checking your Nostr profile...");
|
||||
webSocketClient.subscribeToParameterizedReplaceable(metadataSubId, 30078, pubkey, "mcauth", authId,
|
||||
(success, username) -> {
|
||||
if (success && username != null) {
|
||||
pendingAuth.metadataVerified = true;
|
||||
pendingAuth.minecraftUsername = username;
|
||||
|
||||
// Verify username matches
|
||||
if (username.equalsIgnoreCase(player.getName())) {
|
||||
player.sendMessage("§6Profile verified! Waiting for authentication event...");
|
||||
|
||||
// Now subscribe to auth events (kind 13378008)
|
||||
webSocketClient.subscribeToAuthEvents(authSubId, 13378008, pubkey, authId);
|
||||
} else {
|
||||
player.sendMessage("§cAuthentication failed: The Minecraft username in your Nostr profile (" +
|
||||
username + ") doesn't match your in-game name (" + player.getName() + ")");
|
||||
webSocketClient.closeSubscription(metadataSubId);
|
||||
pendingAuths.remove(authId);
|
||||
}
|
||||
} else {
|
||||
player.sendMessage("§cCouldn't find a Nostr profile with the Minecraft username. " +
|
||||
"Make sure you've created a kind 30078 event with d=mcauth containing your username.");
|
||||
webSocketClient.closeSubscription(metadataSubId);
|
||||
pendingAuths.remove(authId);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void handleSpecificEvent(String content, Player player) {
|
||||
if (content.equals(player.getName())) {
|
||||
// Process an authentication event (kind 13378008)
|
||||
private void handleAuthEvent(UUID authId, JsonNode eventNode) {
|
||||
PendingAuth auth = pendingAuths.get(authId);
|
||||
if (auth == null || !auth.metadataVerified) {
|
||||
getLogger().warning("Received auth event for unknown auth ID or unverified metadata: " + authId);
|
||||
return;
|
||||
}
|
||||
|
||||
Player player = auth.player;
|
||||
|
||||
try {
|
||||
// Get event kind to verify it's 13378008
|
||||
int kind = eventNode.path("kind").asInt();
|
||||
if (kind != 13378008) {
|
||||
getLogger().warning("Expected kind 13378008, got: " + kind);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify the pubkey matches
|
||||
String eventPubkey = eventNode.path("pubkey").asText("");
|
||||
if (!eventPubkey.equals(auth.pubkey)) {
|
||||
player.sendMessage("§cAuthentication failed: Event signed by wrong pubkey");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check tags for the d tag with "mcauth" value
|
||||
boolean hasMcAuthTag = false;
|
||||
JsonNode tagsNode = eventNode.path("tags");
|
||||
if (tagsNode.isArray()) {
|
||||
for (JsonNode tag : tagsNode) {
|
||||
if (tag.isArray() && tag.size() > 1 &&
|
||||
"d".equals(tag.get(0).asText()) &&
|
||||
"mcauth".equals(tag.get(1).asText())) {
|
||||
hasMcAuthTag = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMcAuthTag) {
|
||||
player.sendMessage("§cAuthentication failed: Event is missing the d=mcauth tag");
|
||||
getLogger().warning("Missing d=mcauth tag in auth event");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the content
|
||||
String content = eventNode.path("content").asText("");
|
||||
|
||||
// Log the received event details
|
||||
getLogger().info("Auth event received - Content: '" + content + "', Player: '" + player.getName() + "'");
|
||||
|
||||
// Verify content matches the player's name
|
||||
if (content.equalsIgnoreCase(player.getName())) {
|
||||
// Authentication successful!
|
||||
authenticatedPlayers.add(player);
|
||||
player.sendMessage("You are now authenticated.");
|
||||
player.sendMessage("§a✓ Authentication successful! You can now move freely.");
|
||||
player.sendMessage("§6 Please note, you'll need to reauth ~30 days");
|
||||
getLogger().info("Player " + player.getName() + " authenticated successfully with pubkey " + auth.pubkey);
|
||||
|
||||
// Cleanup
|
||||
String authSubId = "auth_" + authId.toString().replace("-", "");
|
||||
String metaSubId = "meta_" + authId.toString().replace("-", "");
|
||||
webSocketClient.closeSubscription(authSubId);
|
||||
webSocketClient.closeSubscription(metaSubId);
|
||||
pendingAuths.remove(authId);
|
||||
} else {
|
||||
player.sendMessage("Authentication failed. Username does not match.");
|
||||
player.sendMessage("§cAuthentication failed: Event content doesn't match your username");
|
||||
getLogger().warning("Auth event content '" + content + "' doesn't match player name '" + player.getName() + "'");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
getLogger().severe("Error processing auth event: " + e.getMessage());
|
||||
player.sendMessage("§cAn error occurred during authentication. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +208,7 @@ public class BukkitstrPlugin extends JavaPlugin implements Listener {
|
||||
Player player = event.getPlayer();
|
||||
if (!authenticatedPlayers.contains(player)) {
|
||||
event.setCancelled(true);
|
||||
player.sendMessage("You need to authenticate first!");
|
||||
player.sendMessage("§cYou need to authenticate first with /nostrauth <pubkey>");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +216,7 @@ public class BukkitstrPlugin extends JavaPlugin implements Listener {
|
||||
|
||||
private final Logger logger;
|
||||
private final ObjectMapper objectMapper;
|
||||
private Consumer<String> eventConsumer;
|
||||
private final Map<String, UUID> subscriptionMap = new HashMap<>();
|
||||
|
||||
public NostrWebSocketClient(URI serverUri, Logger logger) {
|
||||
super(serverUri);
|
||||
@@ -92,7 +227,6 @@ public class BukkitstrPlugin extends JavaPlugin implements Listener {
|
||||
@Override
|
||||
public void onOpen(ServerHandshake handshakedata) {
|
||||
logger.info("Connected to Nostr relay: " + getURI());
|
||||
sendInitialRequest();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -114,37 +248,217 @@ public class BukkitstrPlugin extends JavaPlugin implements Listener {
|
||||
private void handleNostrEvent(String message) {
|
||||
try {
|
||||
JsonNode jsonNode = objectMapper.readTree(message);
|
||||
if (jsonNode.isArray() && jsonNode.size() > 2) {
|
||||
|
||||
// Log all messages for debugging
|
||||
logger.info("Processing Nostr message: " + message);
|
||||
|
||||
// Check if this is an EVENT message
|
||||
if (jsonNode.isArray() && jsonNode.size() >= 3 && "EVENT".equals(jsonNode.get(0).asText())) {
|
||||
String subscriptionId = jsonNode.get(1).asText();
|
||||
JsonNode eventNode = jsonNode.get(2);
|
||||
JsonNode tagsNode = eventNode.path("tags");
|
||||
if (tagsNode.isArray()) {
|
||||
for (JsonNode tag : tagsNode) {
|
||||
if (tag.isArray() && tag.size() > 1 && "d".equals(tag.get(0).asText())) {
|
||||
String content = tag.get(1).asText();
|
||||
if (eventConsumer != null) {
|
||||
eventConsumer.accept(content);
|
||||
|
||||
// Log the event kind for debugging
|
||||
int kind = eventNode.path("kind").asInt();
|
||||
logger.info("Received event with kind: " + kind + " for subscription: " + subscriptionId);
|
||||
|
||||
// Figure out what kind of subscription this is
|
||||
if (subscriptionId.startsWith("meta_")) {
|
||||
logger.info("Processing as metadata event");
|
||||
handleMetadataResponse(subscriptionId, eventNode);
|
||||
} else if (subscriptionId.startsWith("auth_")) {
|
||||
logger.info("Processing as auth event");
|
||||
handleAuthResponse(subscriptionId, eventNode);
|
||||
}
|
||||
} else if (jsonNode.isArray() && jsonNode.size() >= 3 && "EOSE".equals(jsonNode.get(0).asText())) {
|
||||
// End of stored events
|
||||
String subscriptionId = jsonNode.get(1).asText();
|
||||
logger.info("Received EOSE for subscription: " + subscriptionId);
|
||||
|
||||
if (subscriptionId.startsWith("meta_")) {
|
||||
UUID authId = getAuthIdFromSubscription(subscriptionId);
|
||||
if (authId != null) {
|
||||
PendingAuth auth = pendingAuths.get(authId);
|
||||
if (auth != null && !auth.metadataVerified) {
|
||||
// No metadata found by EOSE
|
||||
auth.player.sendMessage("§cNo Nostr profile found with Minecraft authentication. " +
|
||||
"Please set up your profile first.");
|
||||
closeSubscription(subscriptionId);
|
||||
pendingAuths.remove(authId);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.warning("No 'tags' array found in the event message.");
|
||||
}
|
||||
} else {
|
||||
logger.warning("Event message is not in the expected format.");
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.severe("Failed to parse Nostr event: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void waitForEvent(Consumer<String> eventConsumer) {
|
||||
this.eventConsumer = eventConsumer;
|
||||
private void handleMetadataResponse(String subscriptionId, JsonNode eventNode) {
|
||||
UUID authId = getAuthIdFromSubscription(subscriptionId);
|
||||
if (authId == null) return;
|
||||
|
||||
try {
|
||||
// Check if this is kind 30078
|
||||
int kind = eventNode.path("kind").asInt();
|
||||
if (kind != 30078) return;
|
||||
|
||||
// Look for d tag with "mcauth" value
|
||||
String dValue = null;
|
||||
JsonNode tagsNode = eventNode.path("tags");
|
||||
if (tagsNode.isArray()) {
|
||||
for (JsonNode tag : tagsNode) {
|
||||
if (tag.isArray() && tag.size() > 1 &&
|
||||
"d".equals(tag.get(0).asText())) {
|
||||
dValue = tag.get(1).asText();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sendInitialRequest() {
|
||||
String initialRequest = "[\"REQ\",\"mc_auth\",{\"kinds\":[13378008],\"limit\":0}]";
|
||||
send(initialRequest);
|
||||
logger.info("Sent initial request to Nostr relay: " + initialRequest);
|
||||
if (!"mcauth".equals(dValue)) {
|
||||
logger.info("Skipping 30078 event with d=" + dValue + " (looking for d=mcauth)");
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse content for the Minecraft username
|
||||
String content = eventNode.path("content").asText("");
|
||||
String username = content.trim();
|
||||
|
||||
if (username.isEmpty()) {
|
||||
logger.warning("Empty Minecraft username in 30078 event");
|
||||
return;
|
||||
}
|
||||
|
||||
PendingAuth auth = pendingAuths.get(authId);
|
||||
if (auth != null) {
|
||||
// Callback with success
|
||||
auth.player.sendMessage("§6Found Nostr profile with Minecraft username: " + username);
|
||||
auth.metadataVerified = true;
|
||||
auth.minecraftUsername = username;
|
||||
|
||||
// Check if username matches
|
||||
if (username.equalsIgnoreCase(auth.player.getName())) {
|
||||
auth.player.sendMessage("§6Username verified! Waiting for authentication event...");
|
||||
|
||||
// Now subscribe to auth events (kind 13378008)
|
||||
String authSubId = "auth_" + authId.toString().replace("-", "");
|
||||
subscribeToAuthEvents(authSubId, 13378008, auth.pubkey, authId);
|
||||
} else {
|
||||
auth.player.sendMessage("§cAuthentication failed: The Minecraft username in your Nostr profile (" +
|
||||
username + ") doesn't match your in-game name (" + auth.player.getName() + ")");
|
||||
closeSubscription(subscriptionId);
|
||||
pendingAuths.remove(authId);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.severe("Error handling metadata response: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleAuthResponse(String subscriptionId, JsonNode eventNode) {
|
||||
UUID authId = getAuthIdFromSubscription(subscriptionId);
|
||||
if (authId != null) {
|
||||
// Log the event details for debugging
|
||||
int kind = eventNode.path("kind").asInt();
|
||||
String pubkey = eventNode.path("pubkey").asText("");
|
||||
String content = eventNode.path("content").asText("");
|
||||
|
||||
logger.info("Auth event received - Kind: " + kind + ", Pubkey: " + pubkey + ", Content: '" + content + "'");
|
||||
|
||||
// Only process kind 13378008 events for authentication
|
||||
if (kind == 13378008) {
|
||||
logger.info("Processing kind 13378008 event for authentication");
|
||||
handleAuthEvent(authId, eventNode);
|
||||
} else {
|
||||
logger.info("Ignoring event with kind " + kind + " (expecting 13378008)");
|
||||
}
|
||||
} else {
|
||||
logger.warning("Cannot find auth ID for subscription: " + subscriptionId);
|
||||
}
|
||||
}
|
||||
|
||||
private UUID getAuthIdFromSubscription(String subscriptionId) {
|
||||
// Extract authId from subscription ID
|
||||
String idPart = subscriptionId.substring(subscriptionId.indexOf('_') + 1);
|
||||
try {
|
||||
return UUID.fromString(
|
||||
idPart.replaceFirst(
|
||||
"(\\p{XDigit}{8})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}+)",
|
||||
"$1-$2-$3-$4-$5"
|
||||
)
|
||||
);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warning("Invalid UUID in subscription ID: " + idPart);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to parameterized replaceable events (for metadata)
|
||||
public void subscribeToParameterizedReplaceable(String subscriptionId, int kind, String pubkey,
|
||||
String dValue, UUID authId,
|
||||
MetadataCallback callback) {
|
||||
try {
|
||||
// Create proper filter for Nostr - using array notation for kinds and correct #d tag format
|
||||
String filterJson = "{" +
|
||||
"\"kinds\":[" + kind + "]," +
|
||||
"\"authors\":[\"" + pubkey + "\"]," +
|
||||
"\"#d\":[\"" + dValue + "\"]" +
|
||||
"}";
|
||||
|
||||
JsonNode filter = objectMapper.readTree(filterJson);
|
||||
String reqMessage = "[\"REQ\",\"" + subscriptionId + "\"," + filter.toString() + "]";
|
||||
send(reqMessage);
|
||||
logger.info("Sent metadata subscription: " + reqMessage);
|
||||
|
||||
// Store the subscription ID mapping
|
||||
subscriptionMap.put(subscriptionId, authId);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.severe("Failed to create metadata subscription: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to authentication events
|
||||
public void subscribeToAuthEvents(String subscriptionId, int kind, String pubkey, UUID authId) {
|
||||
try {
|
||||
// Create proper filter format for Nostr - using array notation for #d tag
|
||||
String filterJson = "{" +
|
||||
"\"kinds\":[" + kind + "]," +
|
||||
"\"authors\":[\"" + pubkey + "\"]," +
|
||||
"\"#d\":[\"mcauth\"]" +
|
||||
"}";
|
||||
|
||||
JsonNode filter = objectMapper.readTree(filterJson);
|
||||
String reqMessage = "[\"REQ\",\"" + subscriptionId + "\"," + filter.toString() + "]";
|
||||
send(reqMessage);
|
||||
logger.info("Sent auth subscription: " + reqMessage);
|
||||
|
||||
// Store the subscription ID mapping
|
||||
subscriptionMap.put(subscriptionId, authId);
|
||||
|
||||
// Notify player
|
||||
PendingAuth auth = pendingAuths.get(authId);
|
||||
if (auth != null) {
|
||||
auth.player.sendMessage("§6Waiting for authentication event (kind 13378008)...");
|
||||
auth.player.sendMessage("§6Please sign an event with your Nostr client.");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.severe("Failed to create auth subscription: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void closeSubscription(String subscriptionId) {
|
||||
String closeMessage = "[\"CLOSE\",\"" + subscriptionId + "\"]";
|
||||
send(closeMessage);
|
||||
logger.info("Closed subscription: " + closeMessage);
|
||||
subscriptionMap.remove(subscriptionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Interface for metadata callback
|
||||
interface MetadataCallback {
|
||||
void onResult(boolean success, String username);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user