diff --git a/.clang-format b/.clang-format index 6a2b0f1..4cd14be 100644 --- a/.clang-format +++ b/.clang-format @@ -7,3 +7,4 @@ AlignAfterOpenBracket: BlockIndent ColumnLimit: 100 BinPackArguments: false AllowAllArgumentsOnNextLine: true +BreakStringLiterals: true diff --git a/common/src/main/java/online/mineroo/common/BindRequest.java b/common/src/main/java/online/mineroo/common/BindRequest.java index b491fe1..d6da473 100644 --- a/common/src/main/java/online/mineroo/common/BindRequest.java +++ b/common/src/main/java/online/mineroo/common/BindRequest.java @@ -4,15 +4,13 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.annotations.SerializedName; - -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; - import java.io.Serializable; import java.util.HashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; /** * BindRequest handles server binding and verification with the Mineroo API. @@ -22,7 +20,6 @@ import java.util.concurrent.CompletableFuture; * to handle the actual HTTP transport and authentication. */ public class BindRequest { - private final Logger logger; private final NetworkServiceInterface networkService; @@ -59,7 +56,6 @@ public class BindRequest { return networkService.getData(path, params) .thenApply(response -> { - if (response.getStatusCode() != 200) { return false; } @@ -158,7 +154,9 @@ public class BindRequest { * @param description The server description (nullable) * @return A future that completes with true if the binding is successful */ - public CompletableFuture finalizeServerBinding(@Nullable String name, @Nullable String description) { + public CompletableFuture finalizeServerBinding( + @Nullable String name, @Nullable String description + ) { String path = "/server/server-bind"; JsonObject json = new JsonObject(); @@ -180,26 +178,20 @@ public class BindRequest { }); } - public class PlayerBindResponse implements Serializable { + public class PlayerBindStatusResponse implements Serializable { + @SerializedName("status") private PlayerBindStatusEnum status; - @SerializedName("status") - private PlayerBindStatus status; + @SerializedName("message") private String message; - @SerializedName("message") - private String message; + @SerializedName("cached") private boolean cached; - @SerializedName("cached") - private boolean cached; + @SerializedName("timestamp") private long timestamp; - @SerializedName("timestamp") - private long timestamp; - - @SerializedName("info") - private PlayerBindInfo info; + @SerializedName("info") private PlayerBindStatusInfo info; // --- Getters --- - public PlayerBindStatus getStatus() { + public PlayerBindStatusEnum getStatus() { return status; } @@ -215,11 +207,11 @@ public class BindRequest { return timestamp; } - public PlayerBindInfo getInfo() { + public PlayerBindStatusInfo getInfo() { return info; } - public void setStatus(PlayerBindStatus status) { + public void setStatus(PlayerBindStatusEnum status) { this.status = status; } @@ -228,35 +220,26 @@ public class BindRequest { } } - public static enum PlayerBindStatus { - @SerializedName("notBound") - NOT_BOUND, + public static enum PlayerBindStatusEnum { + @SerializedName("notBound") NOT_BOUND, - @SerializedName("bound") - BOUND, + @SerializedName("bound") BOUND, - @SerializedName("conflictUser") - CONFLICT_USER, + @SerializedName("conflictUser") CONFLICT_USER, - @SerializedName("conflictPlayer") - CONFLICT_PLAYER, + @SerializedName("conflictPlayer") CONFLICT_PLAYER, - @SerializedName("error") - ERROR + @SerializedName("error") ERROR } - public static class PlayerBindInfo implements Serializable { - @SerializedName("bound_uuid") - private String boundUuid; + public static class PlayerBindStatusInfo implements Serializable { + @SerializedName("bound_uuid") private String boundUuid; - @SerializedName("minecraft_uuid") - private String minecraftUuid; + @SerializedName("minecraft_uuid") private String minecraftUuid; - @SerializedName("bound_user_id") - private Integer boundUserId; + @SerializedName("bound_user_id") private Integer boundUserId; - @SerializedName("user_id") - private Integer userId; + @SerializedName("user_id") private Integer userId; // --- Getters --- @@ -285,20 +268,99 @@ public class BindRequest { } } - private PlayerBindResponse createErrorResponse(String errorMessage) { - PlayerBindResponse errorRes = new PlayerBindResponse(); - errorRes.setStatus(PlayerBindStatus.ERROR); + private PlayerBindStatusResponse createBindStatusErrorResponse(String errorMessage) { + PlayerBindStatusResponse errorRes = new PlayerBindStatusResponse(); + errorRes.setStatus(PlayerBindStatusEnum.ERROR); errorRes.setMessage(errorMessage); return errorRes; } - public CompletableFuture checkPlayerBindStatus(UUID player_uuid, String user_email) { + public CompletableFuture checkPlayerBindStatus( + UUID player_uuid, String user_email + ) { String path = "/server/user-bind-status"; Map params = new HashMap<>(); - params.put("user_email", user_email); - params.put("player_uuid", player_uuid.toString()); + params.put("web_email", user_email); + params.put("mc_uuid", player_uuid.toString()); + + return networkService.getData(path, params) + .thenApply(response -> { + Gson gson = new Gson(); + String responseBody = response.getBody(); + + if (response.getStatusCode() != 200) { + try { + return gson.fromJson(responseBody, PlayerBindStatusResponse.class); + } catch (Exception ignored) { + return createBindStatusErrorResponse("HTTP Error: " + response.getStatusCode()); + } + } + + try { + return gson.fromJson(responseBody, PlayerBindStatusResponse.class); + } catch (Exception e) { + logger.error("Mineroo Bind: Failed to parse JSON", e); + return createBindStatusErrorResponse("JSON Parse Error"); + } + }) + .exceptionally(e -> { + logger.error("Mineroo Bind: Network exception", e); + return createBindStatusErrorResponse("Network Error: " + e.getMessage()); + }); + } + + public class PlayerBindResponse implements Serializable { + @SerializedName("status") private PlayerBindEnum status; + @SerializedName("timestamp") private long timestamp; + @SerializedName("message") private String message; + + public PlayerBindEnum getStatus() { + return status; + } + + public String getMessage() { + return message; + } + + public void setStatus(PlayerBindEnum status) { + this.status = status; + } + + public void setMessage(String message) { + this.message = message; + } + } + + public static enum PlayerBindEnum { + + @SerializedName("pending") PENDING, + + @SerializedName("bound") BOUND, + + @SerializedName("expired") EXPIRED, + + @SerializedName("error") ERROR + } + + private PlayerBindResponse createPlayerBindErrorResponse(String errorMessage) { + PlayerBindResponse errorRes = new PlayerBindResponse(); + errorRes.setStatus(PlayerBindEnum.ERROR); + errorRes.setMessage(errorMessage); + return errorRes; + } + + public CompletableFuture PlayerBindRequest( + String webEmail, String playerUuid, String playerUsername + ) { + String path = "/server/user-bind"; + + Map params = new HashMap<>(); + + params.put("web_email", webEmail); + params.put("mc_uuid", playerUuid); + params.put("mc_username", playerUsername); return networkService.getData(path, params) .thenApply(response -> { @@ -309,7 +371,7 @@ public class BindRequest { try { return gson.fromJson(responseBody, PlayerBindResponse.class); } catch (Exception ignored) { - return createErrorResponse("HTTP Error: " + response.getStatusCode()); + return createPlayerBindErrorResponse("HTTP Error: " + response.getStatusCode()); } } @@ -317,12 +379,12 @@ public class BindRequest { return gson.fromJson(responseBody, PlayerBindResponse.class); } catch (Exception e) { logger.error("Mineroo Bind: Failed to parse JSON", e); - return createErrorResponse("JSON Parse Error"); + return createPlayerBindErrorResponse("JSON Parse Error"); } }) .exceptionally(e -> { logger.error("Mineroo Bind: Network exception", e); - return createErrorResponse("Network Error: " + e.getMessage()); + return createPlayerBindErrorResponse("Network Error: " + e.getMessage()); }); } diff --git a/paper/src/main/java/online/mineroo/paper/MinerooCoreBootstrap.java b/paper/src/main/java/online/mineroo/paper/MinerooCoreBootstrap.java index 366c8c8..c6a3f76 100644 --- a/paper/src/main/java/online/mineroo/paper/MinerooCoreBootstrap.java +++ b/paper/src/main/java/online/mineroo/paper/MinerooCoreBootstrap.java @@ -4,6 +4,7 @@ import io.papermc.paper.plugin.bootstrap.BootstrapContext; import io.papermc.paper.plugin.bootstrap.PluginBootstrap; import io.papermc.paper.registry.data.dialog.ActionButton; import io.papermc.paper.registry.data.dialog.DialogBase; +import io.papermc.paper.registry.data.dialog.action.DialogAction; import io.papermc.paper.registry.data.dialog.body.DialogBody; import io.papermc.paper.registry.data.dialog.input.DialogInput; import io.papermc.paper.registry.data.dialog.type.DialogType; @@ -16,16 +17,25 @@ import online.mineroo.common.MessageManager; public class MinerooCoreBootstrap implements PluginBootstrap { @Override public void bootstrap(BootstrapContext context) { + // TODO: language auto check from config MessageManager messageManager = new MessageManager(); - ActionButton confirmButton = - ActionButton.create(messageManager.get("dialog.bind.player.confirm"), null, 100, null); + ActionButton confirmButton = ActionButton.create( + messageManager.get("dialog.bind.player.confirm"), + null, + 100, + DialogAction.customClick(Key.key("mineroo:bind_user/confirm"), null) + ); - ActionButton cancelButton = - ActionButton.create(messageManager.get("dialog.bind.player.cancel"), null, 100, null); + ActionButton cancelButton = ActionButton.create( + messageManager.get("dialog.bind.player.cancel"), + null, + 100, + DialogAction.customClick(Key.key("mineroo:bind_user/cancel"), null) + ); DialogInput usernameInput = - DialogInput.text("username", messageManager.get("dialog.bind.player.email")).build(); + DialogInput.text("user_email", messageManager.get("dialog.bind.player.email")).build(); DialogBase dialogBase = DialogBase.builder(messageManager.get("dialog.bind.player.title")) @@ -40,7 +50,7 @@ public class MinerooCoreBootstrap implements PluginBootstrap { context.getLifecycleManager().registerEventHandler(RegistryEvents.DIALOG.compose().newHandler( event -> event.registry().register( - DialogKeys.create(Key.key("papermc:bind_mineroo_user")), + DialogKeys.create(Key.key("mineroo:bind_user")), builder -> builder.base(dialogBase).type(confirmationType) ) )); diff --git a/paper/src/main/java/online/mineroo/paper/listeners/BindListener.java b/paper/src/main/java/online/mineroo/paper/listeners/BindListener.java index 2fe5168..dc3d658 100644 --- a/paper/src/main/java/online/mineroo/paper/listeners/BindListener.java +++ b/paper/src/main/java/online/mineroo/paper/listeners/BindListener.java @@ -1,16 +1,13 @@ package online.mineroo.paper.listeners; +import java.util.concurrent.atomic.AtomicReference; +import net.kyori.adventure.text.Component; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.server.ServerListPingEvent; -import net.kyori.adventure.text.Component; - -import java.util.concurrent.atomic.AtomicReference; - public class BindListener implements Listener { - private final AtomicReference verificationToken = new AtomicReference<>(null); public void setVerificationToken(String token) { diff --git a/paper/src/main/java/online/mineroo/paper/listeners/DialogResponseView.java b/paper/src/main/java/online/mineroo/paper/listeners/DialogResponseView.java new file mode 100644 index 0000000..e69de29 diff --git a/paper/src/main/java/online/mineroo/paper/listeners/PlayerBindListener.java b/paper/src/main/java/online/mineroo/paper/listeners/PlayerBindListener.java index f6737b0..581a255 100644 --- a/paper/src/main/java/online/mineroo/paper/listeners/PlayerBindListener.java +++ b/paper/src/main/java/online/mineroo/paper/listeners/PlayerBindListener.java @@ -1,175 +1,167 @@ + package online.mineroo.paper.listeners; -import java.util.HashSet; -import java.util.Set; +import com.destroystokyo.paper.event.player.PlayerConnectionCloseEvent; +import io.papermc.paper.connection.PlayerConfigurationConnection; +import io.papermc.paper.connection.PlayerGameConnection; +import io.papermc.paper.dialog.Dialog; +import io.papermc.paper.dialog.DialogResponseView; +import io.papermc.paper.event.connection.configuration.AsyncPlayerConnectionConfigureEvent; +import io.papermc.paper.event.player.PlayerCustomClickEvent; +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; +import java.util.Map; import java.util.UUID; - -import org.bukkit.Location; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.block.BlockBreakEvent; -import org.bukkit.event.block.BlockPlaceEvent; -import org.bukkit.event.entity.EntityDamageEvent; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.event.player.PlayerDropItemEvent; -import org.bukkit.event.player.PlayerInteractEvent; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerMoveEvent; -import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; -import org.jspecify.annotations.NullMarked; - -import online.mineroo.common.BindRequest.PlayerBindStatus; -import online.mineroo.common.MessageManager; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import online.mineroo.common.BindRequest; +import online.mineroo.common.BindRequest.PlayerBindStatusEnum; import online.mineroo.paper.Config; import online.mineroo.paper.MinerooCore; -import online.mineroo.paper.utils.PlayerBindDialog; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.jspecify.annotations.NullMarked; +import org.slf4j.Logger; @NullMarked public class PlayerBindListener implements Listener { - private final MinerooCore plugin; - private Set pendingBindPlayers = new HashSet<>(); + private final Map> awaitingResponse = new ConcurrentHashMap<>(); public PlayerBindListener(MinerooCore plugin) { this.plugin = plugin; } @EventHandler - public void onPlayerJoin(PlayerJoinEvent event) { - - MessageManager messageManager = this.plugin.getMessageManager(); + public void onPlayerConfigure(AsyncPlayerConnectionConfigureEvent event) { Config config = this.plugin.getConfigObject(); - Player player = event.getPlayer(); - - boolean isForceBind = config.getPlayer().getPlayerBind().isRequired(); - - if (isForceBind) { - lockPlayer(player); - } - - this.plugin.getBindRequest().checkPlayerBindStatus(player.getUniqueId(), null) - .thenAccept(response -> { - - // 4. 切回主线程 (Bukkit API 必须在主线程调用) - // 这一步非常重要,否则报错 "Asynchronous player tracker update" - this.plugin.getServer().getScheduler().runTask(this.plugin, () -> { - - // 检查玩家是否还在同一个服务器 (防止请求期间玩家退出了) - if (!player.isOnline()) - return; - - PlayerBindStatus status = response.getStatus(); - - if (status == PlayerBindStatus.BOUND) { - if (isForceBind) { - unlockPlayer(player); - } - - } else { - if (status == PlayerBindStatus.ERROR) { - this.plugin.getLogger() - .warning("Bind check failed for " + player.getName() + ": " + response.getMessage()); - } - - PlayerBindDialog dialog = new PlayerBindDialog(); - player.showDialog(dialog.getDialog(messageManager)); - } - }); - }); - - } - - @EventHandler(priority = EventPriority.LOWEST) - public void onPlayerMove(PlayerMoveEvent event) { - Player player = event.getPlayer(); - - if (!pendingBindPlayers.contains(player.getUniqueId())) { + if (!config.getPlayer().getPlayerBind().isRequired()) { return; } - Location from = event.getFrom(); - Location to = event.getTo(); + UUID uuid = event.getConnection().getProfile().getId(); + if (uuid == null) + return; - if (from.getX() == to.getX() && from.getY() == to.getY() && from.getZ() == to.getZ()) { + try { + var response = this.plugin.getBindRequest() + .checkPlayerBindStatus(uuid, event.getConnection().getProfile().getName()) + .join(); + + PlayerBindStatusEnum status = response.getStatus(); + + if (status == PlayerBindStatusEnum.BOUND) { + return; + } else if (status == PlayerBindStatusEnum.NOT_BOUND) { + Dialog dialog = RegistryAccess.registryAccess() + .getRegistry(RegistryKey.DIALOG) + .get(Key.key("mineroo:bind_user")); + if (dialog == null) { + return; + } + + PlayerConfigurationConnection connection = event.getConnection(); + + UUID uniqueId = connection.getProfile().getId(); + + if (uniqueId == null) { + return; + } + + CompletableFuture dialogResponse = new CompletableFuture<>(); + dialogResponse.completeOnTimeout(false, 1, TimeUnit.MINUTES); + awaitingResponse.put(uniqueId, dialogResponse); + + Audience audience = connection.getAudience(); + audience.showDialog(dialog); + + if (!dialogResponse.join()) { + audience.closeDialog(); + connection.disconnect(Component.text("You hate Paper-chan :(", NamedTextColor.RED)); + } + awaitingResponse.remove(uniqueId); + + } else { + event.getConnection().disconnect( + Component.text("服务器内部错误,请稍后重试", NamedTextColor.RED) + ); + return; + } + + } catch (Exception e) { + this.plugin.getLogger().severe("Failed to check bind status for " + uuid); + e.printStackTrace(); + event.getConnection().disconnect( + Component.text("服务器内部错误,请稍后重试", NamedTextColor.RED) + ); + } + } + + @EventHandler + void onHandleDialog(PlayerCustomClickEvent event) { + Logger logger = plugin.getSLF4JLogger(); + + logger.info("12313213131231"); + + // Handle custom click only for configuration connection. + if (!(event.getCommonConnection() + instanceof PlayerConfigurationConnection configurationConnection)) { return; } - Location newLocation = from.clone(); - newLocation.setYaw(to.getYaw()); - newLocation.setPitch(to.getPitch()); - event.setTo(newLocation); - } + UUID uniqueId = configurationConnection.getProfile().getId(); - @EventHandler - public void onInteract(PlayerInteractEvent event) { - if (isLocked(event.getPlayer())) - event.setCancelled(true); - } + if (uniqueId == null) { + return; + } - @EventHandler - public void onDrop(PlayerDropItemEvent event) { - if (isLocked(event.getPlayer())) - event.setCancelled(true); - } + Key key = event.getIdentifier(); - @EventHandler - public void onInventoryClick(InventoryClickEvent event) { - if (event.getWhoClicked() instanceof Player player && isLocked(player)) { - event.setCancelled(true); + if (key.equals(Key.key("mineroo:bind_user/confirm"))) { + DialogResponseView view = event.getDialogResponseView(); + if (view == null) { + return; + } + + String user_email = view.getText("user_email"); + + if (event.getCommonConnection() instanceof PlayerConfigurationConnection conn) { + BindRequest bindRequest = plugin.getBindRequest(); + } + + setConnectionJoinResult(uniqueId, true); + + } else if (key.equals(Key.key("mineroo:bind_user/cancel"))) { + // If it is the same as the agree one, set the result to true. + + setConnectionJoinResult(uniqueId, false); } } @EventHandler - public void onDamage(EntityDamageEvent event) { - if (event.getEntity() instanceof Player player && isLocked(player)) { - event.setCancelled(true); - } + void onConnectionClose(PlayerConnectionCloseEvent event) { + awaitingResponse.remove(event.getPlayerUniqueId()); } - @EventHandler - public void onBlockBreak(BlockBreakEvent event) { - if (isLocked(event.getPlayer())) - event.setCancelled(true); - } + /** - @EventHandler - public void onBlockPlace(BlockPlaceEvent event) { - if (isLocked(event.getPlayer())) - event.setCancelled(true); - } + * Simple utility method for setting a connection's dialog response result. - @EventHandler - public void onChat(io.papermc.paper.event.player.AsyncChatEvent event) { - if (isLocked(event.getPlayer())) - event.setCancelled(true); - } + */ - private boolean isLocked(Player player) { - return pendingBindPlayers.contains(player.getUniqueId()); - } + private void setConnectionJoinResult(UUID uniqueId, boolean value) { + CompletableFuture future = awaitingResponse.get(uniqueId); - private void lockPlayer(Player player) { - pendingBindPlayers.add(player.getUniqueId()); - - player.setWalkSpeed(0f); - player.setFlySpeed(0f); - - // player.addPotionEffect(new PotionEffect(PotionEffectType.JUMP, - // Integer.MAX_VALUE, 200, false, false)); - player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, Integer.MAX_VALUE, 1, false, false)); - } - - public void unlockPlayer(Player player) { - if (pendingBindPlayers.remove(player.getUniqueId())) { - player.setWalkSpeed(0.2f); - player.setFlySpeed(0.1f); - // player.removePotionEffect(PotionEffectType.JUMP); - player.removePotionEffect(PotionEffectType.BLINDNESS); + if (future != null) { + future.complete(value); } } } diff --git a/paper/src/main/java/online/mineroo/paper/utils/PlayerBindDialog.java b/paper/src/main/java/online/mineroo/paper/utils/PlayerBindDialog.java deleted file mode 100644 index d990ec2..0000000 --- a/paper/src/main/java/online/mineroo/paper/utils/PlayerBindDialog.java +++ /dev/null @@ -1,34 +0,0 @@ -package online.mineroo.paper.utils; - -import java.util.List; - -import io.papermc.paper.dialog.Dialog; -import io.papermc.paper.registry.data.dialog.ActionButton; -import io.papermc.paper.registry.data.dialog.DialogBase; -import io.papermc.paper.registry.data.dialog.body.DialogBody; -import io.papermc.paper.registry.data.dialog.input.DialogInput; -import io.papermc.paper.registry.data.dialog.type.DialogType; -import online.mineroo.common.MessageManager; - -public class PlayerBindDialog { - public Dialog getDialog(MessageManager messageManager) { - - Dialog dialog = Dialog.create(builder -> builder.empty() - .base(DialogBase.builder(messageManager.get("dialog.bind.player.title")) - .body(List.of(DialogBody.plainMessage(messageManager.get("dialog.bind.player.content")))) - .inputs( - List.of( - // username - DialogInput.text("username", messageManager.get("dialog.bind.player.email")).build() - // ... - )) - .build()) - .type(DialogType.confirmation( - ActionButton.create(messageManager.get("dialog.bind.player.confirm"), null, 100, null), - ActionButton.create(messageManager.get("dialog.bind.player.cancel"), null, 100, null) - // ... - ))); - - return dialog; - } -} diff --git a/paper/src/main/resources/paper-plugin.yml b/paper/src/main/resources/paper-plugin.yml new file mode 100644 index 0000000..9563bbe --- /dev/null +++ b/paper/src/main/resources/paper-plugin.yml @@ -0,0 +1,6 @@ +name: MinerooCore +version: "1.0" +main: "online.mineroo.paper.MinerooCore" +description: Paper Test Plugin +api-version: "1.21.10" +bootstrapper: "online.mineroo.paper.MinerooCoreBootstrap" diff --git a/velocity/src/main/java/online/mineroo/velocity/listeners/BindListener.java b/velocity/src/main/java/online/mineroo/velocity/listeners/BindListener.java index 06c5df7..dd982ac 100644 --- a/velocity/src/main/java/online/mineroo/velocity/listeners/BindListener.java +++ b/velocity/src/main/java/online/mineroo/velocity/listeners/BindListener.java @@ -3,12 +3,10 @@ package online.mineroo.velocity.listeners; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.proxy.ProxyPingEvent; import com.velocitypowered.api.proxy.server.ServerPing; +import java.util.concurrent.atomic.AtomicReference; import net.kyori.adventure.text.Component; -import java.util.concurrent.atomic.AtomicReference; - public class BindListener { - private final AtomicReference verificationToken = new AtomicReference<>(null); public void setVerificationToken(String token) {