ProjectAres/Commons/bukkit/src/main/java/tc/oc/commons/bukkit/nick/NicknameCommands.java

376 lines
15 KiB
Java

package tc.oc.commons.bukkit.nick;
import com.google.common.base.Function;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.sk89q.minecraft.util.commands.Command;
import com.sk89q.minecraft.util.commands.CommandContext;
import com.sk89q.minecraft.util.commands.CommandException;
import com.sk89q.minecraft.util.commands.CommandPermissions;
import java.time.Duration;
import java.time.Instant;
import java.util.Objects;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TranslatableComponent;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionAttachment;
import org.bukkit.permissions.PermissionDefault;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;
import tc.oc.api.bukkit.users.BukkitUserStore;
import tc.oc.api.bukkit.users.OnlinePlayers;
import tc.oc.api.docs.PlayerId;
import tc.oc.api.docs.User;
import tc.oc.api.docs.virtual.UserDoc;
import tc.oc.api.exceptions.UnprocessableEntity;
import tc.oc.api.users.UserService;
import tc.oc.commons.bukkit.chat.Audiences;
import tc.oc.commons.bukkit.chat.NameStyle;
import tc.oc.commons.bukkit.chat.PlayerComponent;
import tc.oc.commons.bukkit.chat.WarningComponent;
import tc.oc.commons.bukkit.commands.CommandUtils;
import tc.oc.commons.bukkit.commands.UserFinder;
import tc.oc.commons.bukkit.event.UserLoginEvent;
import tc.oc.commons.bukkit.localization.Translations;
import tc.oc.commons.core.chat.Audience;
import tc.oc.commons.core.chat.Component;
import tc.oc.commons.core.commands.CommandFutureCallback;
import tc.oc.commons.core.commands.Commands;
import tc.oc.commons.core.commands.ComponentCommandException;
import tc.oc.commons.core.commands.TranslatableCommandException;
import tc.oc.commons.core.formatting.PeriodFormats;
import tc.oc.minecraft.scheduler.SyncExecutor;
@Singleton
public class NicknameCommands implements Listener, Commands {
public static final String PERMISSION = "setting.nick";
public static final String PERMISSION_SET = PERMISSION + ".set";
public static final String PERMISSION_GET = PERMISSION + ".get";
public static final String PERMISSION_IMMEDIATE = PERMISSION + ".immediate";
public static final String PERMISSION_ANY = PERMISSION + ".any";
public static final String PERMISSION_ANY_SET = PERMISSION_ANY + ".set";
public static final String PERMISSION_ANY_GET = PERMISSION_ANY + ".get";
public static final String PERMISSION_UNLIMITED = PERMISSION + ".unlimited";
private final NicknameConfiguration config;
private final SyncExecutor syncExecutor;
private final BukkitUserStore userStore;
private final UserService userService;
private final Audiences audiences;
private final IdentityProvider identities;
private final OnlinePlayers onlinePlayers;
private final UserFinder userFinder;
private final PluginManager pluginManager;
private final Plugin plugin;
@Inject NicknameCommands(NicknameConfiguration config,
SyncExecutor syncExecutor,
BukkitUserStore userStore,
UserService userService,
Audiences audiences,
IdentityProvider identities,
OnlinePlayers onlinePlayers,
UserFinder userFinder,
PluginManager pluginManager,
Plugin plugin) {
this.config = config;
this.syncExecutor = syncExecutor;
this.userStore = userStore;
this.userService = userService;
this.audiences = audiences;
this.identities = identities;
this.onlinePlayers = onlinePlayers;
this.userFinder = userFinder;
this.pluginManager = pluginManager;
this.plugin = plugin;
}
@Override
public void enable() {
final PermissionAttachment attachment = Bukkit.getConsoleSender().addAttachment(plugin);
Stream.of(
PERMISSION,
PERMISSION_GET,
PERMISSION_SET,
PERMISSION_ANY,
PERMISSION_ANY_GET,
PERMISSION_ANY_SET,
PERMISSION_IMMEDIATE,
PERMISSION_UNLIMITED
).forEach(name -> {
final Permission permission = new Permission(name, PermissionDefault.FALSE);
pluginManager.addPermission(permission);
attachment.setPermission(permission, true);
});
}
private static boolean isSelf(CommandSender sender, @Nullable String username) {
return username == null || username.equals(sender.getName());
}
private void assertWritePerms(CommandSender sender, boolean self, boolean immediate) throws CommandException {
if(self) {
CommandUtils.assertPermission(sender, PERMISSION_SET);
} else {
CommandUtils.assertPermission(sender, PERMISSION_ANY_SET);
}
if(immediate) {
CommandUtils.assertPermission(sender, PERMISSION_IMMEDIATE);
}
if(sender instanceof Player) {
Instant updatedAt = userStore.getUser((Player) sender).nickname_updated_at();
Instant nextAt = updatedAt == null ? Instant.now() : updatedAt.plus(Duration.ofDays(1));
if(!sender.hasPermission(PERMISSION_UNLIMITED) && nextAt.isAfter(Instant.now())) {
throw new TranslatableCommandException(
"command.nick.mustWait",
PeriodFormats.relativeFutureApproximate(nextAt)
);
}
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void sendNickReminderOnLogin(UserLoginEvent event) {
if(event.getUser().nickname() != null) {
if(event.getPlayer().hasPermission(PERMISSION_SET)) {
audiences.get(event.getPlayer()).sendMessage(new TranslatableComponent(
"nick.joinReminder",
new Component("/nick", ChatColor.GOLD),
new Component("/nick clear", ChatColor.GOLD)
));
} else {
set(event.getUser(), null, true);
}
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void sendNickChangeMessage(PlayerIdentityChangeEvent event) {
if(event.getNewIdentity().getNickname() == null) {
audiences.get(event.getPlayer()).sendMessage(new TranslatableComponent("command.nick.clearSelf.immediate"));
} else {
audiences.get(event.getPlayer()).sendMessage(new TranslatableComponent("command.nick.setSelf.immediate", highlight(event.getNewIdentity().getNickname())));
}
}
@Command(aliases = {"nick" },
usage = "list | show [player] | set <nickname> [player] | clear [player] | <nickname> [player]",
desc = "Show, set, or clear a nickname for yourself or another player. " +
"Changes will take effect the next time the player " +
"connects to the server. The -i option makes the change " +
"visible immediately.",
flags = "i",
min = 0,
max = 3)
@CommandPermissions(PERMISSION)
public void nick(final CommandContext args, final CommandSender sender) throws CommandException {
if(!config.enabled()) {
throw new CommandException(Translations.get().t("command.nick.notEnabled", sender));
}
final boolean immediate = args.hasFlag('i');
if(args.argsLength() == 0) {
show(sender, null);
} else {
final String arg = args.getString(0);
switch(arg) {
case "list":
list(sender);
break;
case "show":
show(sender, args.getString(1, null));
break;
case "set":
if(args.argsLength() < 2) CommandUtils.notEnoughArguments(sender);
set(sender, args.getString(1), args.getString(2, null), immediate);
break;
case "clear":
set(sender, null, args.getString(1, null), immediate);
break;
default:
set(sender, arg, args.getString(1, null), immediate);
break;
}
}
}
public void list(final CommandSender sender) throws CommandException {
CommandUtils.assertPermission(sender, PERMISSION_ANY_GET);
final Audience audience = audiences.get(sender);
boolean some = false;
for(Player player : onlinePlayers.all()) {
final Identity identity = identities.currentIdentity(player);
if(identity.getNickname() != null) {
some = true;
audience.sendMessage(new PlayerComponent(identity, NameStyle.VERBOSE));
}
}
if(!some) {
audience.sendMessage(new TranslatableComponent("command.nick.noActiveNicks"));
}
}
public void show(final CommandSender sender, final @Nullable String username) throws CommandException {
final boolean self = isSelf(sender, username);
final Audience audience = audiences.get(sender);
syncExecutor.callback(
userFinder.findUser(sender, username, UserFinder.Scope.ALL, UserFinder.Default.SENDER),
CommandFutureCallback.onSuccess(sender, result -> {
final Identity identity = identities.currentIdentity(result.user);
if(self || identity.isFriend(sender)) {
CommandUtils.assertPermission(sender, PERMISSION_GET);
} else {
CommandUtils.assertPermission(sender, PERMISSION_ANY_GET);
}
final String currentNick = identity.getNickname();
final String pendingNick = result.user.nickname();
final String who = self ? "Self" : "Other";
final PlayerComponent name = self ? null : new PlayerComponent(identity, NameStyle.FANCY);
TranslatableComponent message = null;
if(currentNick != null) {
message = new TranslatableComponent("command.nick.set" + who + ".immediate", highlight(currentNick));
} else if(pendingNick == null) {
message = new TranslatableComponent("command.nick.clear" + who + ".immediate");
}
if(message != null) {
if(name != null) message.addWith(name);
audience.sendMessage(message);
}
if(!Objects.equals(currentNick, pendingNick)) {
if(pendingNick != null) {
message = new TranslatableComponent("command.nick.set" + who + ".queued", highlight(pendingNick));
} else {
message = new TranslatableComponent("command.nick.clear" + who + ".queued");
}
if(name != null) message.addWith(name);
audience.sendMessage(message);
}
})
);
}
private static Component highlight(String nickname) {
return new Component(nickname, ChatColor.AQUA);
}
public @Nullable BaseComponent invalidReason(@Nullable String nickname) {
if(nickname == null) return null;
if(!nickname.matches("^[A-Za-z0-9_]+$")) {
return new TranslatableComponent("command.nick.invalidCharacters");
}
if(nickname.length() > 16) {
return new TranslatableComponent("command.nick.tooLong");
}
if(nickname.length() < 4) {
return new TranslatableComponent("command.nick.tooShort");
}
if(onlinePlayers.find(nickname) != null) {
return new TranslatableComponent("command.nick.nickTaken", highlight(nickname));
}
return null;
}
private void validate(@Nullable String nickname) throws CommandException {
final BaseComponent reason = invalidReason(nickname);
if(reason != null) {
throw new ComponentCommandException(reason);
}
}
public void set(final CommandSender sender, final @Nullable String nickname, final @Nullable String username, final boolean immediate) throws CommandException {
final boolean self = isSelf(sender, username);
// Don't need perms to clear your own nickname, only to set it
if(!(self && nickname == null)) {
assertWritePerms(sender, self, immediate);
}
final Audience audience = audiences.get(sender);
if(nickname != null) {
validate(nickname);
audience.sendMessage(new TranslatableComponent("command.nick.checkingNickname", highlight(nickname)));
}
// TODO: a way to do this with only one API call instead of two
syncExecutor.callback(
userFinder.findUser(sender, username, UserFinder.Scope.ALL, UserFinder.Default.SENDER),
CommandFutureCallback.onSuccess(sender, response -> {
syncExecutor.callback(
set(response.user, nickname, immediate),
CommandFutureCallback.<User>onSuccess(sender, user -> {
if(!(self && immediate)) {
final String key = "command.nick." +
(nickname == null ? "clear" : "set") +
(self ? "Self" : "Other") +
(immediate ? ".immediate" : ".queued");
final TranslatableComponent message = new TranslatableComponent(key);
if(nickname != null) message.addWith(highlight(nickname));
if(!self) message.addWith(new PlayerComponent(identities.createIdentity(user, null), NameStyle.FANCY));
audience.sendMessage(message);
}
}).onFailure(UnprocessableEntity.class, ex -> {
// Assume any validation error is a username collision
audience.sendMessage(new WarningComponent("command.nick.nickTaken", highlight(nickname)));
})
);
})
);
}
public ListenableFuture<User> set(PlayerId playerId, @Nullable String nickname, boolean immediate) {
final BaseComponent reason = invalidReason(nickname);
if(reason != null) {
return Futures.immediateFailedFuture(new ComponentCommandException(reason));
}
return Futures.transform(
userService.update(playerId, (UserDoc.Nickname) () -> nickname),
(Function<User, User>) user -> {
if(immediate) {
final Player player = onlinePlayers.find(user);
if(player != null) {
identities.changeIdentity(player, nickname);
}
}
return user;
},
syncExecutor
);
}
}