Prefixes & suffixes, fix to Placeholders utility class, added more tests

This commit is contained in:
Vankka 2022-04-02 18:40:41 +03:00
parent bbc722e689
commit b699a75821
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
15 changed files with 196 additions and 37 deletions

View File

@ -1,24 +1,28 @@
/* /*
* This file is part of DiscordSRV, licensed under the GPLv3 License * This file is part of the DiscordSRV API, licensed under the MIT License
* Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
* *
* This program is free software: you can redistribute it and/or modify * Permission is hereby granted, free of charge, to any person obtaining a copy
* it under the terms of the GNU General Public License as published by * of this software and associated documentation files (the "Software"), to deal
* the Free Software Foundation, either version 3 of the License, or * in the Software without restriction, including without limitation the rights
* (at your option) any later version. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* *
* This program is distributed in the hope that it will be useful, * The above copyright notice and this permission notice shall be included in all
* but WITHOUT ANY WARRANTY; without even the implied warranty of * copies or substantial portions of the Software.
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* along with this program. If not, see <https://www.gnu.org/licenses/>. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/ */
package com.discordsrv.common.module.type; package com.discordsrv.api.module.type;
import com.discordsrv.api.module.type.Module;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;

View File

@ -29,9 +29,10 @@ import org.jetbrains.annotations.NotNull;
public interface PlaceholderResultMapper { public interface PlaceholderResultMapper {
/** /**
* Converts a successful placeholder lookup result into a {@link String}. * Converts a successful placeholder lookup result into an object that is better suited to be used in place of the result.
* @param result the result * @param result the result
* @return the result in {@link String} form or {@code null} if this stringifier doesn't know what to do with this result * @return the result in the form that should be used for converting to a {@link String} or used directly by placeholder service users
* or {@code null} if this mapper doesn't know what to do with this result
*/ */
Object convertResult(@NotNull Object result); Object convertResult(@NotNull Object result);

View File

@ -85,16 +85,20 @@ public class Placeholders {
for (Map.Entry<Pattern, Function<Matcher, Object>> entry : replacements.entrySet()) { for (Map.Entry<Pattern, Function<Matcher, Object>> entry : replacements.entrySet()) {
Pattern pattern = entry.getKey(); Pattern pattern = entry.getKey();
Matcher matcher = pattern.matcher(input); Matcher matcher = pattern.matcher(input);
if (!matcher.find()) { StringBuffer buffer = new StringBuffer();
continue; int lastEnd = -1;
} while (matcher.find()) {
lastEnd = matcher.end();
Function<Matcher, Object> replacement = entry.getValue(); Function<Matcher, Object> replacement = entry.getValue();
Object value = replacement.apply(matcher); Object value = replacement.apply(matcher);
input = matcher.replaceAll( matcher.appendReplacement(buffer, Matcher.quoteReplacement(String.valueOf(value)));
Matcher.quoteReplacement( }
String.valueOf(value))); if (lastEnd == -1) {
continue;
}
buffer.append(input.substring(lastEnd));
input = buffer.toString();
} }
return input; return input;
} }

View File

@ -25,6 +25,8 @@ package com.discordsrv.api.placeholder.util;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
public class PlaceholdersTest { public class PlaceholdersTest {
@ -38,4 +40,40 @@ public class PlaceholdersTest {
assertEquals("b", placeholders.toString()); assertEquals("b", placeholders.toString());
} }
@Test
public void uselessContentTest() {
Placeholders placeholders = new Placeholders("stuff a stuff");
placeholders.replace("a", "b");
assertEquals("stuff b stuff", placeholders.toString());
}
@Test
public void multipleTest() {
Placeholders placeholders = new Placeholders("a b");
placeholders.replace("a", "c");
placeholders.replace("b", "d");
assertEquals("c d", placeholders.toString());
}
@Test
public void multipleSamePatternTest() {
Placeholders placeholders = new Placeholders("a a");
AtomicBoolean used = new AtomicBoolean(false);
placeholders.replace("a", matcher -> {
if (used.get()) {
return "c";
} else {
used.set(true);
return "b";
}
});
assertEquals("b c", placeholders.toString());
}
} }

View File

@ -22,7 +22,7 @@ import com.discordsrv.bukkit.BukkitDiscordSRV;
import com.discordsrv.common.exception.MessageException; import com.discordsrv.common.exception.MessageException;
import com.discordsrv.common.function.CheckedSupplier; import com.discordsrv.common.function.CheckedSupplier;
import com.discordsrv.common.future.util.CompletableFutureUtil; import com.discordsrv.common.future.util.CompletableFutureUtil;
import com.discordsrv.common.module.type.PermissionDataProvider; import com.discordsrv.api.module.type.PermissionDataProvider;
import com.discordsrv.common.module.type.PluginIntegration; import com.discordsrv.common.module.type.PluginIntegration;
import net.milkbowl.vault.chat.Chat; import net.milkbowl.vault.chat.Chat;
import net.milkbowl.vault.permission.Permission; import net.milkbowl.vault.permission.Permission;

View File

@ -62,7 +62,7 @@ import com.discordsrv.common.messageforwarding.game.StartMessageModule;
import com.discordsrv.common.messageforwarding.game.StopMessageModule; import com.discordsrv.common.messageforwarding.game.StopMessageModule;
import com.discordsrv.common.module.ModuleManager; import com.discordsrv.common.module.ModuleManager;
import com.discordsrv.common.module.type.AbstractModule; import com.discordsrv.common.module.type.AbstractModule;
import com.discordsrv.common.placeholder.ComponentResultStringifier; import com.discordsrv.common.placeholder.result.ComponentResultStringifier;
import com.discordsrv.common.placeholder.PlaceholderServiceImpl; import com.discordsrv.common.placeholder.PlaceholderServiceImpl;
import com.discordsrv.common.placeholder.context.GlobalTextHandlingContext; import com.discordsrv.common.placeholder.context.GlobalTextHandlingContext;
import com.discordsrv.common.profile.ProfileManager; import com.discordsrv.common.profile.ProfileManager;

View File

@ -35,7 +35,7 @@ public class MinecraftToDiscordChatConfig implements IMessageConfig {
@Untranslated(Untranslated.Type.VALUE) @Untranslated(Untranslated.Type.VALUE)
public SendableDiscordMessage.Builder format = SendableDiscordMessage.builder() public SendableDiscordMessage.Builder format = SendableDiscordMessage.builder()
.setWebhookUsername("%player_display_name%") .setWebhookUsername("%player_meta_prefix|player_prefix%%player_display_name|player_name%%player_meta_suffix|player_suffix%")
.setWebhookAvatarUrl("%player_avatar_url%") .setWebhookAvatarUrl("%player_avatar_url%")
.setContent("%message%"); .setContent("%message%");

View File

@ -34,7 +34,7 @@ import com.discordsrv.common.groupsync.enums.GroupSyncResult;
import com.discordsrv.common.groupsync.enums.GroupSyncSide; import com.discordsrv.common.groupsync.enums.GroupSyncSide;
import com.discordsrv.common.logging.NamedLogger; import com.discordsrv.common.logging.NamedLogger;
import com.discordsrv.common.module.type.AbstractModule; import com.discordsrv.common.module.type.AbstractModule;
import com.discordsrv.common.module.type.PermissionDataProvider; import com.discordsrv.api.module.type.PermissionDataProvider;
import com.discordsrv.common.player.IPlayer; import com.discordsrv.common.player.IPlayer;
import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Cache;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;

View File

@ -23,7 +23,7 @@ import com.discordsrv.common.exception.MessageException;
import com.discordsrv.common.future.util.CompletableFutureUtil; import com.discordsrv.common.future.util.CompletableFutureUtil;
import com.discordsrv.common.groupsync.GroupSyncModule; import com.discordsrv.common.groupsync.GroupSyncModule;
import com.discordsrv.common.groupsync.enums.GroupSyncCause; import com.discordsrv.common.groupsync.enums.GroupSyncCause;
import com.discordsrv.common.module.type.PermissionDataProvider; import com.discordsrv.api.module.type.PermissionDataProvider;
import com.discordsrv.common.module.type.PluginIntegration; import com.discordsrv.common.module.type.PluginIntegration;
import net.luckperms.api.LuckPerms; import net.luckperms.api.LuckPerms;
import net.luckperms.api.LuckPermsProvider; import net.luckperms.api.LuckPermsProvider;

View File

@ -121,7 +121,7 @@ public abstract class AbstractGameMessageModule<T extends IMessageConfig> extend
messageFutures = sendMessageToChannels( messageFutures = sendMessageToChannels(
moduleConfig, format, messageChannels, message, moduleConfig, format, messageChannels, message,
// Context // Context
channelConfig, player config, player
); );
return CompletableFuture.allOf(messageFutures.keySet().toArray(new CompletableFuture[0])) return CompletableFuture.allOf(messageFutures.keySet().toArray(new CompletableFuture[0]))

View File

@ -0,0 +1,78 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.permission.util;
import com.discordsrv.api.module.type.PermissionDataProvider;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.component.util.ComponentUtil;
import net.kyori.adventure.text.Component;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
public final class PermissionUtil {
public static final String PREFIX_META_KEY = "discordsrv_prefix";
public static final String SUFFIX_META_KEY = "discordsrv_suffix";
private PermissionUtil() {}
public static Component getMetaPrefix(DiscordSRV discordSRV, UUID uuid) {
return getMeta(discordSRV, uuid, PREFIX_META_KEY);
}
public static Component getMetaSuffix(DiscordSRV discordSRV, UUID uuid) {
return getMeta(discordSRV, uuid, SUFFIX_META_KEY);
}
public static Component getPrefix(DiscordSRV discordSRV, UUID uuid) {
return getLegacy(discordSRV, perm -> perm.getPrefix(uuid));
}
public static Component getSuffix(DiscordSRV discordSRV, UUID uuid) {
return getLegacy(discordSRV, perm -> perm.getSuffix(uuid));
}
private static Component getMeta(DiscordSRV discordSRV, UUID uuid, String metaKey) {
PermissionDataProvider.Meta meta = discordSRV.getModule(PermissionDataProvider.Meta.class);
if (meta == null) {
return null;
}
String data = meta.getMeta(uuid, metaKey).join();
return translate(discordSRV, data);
}
private static Component getLegacy(
DiscordSRV discordSRV,
Function<PermissionDataProvider.PrefixAndSuffix, CompletableFuture<String>> legacy
) {
PermissionDataProvider.PrefixAndSuffix permission = discordSRV.getModule(PermissionDataProvider.PrefixAndSuffix.class);
if (permission == null) {
return null;
}
String data = legacy.apply(permission).join();
return translate(discordSRV, data);
}
private static Component translate(DiscordSRV discordSRV, String data) {
return data != null ? ComponentUtil.fromAPI(discordSRV.componentFactory().enhancedBuilder(data).build()) : null;
}
}

View File

@ -19,11 +19,11 @@
package com.discordsrv.common.placeholder; package com.discordsrv.common.placeholder;
import com.discordsrv.api.event.events.placeholder.PlaceholderLookupEvent; import com.discordsrv.api.event.events.placeholder.PlaceholderLookupEvent;
import com.discordsrv.api.placeholder.annotation.Placeholder;
import com.discordsrv.api.placeholder.PlaceholderLookupResult; import com.discordsrv.api.placeholder.PlaceholderLookupResult;
import com.discordsrv.api.placeholder.mapper.PlaceholderResultMapper;
import com.discordsrv.api.placeholder.PlaceholderService; import com.discordsrv.api.placeholder.PlaceholderService;
import com.discordsrv.api.placeholder.annotation.Placeholder;
import com.discordsrv.api.placeholder.annotation.PlaceholderRemainder; import com.discordsrv.api.placeholder.annotation.PlaceholderRemainder;
import com.discordsrv.api.placeholder.mapper.PlaceholderResultMapper;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.placeholder.provider.AnnotationPlaceholderProvider; import com.discordsrv.common.placeholder.provider.AnnotationPlaceholderProvider;
import com.discordsrv.common.placeholder.provider.PlaceholderProvider; import com.discordsrv.common.placeholder.provider.PlaceholderProvider;
@ -182,14 +182,14 @@ public class PlaceholderServiceImpl implements PlaceholderService {
private String getResultAsString(Object result) { private String getResultAsString(Object result) {
if (result == null) { if (result == null) {
return "null"; return "";
} else if (result instanceof CharSequence) { } else if (result instanceof CharSequence) {
return result.toString(); return result.toString();
} }
Object output = null; Object output = null;
for (PlaceholderResultMapper stringifier : mappers) { for (PlaceholderResultMapper mapper : mappers) {
output = stringifier.convertResult(result); output = mapper.convertResult(result);
if (output != null) { if (output != null) {
break; break;
} }
@ -240,7 +240,10 @@ public class PlaceholderServiceImpl implements PlaceholderService {
switch (type) { switch (type) {
case SUCCESS: case SUCCESS:
replacement = result.getValue(); replacement = result.getValue();
if (replacement != null && StringUtils.isNotBlank(getResultAsString(replacement))) { if (replacement == null) {
replacement = getResultAsString(null);
}
if (StringUtils.isNotBlank(getResultAsString(replacement))) {
return replacement; return replacement;
} }
break; break;

View File

@ -49,7 +49,6 @@ public final class PlaceholderMethodUtil {
for (Object o : context) { for (Object o : context) {
Class<?> objectType = o.getClass(); Class<?> objectType = o.getClass();
apply(parameters, (parameter, i) -> { apply(parameters, (parameter, i) -> {
Class<?> type = parameter.getType();
if (parameter.getType().isAssignableFrom(objectType)) { if (parameter.getType().isAssignableFrom(objectType)) {
parameters[i] = null; parameters[i] = null;
parameterValues[i] = o; parameterValues[i] = o;

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.discordsrv.common.placeholder; package com.discordsrv.common.placeholder.result;
import com.discordsrv.api.component.MinecraftComponent; import com.discordsrv.api.component.MinecraftComponent;
import com.discordsrv.api.placeholder.mapper.PlaceholderResultMapper; import com.discordsrv.api.placeholder.mapper.PlaceholderResultMapper;

View File

@ -25,6 +25,7 @@ import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.command.game.sender.ICommandSender; import com.discordsrv.common.command.game.sender.ICommandSender;
import com.discordsrv.common.config.main.channels.base.BaseChannelConfig; import com.discordsrv.common.config.main.channels.base.BaseChannelConfig;
import com.discordsrv.common.function.OrDefault; import com.discordsrv.common.function.OrDefault;
import com.discordsrv.common.permission.util.PermissionUtil;
import com.discordsrv.common.profile.Profile; import com.discordsrv.common.profile.Profile;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
@ -35,6 +36,7 @@ import java.util.UUID;
public interface IPlayer extends DiscordSRVPlayer, IOfflinePlayer, ICommandSender { public interface IPlayer extends DiscordSRVPlayer, IOfflinePlayer, ICommandSender {
@Override
DiscordSRV discordSRV(); DiscordSRV discordSRV();
@ApiStatus.NonExtendable @ApiStatus.NonExtendable
@ -47,6 +49,7 @@ public interface IPlayer extends DiscordSRVPlayer, IOfflinePlayer, ICommandSende
@Override @Override
@ApiStatus.NonExtendable @ApiStatus.NonExtendable
@Placeholder("player_uuid")
default @NotNull UUID uniqueId() { default @NotNull UUID uniqueId() {
return identity().uuid(); return identity().uuid();
} }
@ -56,6 +59,7 @@ public interface IPlayer extends DiscordSRVPlayer, IOfflinePlayer, ICommandSende
Component displayName(); Component displayName();
@Nullable @Nullable
@ApiStatus.NonExtendable
@Placeholder("player_avatar_url") @Placeholder("player_avatar_url")
default String getAvatarUrl(OrDefault<BaseChannelConfig> config) { default String getAvatarUrl(OrDefault<BaseChannelConfig> config) {
String avatarUrlProvider = config.get(cfg -> cfg.avatarUrlProvider); String avatarUrlProvider = config.get(cfg -> cfg.avatarUrlProvider);
@ -70,4 +74,32 @@ public interface IPlayer extends DiscordSRVPlayer, IOfflinePlayer, ICommandSende
.toString(); .toString();
} }
@Nullable
@ApiStatus.NonExtendable
@Placeholder("player_meta_prefix")
default Component getMetaPrefix() {
return PermissionUtil.getMetaPrefix(discordSRV(), uniqueId());
}
@Nullable
@ApiStatus.NonExtendable
@Placeholder("player_meta_suffix")
default Component getMetaSuffix() {
return PermissionUtil.getMetaSuffix(discordSRV(), uniqueId());
}
@Nullable
@ApiStatus.NonExtendable
@Placeholder("player_prefix")
default Component getPrefix() {
return PermissionUtil.getPrefix(discordSRV(), uniqueId());
}
@Nullable
@ApiStatus.NonExtendable
@Placeholder("player_suffix")
default Component getSuffix() {
return PermissionUtil.getSuffix(discordSRV(), uniqueId());
}
} }