mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2024-11-01 00:10:12 +01:00
Compare commits
75 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
0b620ca897 | ||
|
e659474dc3 | ||
|
922dc0b0d5 | ||
|
8a23b98ce5 | ||
|
511b41eb68 | ||
|
ac74d3cd4e | ||
|
ac3bffb484 | ||
|
c8fee67f10 | ||
|
bea0ef85b3 | ||
|
8309f8725e | ||
|
8e6befc953 | ||
|
41ae9a3a70 | ||
|
18a154e6b0 | ||
|
3e30340aa8 | ||
|
4b67a89d8c | ||
|
c5513cd5db | ||
|
cde66f1981 | ||
|
4481c6411e | ||
|
99d22185bb | ||
|
26fd2992a8 | ||
|
5472144f74 | ||
|
dddb9186b2 | ||
|
c5f811f99e | ||
|
b2deda8a28 | ||
|
edb871661c | ||
|
ad98e28e4c | ||
|
365ea2d333 | ||
|
fefbd9ae23 | ||
|
5e0780be1a | ||
|
3dc6ac5afa | ||
|
8a16455e95 | ||
|
de75d3c56f | ||
|
7de0464137 | ||
|
8b2f32bb7d | ||
|
ca9870d9fe | ||
|
9e25f2b26c | ||
|
24a8c75b67 | ||
|
132fa2f919 | ||
|
7f268d9a07 | ||
|
be8c8951c9 | ||
|
e630db1b71 | ||
|
0d5d9adb77 | ||
|
590e0445cb | ||
|
bd4108a367 | ||
|
faa911e232 | ||
|
6776c79a14 | ||
|
13d168a593 | ||
|
634d6f2a77 | ||
|
74ae2dcf40 | ||
|
f40e1498c1 | ||
|
b6f68936cf | ||
|
55799eafa0 | ||
|
8c36f318c7 | ||
|
49269d3aab | ||
|
95a20b54a3 | ||
|
2b9314a104 | ||
|
5a80c6482b | ||
|
92f887b4a0 | ||
|
5af89eb787 | ||
|
d5fc213523 | ||
|
e4a43af3ef | ||
|
a543af9562 | ||
|
9a605994e8 | ||
|
aac41547db | ||
|
6d9494d680 | ||
|
b800a9b3ee | ||
|
bd8d92b45c | ||
|
4fb67d7ba7 | ||
|
a4cd8257e2 | ||
|
20b8ab9baa | ||
|
e021162729 | ||
|
7463d4e440 | ||
|
9fa1a94301 | ||
|
30532acf46 | ||
|
4617876e44 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -30,7 +30,7 @@ jobs:
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '17'
|
||||
java-version: '21'
|
||||
- name: 💼 Load Gradle Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
|
@ -16,8 +16,8 @@ plugins {
|
||||
id 'java-library'
|
||||
id "jacoco"
|
||||
id "checkstyle"
|
||||
id "org.sonarqube" version "4.4.1.3373"
|
||||
id 'fabric-loom' version '1.3-SNAPSHOT' apply false
|
||||
id "org.sonarqube" version "5.1.0.4882"
|
||||
id 'fabric-loom' version '1.6-SNAPSHOT' apply false
|
||||
}
|
||||
|
||||
apply plugin: 'nebula-aggregate-javadocs'
|
||||
@ -69,9 +69,9 @@ subprojects {
|
||||
}
|
||||
|
||||
ext {
|
||||
daggerVersion = "2.51"
|
||||
daggerVersion = "2.51.1"
|
||||
|
||||
palVersion = "5.1.0"
|
||||
palVersion = "5.2.0"
|
||||
|
||||
bukkitVersion = "1.13.2-R0.1-SNAPSHOT"
|
||||
spigotVersion = "1.13.2-R0.1-SNAPSHOT"
|
||||
@ -83,20 +83,20 @@ subprojects {
|
||||
redisBungeeVersion = "0.3.8-SNAPSHOT"
|
||||
redisBungeeProxioDevVersion = "0.7.3"
|
||||
|
||||
commonsTextVersion = "1.11.0"
|
||||
commonsCompressVersion = "1.26.0"
|
||||
commonsCodecVersion = "1.16.1"
|
||||
commonsTextVersion = "1.12.0"
|
||||
commonsCompressVersion = "1.26.2"
|
||||
commonsCodecVersion = "1.17.1"
|
||||
caffeineVersion = "3.1.8"
|
||||
jettyVersion = "11.0.20"
|
||||
jettyVersion = "11.0.22"
|
||||
caffeineVersion = "2.9.2"
|
||||
mysqlVersion = "8.3.0"
|
||||
mariadbVersion = "3.3.3"
|
||||
mysqlVersion = "8.4.0"
|
||||
mariadbVersion = "3.4.1"
|
||||
sqliteVersion = "3.42.0.1"
|
||||
adventureVersion = "4.14.0"
|
||||
adventureVersion = "4.17.0"
|
||||
hikariVersion = "5.1.0"
|
||||
slf4jVersion = "2.0.11"
|
||||
slf4jVersion = "2.0.13"
|
||||
geoIpVersion = "4.2.0"
|
||||
gsonVersion = "2.10.1"
|
||||
gsonVersion = "2.11.0"
|
||||
dependencyDownloadVersion = "1.3.1"
|
||||
ipAddressMatcherVersion = "5.5.0"
|
||||
jasyptVersion = "1.9.3"
|
||||
@ -105,10 +105,11 @@ subprojects {
|
||||
placeholderapiVersion = "2.11.5"
|
||||
nkPlaceholderapiVersion = "1.4-SNAPSHOT"
|
||||
|
||||
junitVersion = "5.10.2"
|
||||
mockitoVersion = "5.11.0"
|
||||
testContainersVersion = "1.19.7"
|
||||
swaggerVersion = "2.2.20"
|
||||
junitVersion = "5.10.3"
|
||||
mockitoVersion = "5.12.0"
|
||||
seleniumVersion = "4.23.0"
|
||||
testContainersVersion = "1.20.1"
|
||||
swaggerVersion = "2.2.22"
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
@ -36,6 +36,7 @@ import org.bukkit.command.PluginCommand;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
@ -62,7 +63,18 @@ public class Plan extends JavaPlugin implements PlanPlugin {
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
abstractionLayer = new BukkitPlatformLayer(this);
|
||||
if (isFolia()) {
|
||||
try {
|
||||
// Attempt to load and use the Folia library for Java 17+
|
||||
Class<?> foliaPlatformLayer = Class.forName("net.playeranalytics.plugin.FoliaPlatformLayer");
|
||||
abstractionLayer = (PlatformAbstractionLayer) foliaPlatformLayer.getConstructor(JavaPlugin.class).newInstance(this);
|
||||
} catch (Exception e) {
|
||||
this.getLogger().log(Level.SEVERE, "Failed to load FoliaPlatformLayer", e);
|
||||
abstractionLayer = new BukkitPlatformLayer(this);
|
||||
}
|
||||
} else {
|
||||
abstractionLayer = new BukkitPlatformLayer(this);
|
||||
}
|
||||
pluginLogger = abstractionLayer.getPluginLogger();
|
||||
runnableFactory = abstractionLayer.getRunnableFactory();
|
||||
}
|
||||
@ -167,7 +179,18 @@ public class Plan extends JavaPlugin implements PlanPlugin {
|
||||
|
||||
public void cancelAllTasks() {
|
||||
runnableFactory.cancelAllKnownTasks();
|
||||
Bukkit.getScheduler().cancelTasks(this);
|
||||
if (!isFolia()) {
|
||||
Bukkit.getScheduler().cancelTasks(this);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isFolia() {
|
||||
try {
|
||||
Class.forName("io.papermc.paper.threadedregions.RegionizedServer");
|
||||
return true;
|
||||
} catch (ClassNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -55,6 +55,9 @@ public class BukkitCommand implements CommandExecutor, TabCompleter {
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
|
||||
if (command.getRequiredPermissions().stream().anyMatch(permission -> !sender.hasPermission(permission))) {
|
||||
return true;
|
||||
}
|
||||
runnableFactory.create(() -> {
|
||||
try {
|
||||
command.getExecutor().accept(getSender(sender), new Arguments(args));
|
||||
@ -70,6 +73,9 @@ public class BukkitCommand implements CommandExecutor, TabCompleter {
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(CommandSender sender, Command cmd, String label, String[] args) {
|
||||
if (command.getRequiredPermissions().stream().anyMatch(permission -> !sender.hasPermission(permission))) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
try {
|
||||
return command.getArgumentResolver().apply(getSender(sender), new Arguments(args));
|
||||
} catch (Exception e) {
|
||||
|
@ -16,6 +16,7 @@
|
||||
*/
|
||||
package com.djrapitops.plan.gathering.listeners.bukkit;
|
||||
|
||||
import com.djrapitops.plan.gathering.JoinAddressValidator;
|
||||
import com.djrapitops.plan.gathering.cache.JoinAddressCache;
|
||||
import com.djrapitops.plan.gathering.domain.BukkitPlayerData;
|
||||
import com.djrapitops.plan.gathering.domain.event.PlayerJoin;
|
||||
@ -29,6 +30,7 @@ import com.djrapitops.plan.storage.database.DBSystem;
|
||||
import com.djrapitops.plan.storage.database.transactions.events.BanStatusTransaction;
|
||||
import com.djrapitops.plan.storage.database.transactions.events.KickStoreTransaction;
|
||||
import com.djrapitops.plan.storage.database.transactions.events.StoreAllowlistBounceTransaction;
|
||||
import com.djrapitops.plan.utilities.dev.Untrusted;
|
||||
import com.djrapitops.plan.utilities.logging.ErrorContext;
|
||||
import com.djrapitops.plan.utilities.logging.ErrorLogger;
|
||||
import org.bukkit.event.EventHandler;
|
||||
@ -51,6 +53,7 @@ public class PlayerOnlineListener implements Listener {
|
||||
|
||||
private final PlayerJoinEventConsumer playerJoinEventConsumer;
|
||||
private final PlayerLeaveEventConsumer playerLeaveEventConsumer;
|
||||
private final JoinAddressValidator joinAddressValidator;
|
||||
private final JoinAddressCache joinAddressCache;
|
||||
|
||||
private final ServerInfo serverInfo;
|
||||
@ -62,6 +65,7 @@ public class PlayerOnlineListener implements Listener {
|
||||
public PlayerOnlineListener(
|
||||
PlayerJoinEventConsumer playerJoinEventConsumer,
|
||||
PlayerLeaveEventConsumer playerLeaveEventConsumer,
|
||||
JoinAddressValidator joinAddressValidator,
|
||||
JoinAddressCache joinAddressCache,
|
||||
ServerInfo serverInfo,
|
||||
DBSystem dbSystem,
|
||||
@ -70,6 +74,7 @@ public class PlayerOnlineListener implements Listener {
|
||||
) {
|
||||
this.playerJoinEventConsumer = playerJoinEventConsumer;
|
||||
this.playerLeaveEventConsumer = playerLeaveEventConsumer;
|
||||
this.joinAddressValidator = joinAddressValidator;
|
||||
this.joinAddressCache = joinAddressCache;
|
||||
this.serverInfo = serverInfo;
|
||||
this.dbSystem = dbSystem;
|
||||
@ -89,17 +94,8 @@ public class PlayerOnlineListener implements Listener {
|
||||
dbSystem.getDatabase().executeTransaction(new StoreAllowlistBounceTransaction(playerUUID, event.getPlayer().getName(), serverUUID, System.currentTimeMillis()));
|
||||
}
|
||||
|
||||
String address = event.getHostname();
|
||||
if (!address.isEmpty()) {
|
||||
if (address.contains(":")) {
|
||||
address = address.substring(0, address.lastIndexOf(':'));
|
||||
}
|
||||
if (address.contains("\u0000")) {
|
||||
address = address.substring(0, address.indexOf('\u0000'));
|
||||
}
|
||||
if (address.contains("fml")) {
|
||||
address = address.substring(0, address.lastIndexOf("fml"));
|
||||
}
|
||||
@Untrusted String address = joinAddressValidator.sanitize(event.getHostname());
|
||||
if (joinAddressValidator.isValid(address)) {
|
||||
joinAddressCache.put(playerUUID, address);
|
||||
}
|
||||
dbSystem.getDatabase().executeTransaction(new BanStatusTransaction(playerUUID, serverUUID, banned));
|
||||
|
@ -19,6 +19,7 @@ package com.djrapitops.plan.command.use;
|
||||
import com.djrapitops.plan.commands.use.Arguments;
|
||||
import com.djrapitops.plan.commands.use.CMDSender;
|
||||
import com.djrapitops.plan.commands.use.Subcommand;
|
||||
import com.djrapitops.plan.settings.Permissions;
|
||||
import com.djrapitops.plan.utilities.logging.ErrorContext;
|
||||
import com.djrapitops.plan.utilities.logging.ErrorLogger;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
@ -42,7 +43,7 @@ public class BungeeCommand extends Command implements TabExecutor {
|
||||
ErrorLogger errorLogger,
|
||||
Subcommand command, String name
|
||||
) {
|
||||
super(name);
|
||||
super(name, Permissions.USE_COMMAND.getPermission());
|
||||
this.runnableFactory = runnableFactory;
|
||||
this.errorLogger = errorLogger;
|
||||
|
||||
|
@ -4,7 +4,7 @@ import org.apache.tools.ant.filters.ReplaceTokens
|
||||
plugins {
|
||||
id "dev.vankka.dependencydownload.plugin" version "$dependencyDownloadVersion"
|
||||
id "com.github.node-gradle.node" version "7.0.2"
|
||||
id "io.swagger.core.v3.swagger-gradle-plugin" version "2.2.20"
|
||||
id "io.swagger.core.v3.swagger-gradle-plugin" version "2.2.22"
|
||||
}
|
||||
|
||||
configurations {
|
||||
@ -56,7 +56,7 @@ dependencies {
|
||||
shadow project(":extensions")
|
||||
|
||||
shadow "net.playeranalytics:platform-abstraction-layer-api:$palVersion"
|
||||
compileOnly "net.kyori:adventure-api:4.14.0"
|
||||
compileOnly "net.kyori:adventure-api:$adventureVersion"
|
||||
shadow("dev.vankka:dependencydownload-runtime:$dependencyDownloadVersion") {
|
||||
// Effectively disables relocating
|
||||
exclude module: "jar-relocator"
|
||||
@ -87,7 +87,7 @@ dependencies {
|
||||
|
||||
|
||||
// Swagger annotations
|
||||
implementation "jakarta.ws.rs:jakarta.ws.rs-api:3.1.0"
|
||||
implementation "jakarta.ws.rs:jakarta.ws.rs-api:4.0.0"
|
||||
implementation "io.swagger.core.v3:swagger-core-jakarta:$swaggerVersion"
|
||||
implementation "io.swagger.core.v3:swagger-jaxrs2-jakarta:$swaggerVersion"
|
||||
|
||||
@ -95,12 +95,21 @@ dependencies {
|
||||
testArtifacts project(":extensions:adventure")
|
||||
testImplementation project(":extensions:adventure")
|
||||
testImplementation "com.google.code.gson:gson:$gsonVersion"
|
||||
testImplementation "org.seleniumhq.selenium:selenium-java:4.18.1"
|
||||
testImplementation "org.seleniumhq.selenium:selenium-java:$seleniumVersion"
|
||||
testImplementation "org.testcontainers:testcontainers:$testContainersVersion"
|
||||
testImplementation "org.testcontainers:junit-jupiter:$testContainersVersion"
|
||||
testImplementation "org.testcontainers:nginx:$testContainersVersion"
|
||||
}
|
||||
|
||||
test {
|
||||
environment "PLAN_TEST_NODE_STRING", "String"
|
||||
environment "PLAN_TEST_NODE_BOOLEAN", "true"
|
||||
environment "PLAN_TEST_NODE_INTEGER", "5"
|
||||
environment "PLAN_TEST_NODE_DOUBLE", "0.5"
|
||||
environment "PLAN_TEST_NODE_LONG", "9223372036854775807"
|
||||
environment "PLAN_TEST_NODE_STRINGLIST", "- Test\n- Another"
|
||||
}
|
||||
|
||||
task updateVersion(type: Copy) {
|
||||
from('src/main/resources') {
|
||||
include 'plugin.yml'
|
||||
|
@ -102,6 +102,7 @@ public class PlanCommand {
|
||||
CommandWithSubcommands command = CommandWithSubcommands.builder(locale)
|
||||
.alias(commandName)
|
||||
.colorScheme(colors)
|
||||
.requirePermission(Permissions.USE_COMMAND.getPermission())
|
||||
.subcommand(serverCommand())
|
||||
.subcommand(serversCommand())
|
||||
.subcommand(networkCommand())
|
||||
@ -140,13 +141,21 @@ public class PlanCommand {
|
||||
}
|
||||
|
||||
public List<String> serverNames(CMDSender sender, @Untrusted Arguments arguments) {
|
||||
@Untrusted String asString = arguments.concatenate(" ");
|
||||
return tabCompleteCache.getMatchingServerIdentifiers(asString);
|
||||
if (sender.hasPermission(Permissions.SERVER)) {
|
||||
@Untrusted String asString = arguments.concatenate(" ");
|
||||
return tabCompleteCache.getMatchingServerIdentifiers(asString);
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private List<String> playerNames(CMDSender sender, @Untrusted Arguments arguments) {
|
||||
@Untrusted String asString = arguments.concatenate(" ");
|
||||
return tabCompleteCache.getMatchingPlayerIdentifiers(asString);
|
||||
if (sender.hasPermission(Permissions.PLAYER_OTHER)) {
|
||||
@Untrusted String asString = arguments.concatenate(" ");
|
||||
return tabCompleteCache.getMatchingPlayerIdentifiers(asString);
|
||||
} else if (sender.hasPermission(Permissions.PLAYER_SELF)) {
|
||||
return sender.getPlayerName().map(List::of).orElse(List.of());
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private Subcommand serverCommand() {
|
||||
@ -403,6 +412,9 @@ public class PlanCommand {
|
||||
}
|
||||
|
||||
private List<String> getBackupFilenames(CMDSender sender, @Untrusted Arguments arguments) {
|
||||
if (!sender.hasPermission(Permissions.DATA_RESTORE)) {
|
||||
return List.of();
|
||||
}
|
||||
if (arguments.get(1).isPresent()) {
|
||||
return DBType.names();
|
||||
}
|
||||
@ -531,6 +543,9 @@ public class PlanCommand {
|
||||
}
|
||||
|
||||
private List<String> webGroupTabComplete(CMDSender sender, @Untrusted Arguments arguments) {
|
||||
if (!sender.hasPermission(Permissions.SET_GROUP)) {
|
||||
return List.of();
|
||||
}
|
||||
Optional<String> groupArgument = arguments.get(1);
|
||||
if (groupArgument.isPresent()) {
|
||||
return tabCompleteCache.getMatchingWebGroupNames(groupArgument.get());
|
||||
|
@ -270,7 +270,7 @@ public class DataUtilityCommands {
|
||||
locale.getString(CommandLang.INGAME_REGISTERED, timestamp.apply(() -> registered)) + '\n' +
|
||||
locale.getString(CommandLang.INGAME_LAST_SEEN, timestamp.apply(() -> lastSeen)) + '\n' +
|
||||
locale.getString(CommandLang.INGAME_GEOLOCATION, geolocation) + '\n' +
|
||||
locale.getString(HtmlLang.TITLE_LATEST_JOIN_ADDRESSES, latestJoinAddress) + '\n' +
|
||||
" §2" + locale.getString(HtmlLang.LABEL_LABEL_JOIN_ADDRESS) + ": §f" + latestJoinAddress + '\n' +
|
||||
locale.getString(CommandLang.INGAME_TIMES_KICKED, player.getValue(PlayerKeys.KICK_COUNT).orElse(0)) + '\n' +
|
||||
'\n' +
|
||||
locale.getString(CommandLang.INGAME_PLAYTIME, length.apply(sessionsMutator.toPlaytime())) + '\n' +
|
||||
|
@ -16,6 +16,8 @@
|
||||
*/
|
||||
package com.djrapitops.plan.delivery.domain;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Object that has a value tied to a date.
|
||||
*
|
||||
@ -39,4 +41,25 @@ public class DateObj<T> implements DateHolder {
|
||||
public T getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
DateObj<?> dateObj = (DateObj<?>) o;
|
||||
return getDate() == dateObj.getDate() && Objects.equals(getValue(), dateObj.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(getDate(), getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DateObj{" +
|
||||
"date=" + date +
|
||||
", value=" + value +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -28,7 +28,7 @@ import java.util.Objects;
|
||||
*/
|
||||
public class JoinAddressCount implements Comparable<JoinAddressCount> {
|
||||
|
||||
private final int count;
|
||||
private int count;
|
||||
private String joinAddress;
|
||||
|
||||
public JoinAddressCount(Map.Entry<String, Integer> entry) {
|
||||
@ -52,6 +52,10 @@ public class JoinAddressCount implements Comparable<JoinAddressCount> {
|
||||
return count;
|
||||
}
|
||||
|
||||
public void setCount(int count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull JoinAddressCount other) {
|
||||
return String.CASE_INSENSITIVE_ORDER.compare(this.joinAddress, other.joinAddress);
|
||||
|
@ -19,6 +19,9 @@ package com.djrapitops.plan.delivery.domain.auth;
|
||||
import com.djrapitops.plan.settings.locale.lang.Lang;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
@ -47,7 +50,8 @@ public enum WebPermission implements Supplier<String>, Lang {
|
||||
PAGE_NETWORK_SESSIONS_LIST("See list of sessions"),
|
||||
PAGE_NETWORK_JOIN_ADDRESSES("See Join Addresses -tab"),
|
||||
PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS("See Join Address graphs"),
|
||||
PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_PIE("See Latest Join Addresses graph"),
|
||||
@Deprecated
|
||||
PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_PIE("See Latest Join Addresses graph", true),
|
||||
PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_TIME("See Join Addresses over time graph"),
|
||||
PAGE_NETWORK_RETENTION("See Player Retention -tab"),
|
||||
PAGE_NETWORK_GEOLOCATIONS("See Geolocations tab"),
|
||||
@ -82,7 +86,8 @@ public enum WebPermission implements Supplier<String>, Lang {
|
||||
PAGE_SERVER_SESSIONS_LIST("See list of sessions"),
|
||||
PAGE_SERVER_JOIN_ADDRESSES("See Join Addresses -tab"),
|
||||
PAGE_SERVER_JOIN_ADDRESSES_GRAPHS("See Join Address graphs"),
|
||||
PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_PIE("See Latest Join Addresses graph"),
|
||||
@Deprecated
|
||||
PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_PIE("See Latest Join Addresses graph", true),
|
||||
PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_TIME("See Join Addresses over time graph"),
|
||||
PAGE_SERVER_RETENTION("See Player Retention -tab"),
|
||||
PAGE_SERVER_GEOLOCATIONS("See Geolocations tab"),
|
||||
@ -156,4 +161,23 @@ public enum WebPermission implements Supplier<String>, Lang {
|
||||
public String getDefault() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public static WebPermission[] nonDeprecatedValues() {
|
||||
return Arrays.stream(values())
|
||||
.filter(Predicate.not(WebPermission::isDeprecated))
|
||||
.toArray(WebPermission[]::new);
|
||||
}
|
||||
|
||||
public static Optional<WebPermission> findByPermission(String permission) {
|
||||
String name = StringUtils.upperCase(permission).replace('.', '_');
|
||||
try {
|
||||
return Optional.of(valueOf(name));
|
||||
} catch (IllegalArgumentException noSuchEnum) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isDeprecated(String permission) {
|
||||
return findByPermission(permission).map(WebPermission::isDeprecated).orElse(false);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* This file is part of Player Analytics (Plan).
|
||||
*
|
||||
* Plan is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License v3 as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Plan 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.djrapitops.plan.delivery.domain.datatransfer;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Represents data returned by {@link com.djrapitops.plan.delivery.webserver.resolver.json.PlayerJoinAddressJSONResolver}.
|
||||
*
|
||||
* @author AuroraLS3
|
||||
*/
|
||||
public class PlayerJoinAddresses {
|
||||
|
||||
private final List<String> joinAddresses;
|
||||
private final Map<UUID, String> joinAddressByPlayer;
|
||||
|
||||
public PlayerJoinAddresses(List<String> joinAddresses, Map<UUID, String> joinAddressByPlayer) {
|
||||
this.joinAddresses = joinAddresses;
|
||||
this.joinAddressByPlayer = joinAddressByPlayer;
|
||||
}
|
||||
|
||||
public List<String> getJoinAddresses() {
|
||||
return joinAddresses;
|
||||
}
|
||||
|
||||
public Map<UUID, String> getJoinAddressByPlayer() {
|
||||
return joinAddressByPlayer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
PlayerJoinAddresses that = (PlayerJoinAddresses) o;
|
||||
return Objects.equals(getJoinAddresses(), that.getJoinAddresses()) && Objects.equals(getJoinAddressByPlayer(), that.getJoinAddressByPlayer());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(getJoinAddresses(), getJoinAddressByPlayer());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PlayerJoinAddresses{" +
|
||||
"joinAddresses=" + joinAddresses +
|
||||
", joinAddressByPlayer=" + joinAddressByPlayer +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -121,7 +121,6 @@ public class NetworkPageExporter extends FileExporter {
|
||||
"graph?type=uniqueAndNew",
|
||||
"graph?type=hourlyUniqueAndNew",
|
||||
"graph?type=serverPie",
|
||||
"graph?type=joinAddressPie",
|
||||
"graph?type=joinAddressByDay",
|
||||
"graph?type=activity",
|
||||
"graph?type=geolocation",
|
||||
|
@ -100,6 +100,7 @@ public class ServerPageExporter extends FileExporter {
|
||||
server + serverUUID + "/playerbase",
|
||||
server + serverUUID + "/join-addresses",
|
||||
server + serverUUID + "/retention",
|
||||
server + serverUUID + "/allowlist",
|
||||
server + serverUUID + "/players",
|
||||
server + serverUUID + "/geolocations",
|
||||
server + serverUUID + "/performance",
|
||||
@ -137,7 +138,6 @@ public class ServerPageExporter extends FileExporter {
|
||||
"graph?type=geolocation&server=" + serverUUID,
|
||||
"graph?type=uniqueAndNew&server=" + serverUUID,
|
||||
"graph?type=hourlyUniqueAndNew&server=" + serverUUID,
|
||||
"graph?type=joinAddressPie&server=" + serverUUID,
|
||||
"graph?type=joinAddressByDay&server=" + serverUUID,
|
||||
"graph?type=serverCalendar&server=" + serverUUID,
|
||||
"graph?type=punchCard&server=" + serverUUID,
|
||||
@ -148,7 +148,8 @@ public class ServerPageExporter extends FileExporter {
|
||||
"extensionData?server=" + serverUUID,
|
||||
"serverIdentity?server=" + serverUUID,
|
||||
"retention?server=" + serverUUID,
|
||||
"joinAddresses?server=" + serverUUID
|
||||
"joinAddresses?server=" + serverUUID,
|
||||
"gameAllowlistBounces?server=" + serverUUID
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -111,7 +111,10 @@ public class Contributors {
|
||||
new Contributor("Jumala9163", LANG),
|
||||
new Contributor("Dreeam-qwq", CODE),
|
||||
new Contributor("jhqwqmc", LANG),
|
||||
new Contributor("liuzhen932", LANG)
|
||||
new Contributor("liuzhen932", LANG),
|
||||
new Contributor("Sniper_TVmc", LANG),
|
||||
new Contributor("mcmdev", CODE),
|
||||
new Contributor("ZhangYuheng", CODE)
|
||||
};
|
||||
|
||||
private Contributors() {
|
||||
|
@ -18,6 +18,7 @@ package com.djrapitops.plan.delivery.rendering.json;
|
||||
|
||||
import com.djrapitops.plan.delivery.domain.DateObj;
|
||||
import com.djrapitops.plan.delivery.domain.RetentionData;
|
||||
import com.djrapitops.plan.delivery.domain.datatransfer.PlayerJoinAddresses;
|
||||
import com.djrapitops.plan.delivery.domain.datatransfer.ServerDto;
|
||||
import com.djrapitops.plan.delivery.domain.mutators.PlayerKillMutator;
|
||||
import com.djrapitops.plan.delivery.domain.mutators.SessionsMutator;
|
||||
@ -34,6 +35,7 @@ import com.djrapitops.plan.identification.Server;
|
||||
import com.djrapitops.plan.identification.ServerInfo;
|
||||
import com.djrapitops.plan.identification.ServerUUID;
|
||||
import com.djrapitops.plan.settings.config.PlanConfig;
|
||||
import com.djrapitops.plan.settings.config.paths.DataGatheringSettings;
|
||||
import com.djrapitops.plan.settings.config.paths.DisplaySettings;
|
||||
import com.djrapitops.plan.settings.config.paths.TimeSettings;
|
||||
import com.djrapitops.plan.settings.locale.Locale;
|
||||
@ -48,6 +50,7 @@ import com.djrapitops.plan.storage.database.queries.analysis.PlayerRetentionQuer
|
||||
import com.djrapitops.plan.storage.database.queries.objects.*;
|
||||
import com.djrapitops.plan.storage.database.queries.objects.playertable.NetworkTablePlayersQuery;
|
||||
import com.djrapitops.plan.storage.database.queries.objects.playertable.ServerTablePlayersQuery;
|
||||
import com.djrapitops.plan.storage.database.sql.tables.JoinAddressTable;
|
||||
import com.djrapitops.plan.utilities.comparators.SessionStartComparator;
|
||||
import com.djrapitops.plan.utilities.dev.Untrusted;
|
||||
import com.djrapitops.plan.utilities.java.Maps;
|
||||
@ -160,14 +163,52 @@ public class JSONFactory {
|
||||
return db.query(PlayerRetentionQueries.fetchRetentionData());
|
||||
}
|
||||
|
||||
public Map<UUID, String> playerJoinAddresses(ServerUUID serverUUID) {
|
||||
Database db = dbSystem.getDatabase();
|
||||
return db.query(JoinAddressQueries.latestJoinAddressesOfPlayers(serverUUID));
|
||||
private static void removeFiltered(Map<UUID, String> addressByPlayerUUID, List<String> filteredJoinAddresses) {
|
||||
if (filteredJoinAddresses.isEmpty() || filteredJoinAddresses.equals(List.of("play.example.com"))) return;
|
||||
|
||||
Set<UUID> toRemove = new HashSet<>();
|
||||
// Remove filtered addresses from the data
|
||||
for (Map.Entry<UUID, String> entry : addressByPlayerUUID.entrySet()) {
|
||||
if (filteredJoinAddresses.contains(entry.getValue())) {
|
||||
toRemove.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
for (UUID playerUUID : toRemove) {
|
||||
addressByPlayerUUID.put(playerUUID, JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP);
|
||||
}
|
||||
}
|
||||
|
||||
public Map<UUID, String> playerJoinAddresses() {
|
||||
public PlayerJoinAddresses playerJoinAddresses(ServerUUID serverUUID, boolean includeByPlayerMap) {
|
||||
Database db = dbSystem.getDatabase();
|
||||
return db.query(JoinAddressQueries.latestJoinAddressesOfPlayers());
|
||||
List<String> filteredJoinAddresses = config.get(DataGatheringSettings.FILTER_JOIN_ADDRESSES);
|
||||
if (includeByPlayerMap) {
|
||||
Map<UUID, String> addresses = db.query(JoinAddressQueries.latestJoinAddressesOfPlayers(serverUUID));
|
||||
|
||||
removeFiltered(addresses, filteredJoinAddresses);
|
||||
|
||||
return new PlayerJoinAddresses(
|
||||
addresses.values().stream().distinct().sorted().collect(Collectors.toList()),
|
||||
addresses
|
||||
);
|
||||
} else {
|
||||
List<String> addresses = db.query(JoinAddressQueries.uniqueJoinAddresses(serverUUID));
|
||||
addresses.removeAll(filteredJoinAddresses);
|
||||
return new PlayerJoinAddresses(addresses, null);
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerJoinAddresses playerJoinAddresses(boolean includeByPlayerMap) {
|
||||
Database db = dbSystem.getDatabase();
|
||||
List<String> filteredJoinAddresses = config.get(DataGatheringSettings.FILTER_JOIN_ADDRESSES);
|
||||
List<String> unique = db.query(JoinAddressQueries.uniqueJoinAddresses());
|
||||
unique.removeAll(filteredJoinAddresses);
|
||||
if (includeByPlayerMap) {
|
||||
Map<UUID, String> latest = db.query(JoinAddressQueries.latestJoinAddressesOfPlayers());
|
||||
removeFiltered(latest, filteredJoinAddresses);
|
||||
return new PlayerJoinAddresses(unique, latest);
|
||||
} else {
|
||||
return new PlayerJoinAddresses(unique, null);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> serverSessionsAsJSONMap(ServerUUID serverUUID) {
|
||||
|
@ -32,7 +32,6 @@ import com.djrapitops.plan.delivery.rendering.json.graphs.line.LineGraphFactory;
|
||||
import com.djrapitops.plan.delivery.rendering.json.graphs.line.PingGraph;
|
||||
import com.djrapitops.plan.delivery.rendering.json.graphs.line.Point;
|
||||
import com.djrapitops.plan.delivery.rendering.json.graphs.pie.Pie;
|
||||
import com.djrapitops.plan.delivery.rendering.json.graphs.pie.PieSlice;
|
||||
import com.djrapitops.plan.delivery.rendering.json.graphs.pie.WorldPie;
|
||||
import com.djrapitops.plan.delivery.rendering.json.graphs.special.WorldMap;
|
||||
import com.djrapitops.plan.delivery.rendering.json.graphs.stack.StackGraph;
|
||||
@ -57,7 +56,7 @@ import com.djrapitops.plan.storage.database.queries.analysis.PlayerCountQueries;
|
||||
import com.djrapitops.plan.storage.database.queries.objects.*;
|
||||
import com.djrapitops.plan.storage.database.sql.tables.JoinAddressTable;
|
||||
import com.djrapitops.plan.utilities.comparators.DateHolderOldestComparator;
|
||||
import com.djrapitops.plan.utilities.comparators.PieSliceComparator;
|
||||
import com.djrapitops.plan.utilities.dev.Untrusted;
|
||||
import com.djrapitops.plan.utilities.java.Lists;
|
||||
import com.djrapitops.plan.utilities.java.Maps;
|
||||
import net.playeranalytics.plugin.scheduling.TimeAmount;
|
||||
@ -457,34 +456,6 @@ public class GraphJSONCreator {
|
||||
.build();
|
||||
}
|
||||
|
||||
public Map<String, Object> playerHostnamePieJSONAsMap() {
|
||||
String[] pieColors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE);
|
||||
Map<String, Integer> joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.latestJoinAddresses());
|
||||
|
||||
translateUnknown(joinAddresses);
|
||||
|
||||
List<PieSlice> slices = graphs.pie().joinAddressPie(joinAddresses).getSlices();
|
||||
slices.sort(new PieSliceComparator());
|
||||
return Maps.builder(String.class, Object.class)
|
||||
.put("colors", pieColors)
|
||||
.put("slices", slices)
|
||||
.build();
|
||||
}
|
||||
|
||||
public Map<String, Object> playerHostnamePieJSONAsMap(ServerUUID serverUUID) {
|
||||
String[] pieColors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE);
|
||||
Map<String, Integer> joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.latestJoinAddresses(serverUUID));
|
||||
|
||||
translateUnknown(joinAddresses);
|
||||
|
||||
List<PieSlice> slices = graphs.pie().joinAddressPie(joinAddresses).getSlices();
|
||||
slices.sort(new PieSliceComparator());
|
||||
return Maps.builder(String.class, Object.class)
|
||||
.put("colors", pieColors)
|
||||
.put("slices", slices)
|
||||
.build();
|
||||
}
|
||||
|
||||
public void translateUnknown(Map<String, Integer> joinAddresses) {
|
||||
Integer unknown = joinAddresses.get(JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP);
|
||||
if (unknown != null) {
|
||||
@ -493,33 +464,65 @@ public class GraphJSONCreator {
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Object> joinAddressesByDay(ServerUUID serverUUID, long after, long before) {
|
||||
public Map<String, Object> joinAddressesByDay(ServerUUID serverUUID, long after, long before, @Untrusted List<String> addressFilter) {
|
||||
String[] pieColors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE);
|
||||
List<DateObj<Map<String, Integer>>> joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.joinAddressesPerDay(serverUUID, config.getTimeZone().getOffset(System.currentTimeMillis()), after, before));
|
||||
List<DateObj<Map<String, Integer>>> joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.joinAddressesPerDay(serverUUID, config.getTimeZone().getOffset(System.currentTimeMillis()), after, before, addressFilter));
|
||||
|
||||
return mapToJson(pieColors, joinAddresses);
|
||||
}
|
||||
|
||||
public Map<String, Object> joinAddressesByDay(long after, long before) {
|
||||
public Map<String, Object> joinAddressesByDay(long after, long before, @Untrusted List<String> addressFilter) {
|
||||
String[] pieColors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE);
|
||||
List<DateObj<Map<String, Integer>>> joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.joinAddressesPerDay(config.getTimeZone().getOffset(System.currentTimeMillis()), after, before));
|
||||
List<DateObj<Map<String, Integer>>> joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.joinAddressesPerDay(config.getTimeZone().getOffset(System.currentTimeMillis()), after, before, addressFilter));
|
||||
|
||||
return mapToJson(pieColors, joinAddresses);
|
||||
}
|
||||
|
||||
private static void removeFilteredAddresses(List<JoinAddressCount> addresses, List<String> filteredJoinAddresses) {
|
||||
if (filteredJoinAddresses.isEmpty() || filteredJoinAddresses.equals(List.of("play.example.com"))) return;
|
||||
|
||||
List<JoinAddressCount> addressesToRemove = addresses.stream()
|
||||
.filter(address -> filteredJoinAddresses.contains(address.getJoinAddress()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!addressesToRemove.isEmpty()) {
|
||||
Optional<JoinAddressCount> foundUnknownAddressCount = addresses.stream()
|
||||
.filter(address -> address.getJoinAddress().equals(JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP))
|
||||
.findFirst();
|
||||
JoinAddressCount unknownAddressCount;
|
||||
if (foundUnknownAddressCount.isEmpty()) {
|
||||
unknownAddressCount = new JoinAddressCount(JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP, 0);
|
||||
addresses.add(unknownAddressCount);
|
||||
} else {
|
||||
unknownAddressCount = foundUnknownAddressCount.get();
|
||||
}
|
||||
|
||||
for (JoinAddressCount toRemove : addressesToRemove) {
|
||||
unknownAddressCount.setCount(unknownAddressCount.getCount() + toRemove.getCount());
|
||||
addresses.remove(toRemove);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> mapToJson(String[] pieColors, List<DateObj<Map<String, Integer>>> joinAddresses) {
|
||||
for (DateObj<Map<String, Integer>> addressesByDate : joinAddresses) {
|
||||
translateUnknown(addressesByDate.getValue());
|
||||
}
|
||||
|
||||
List<String> filteredJoinAddresses = config.get(DataGatheringSettings.FILTER_JOIN_ADDRESSES);
|
||||
|
||||
List<JoinAddressCounts> joinAddressCounts = joinAddresses.stream()
|
||||
.map(addressesOnDay -> new JoinAddressCounts(
|
||||
addressesOnDay.getDate(),
|
||||
addressesOnDay.getValue().entrySet()
|
||||
.stream()
|
||||
.map(JoinAddressCount::new)
|
||||
.sorted()
|
||||
.collect(Collectors.toList())))
|
||||
.map(addressesOnDay -> {
|
||||
List<JoinAddressCount> addresses = addressesOnDay.getValue().entrySet()
|
||||
.stream()
|
||||
.map(JoinAddressCount::new)
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
removeFilteredAddresses(addresses, filteredJoinAddresses);
|
||||
|
||||
return new JoinAddressCounts(addressesOnDay.getDate(), addresses);
|
||||
})
|
||||
.sorted(new DateHolderOldestComparator())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
|
@ -1,38 +0,0 @@
|
||||
/*
|
||||
* This file is part of Player Analytics (Plan).
|
||||
*
|
||||
* Plan is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License v3 as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Plan 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.djrapitops.plan.delivery.rendering.json.graphs.pie;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class JoinAddressPie extends Pie {
|
||||
|
||||
JoinAddressPie(Map<String, Integer> joinAddresses) {
|
||||
super(turnToSlices(joinAddresses));
|
||||
}
|
||||
|
||||
private static List<PieSlice> turnToSlices(Map<String, Integer> joinAddresses) {
|
||||
List<PieSlice> slices = new ArrayList<>();
|
||||
for (Map.Entry<String, Integer> address : joinAddresses.entrySet()) {
|
||||
String joinAddress = address.getKey();
|
||||
Integer total = address.getValue();
|
||||
slices.add(new PieSlice(joinAddress, total));
|
||||
}
|
||||
return slices;
|
||||
}
|
||||
}
|
@ -68,10 +68,6 @@ public class PieGraphFactory {
|
||||
return new ServerPreferencePie(playtimeByServerName);
|
||||
}
|
||||
|
||||
public Pie joinAddressPie(Map<String, Integer> joinAddresses) {
|
||||
return new JoinAddressPie(joinAddresses);
|
||||
}
|
||||
|
||||
public WorldPie worldPie(WorldTimes worldTimes) {
|
||||
WorldAliasSettings worldAliasSettings = config.getWorldAliasSettings();
|
||||
Map<String, Long> playtimePerAlias = worldAliasSettings.getPlaytimePerAlias(worldTimes);
|
||||
|
@ -40,14 +40,7 @@ public class RateLimitGuard {
|
||||
.expireAfterWrite(120, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
public boolean shouldPreventRequest(@Untrusted String accessor) {
|
||||
Integer attempts = requests.getIfPresent(accessor);
|
||||
if (attempts == null) return false;
|
||||
// Too many attempts, forbid further attempts.
|
||||
return attempts >= ATTEMPT_LIMIT;
|
||||
}
|
||||
|
||||
public void increaseAttemptCount(@Untrusted String requestPath, @Untrusted String accessor) {
|
||||
public boolean shouldPreventRequest(@Untrusted String requestPath, @Untrusted String accessor) {
|
||||
String previous = lastRequestPath.getIfPresent(accessor);
|
||||
if (!Objects.equals(previous, requestPath)) {
|
||||
resetAttemptCount(accessor);
|
||||
@ -60,23 +53,23 @@ public class RateLimitGuard {
|
||||
|
||||
lastRequestPath.put(accessor, requestPath);
|
||||
requests.put(accessor, attempts + 1);
|
||||
|
||||
// Too many attempts, forbid further attempts.
|
||||
return attempts + 1 >= ATTEMPT_LIMIT;
|
||||
}
|
||||
|
||||
public void resetAttemptCount(@Untrusted String accessor) {
|
||||
// previous request changed
|
||||
requests.cleanUp();
|
||||
requests.invalidate(accessor);
|
||||
requests.cleanUp();
|
||||
}
|
||||
|
||||
public static class Disabled extends RateLimitGuard {
|
||||
@Override
|
||||
public boolean shouldPreventRequest(String accessor) {
|
||||
public boolean shouldPreventRequest(@Untrusted String requestedPath, String accessor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void increaseAttemptCount(String requestPath, String accessor) { /* Disabled */ }
|
||||
|
||||
@Override
|
||||
public void resetAttemptCount(String accessor) { /* Disabled */ }
|
||||
}
|
||||
|
@ -42,7 +42,6 @@ public enum DataID {
|
||||
GRAPH_ACTIVITY,
|
||||
GRAPH_PING,
|
||||
GRAPH_SERVER_PIE,
|
||||
GRAPH_HOSTNAME_PIE,
|
||||
GRAPH_PUNCHCARD,
|
||||
SERVER_OVERVIEW,
|
||||
ONLINE_OVERVIEW,
|
||||
@ -54,12 +53,26 @@ public enum DataID {
|
||||
EXTENSION_TABS,
|
||||
EXTENSION_JSON,
|
||||
LIST_SERVERS,
|
||||
JOIN_ADDRESSES_BY_DAY,
|
||||
JOIN_ADDRESSES_BY_DAY(false),
|
||||
PLAYER_RETENTION,
|
||||
PLAYER_JOIN_ADDRESSES,
|
||||
PLAYER_ALLOWLIST_BOUNCES,
|
||||
;
|
||||
|
||||
private final boolean cacheable;
|
||||
|
||||
DataID() {
|
||||
this(true);
|
||||
}
|
||||
|
||||
DataID(boolean cacheable) {
|
||||
this.cacheable = cacheable;
|
||||
}
|
||||
|
||||
public boolean isCacheable() {
|
||||
return cacheable;
|
||||
}
|
||||
|
||||
public String of(ServerUUID serverUUID) {
|
||||
if (serverUUID == null) return name();
|
||||
return name() + "_" + serverUUID;
|
||||
|
@ -74,6 +74,10 @@ public interface JSONStorage extends SubSystem {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public static StoredJSON fromObject(Object json, long timestamp) {
|
||||
return new StoredJSON(new Gson().toJson(json), timestamp);
|
||||
}
|
||||
|
||||
public String getJson() {
|
||||
return json;
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ public class WebserverLogMessages {
|
||||
}
|
||||
|
||||
public void warnWebserverDisabledByConfig() {
|
||||
logger.warn(locale.getString(PluginLang.ENABLE_NOTIFY_WEB_SERVER_DISABLED));
|
||||
logger.info(locale.getString(PluginLang.ENABLE_NOTIFY_WEB_SERVER_DISABLED));
|
||||
}
|
||||
|
||||
public void keystoreNotFoundError(InvalidPathException error, String keyStorePath) {
|
||||
|
@ -58,7 +58,6 @@ public class RequestHandler {
|
||||
public Response getResponse(InternalRequest internalRequest) {
|
||||
@Untrusted String accessAddress = internalRequest.getAccessAddress(webserverConfiguration);
|
||||
@Untrusted String requestedPath = internalRequest.getRequestedPath();
|
||||
rateLimitGuard.increaseAttemptCount(requestedPath, accessAddress);
|
||||
|
||||
boolean blocked = false;
|
||||
Response response;
|
||||
@ -66,7 +65,7 @@ public class RequestHandler {
|
||||
if (bruteForceGuard.shouldPreventRequest(accessAddress)) {
|
||||
response = responseFactory.failedLoginAttempts403();
|
||||
blocked = true;
|
||||
} else if (rateLimitGuard.shouldPreventRequest(accessAddress)) {
|
||||
} else if (rateLimitGuard.shouldPreventRequest(requestedPath, accessAddress)) {
|
||||
response = responseFactory.failedRateLimit403();
|
||||
blocked = true;
|
||||
} else if (!webserverConfiguration.getAllowedIpList().isAllowed(accessAddress)) {
|
||||
|
@ -39,11 +39,16 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Resolves /v1/graph JSON requests.
|
||||
@ -117,7 +122,6 @@ public class GraphsJSONResolver extends JSONResolver {
|
||||
@ExampleObject("aggregatedPing"),
|
||||
@ExampleObject("punchCard"),
|
||||
@ExampleObject("serverPie"),
|
||||
@ExampleObject("joinAddressPie"),
|
||||
@ExampleObject("joinAddressByDay"),
|
||||
}),
|
||||
@Parameter(in = ParameterIn.QUERY, name = "server", description = "Server identifier to get data for", examples = {
|
||||
@ -156,15 +160,22 @@ public class GraphsJSONResolver extends JSONResolver {
|
||||
JSONStorage.StoredJSON storedJSON;
|
||||
if (request.getQuery().get("server").isPresent()) {
|
||||
ServerUUID serverUUID = identifiers.getServerUUID(request); // Can throw BadRequestException
|
||||
storedJSON = jsonResolverService.resolve(
|
||||
timestamp, dataID, serverUUID,
|
||||
theServerUUID -> generateGraphDataJSONOfType(dataID, theServerUUID, request.getQuery())
|
||||
);
|
||||
Function<ServerUUID, Object> generationFunction = theServerUUID -> generateGraphDataJSONOfType(dataID, theServerUUID, request.getQuery());
|
||||
if (dataID.isCacheable()) {
|
||||
storedJSON = jsonResolverService.resolve(timestamp, dataID, serverUUID, generationFunction);
|
||||
} else {
|
||||
storedJSON = JSONStorage.StoredJSON.fromObject(generationFunction.apply(serverUUID), System.currentTimeMillis());
|
||||
}
|
||||
} else {
|
||||
// Assume network
|
||||
storedJSON = jsonResolverService.resolve(
|
||||
timestamp, dataID, () -> generateGraphDataJSONOfType(dataID, request.getQuery())
|
||||
);
|
||||
Supplier<Object> generationFunction = () -> generateGraphDataJSONOfType(dataID, request.getQuery());
|
||||
if (dataID.isCacheable()) {
|
||||
storedJSON = jsonResolverService.resolve(
|
||||
timestamp, dataID, generationFunction
|
||||
);
|
||||
} else {
|
||||
storedJSON = JSONStorage.StoredJSON.fromObject(generationFunction.get(), System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
return storedJSON;
|
||||
}
|
||||
@ -197,8 +208,6 @@ public class GraphsJSONResolver extends JSONResolver {
|
||||
return DataID.GRAPH_PUNCHCARD;
|
||||
case "serverPie":
|
||||
return DataID.GRAPH_SERVER_PIE;
|
||||
case "joinAddressPie":
|
||||
return DataID.GRAPH_HOSTNAME_PIE;
|
||||
case "joinAddressByDay":
|
||||
return DataID.JOIN_ADDRESSES_BY_DAY;
|
||||
default:
|
||||
@ -229,8 +238,6 @@ public class GraphsJSONResolver extends JSONResolver {
|
||||
return List.of(WebPermission.PAGE_SERVER_PLAYERBASE_GRAPHS);
|
||||
case GRAPH_WORLD_MAP:
|
||||
return List.of(WebPermission.PAGE_SERVER_GEOLOCATIONS_MAP);
|
||||
case GRAPH_HOSTNAME_PIE:
|
||||
return List.of(WebPermission.PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_PIE);
|
||||
case JOIN_ADDRESSES_BY_DAY:
|
||||
return List.of(WebPermission.PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_TIME);
|
||||
default:
|
||||
@ -258,8 +265,6 @@ public class GraphsJSONResolver extends JSONResolver {
|
||||
return List.of(WebPermission.PAGE_NETWORK_GEOLOCATIONS_MAP);
|
||||
case GRAPH_ONLINE_PROXIES:
|
||||
return List.of(WebPermission.PAGE_NETWORK_OVERVIEW_GRAPHS_ONLINE);
|
||||
case GRAPH_HOSTNAME_PIE:
|
||||
return List.of(WebPermission.PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_PIE);
|
||||
case JOIN_ADDRESSES_BY_DAY:
|
||||
return List.of(WebPermission.PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_TIME);
|
||||
default:
|
||||
@ -283,8 +288,6 @@ public class GraphsJSONResolver extends JSONResolver {
|
||||
return graphJSON.serverCalendarJSON(serverUUID);
|
||||
case GRAPH_WORLD_PIE:
|
||||
return graphJSON.serverWorldPieJSONAsMap(serverUUID);
|
||||
case GRAPH_HOSTNAME_PIE:
|
||||
return graphJSON.playerHostnamePieJSONAsMap(serverUUID);
|
||||
case GRAPH_ACTIVITY:
|
||||
return graphJSON.activityGraphsJSONAsMap(serverUUID);
|
||||
case GRAPH_WORLD_MAP:
|
||||
@ -294,19 +297,24 @@ public class GraphsJSONResolver extends JSONResolver {
|
||||
case GRAPH_PUNCHCARD:
|
||||
return graphJSON.punchCardJSONAsMap(serverUUID);
|
||||
case JOIN_ADDRESSES_BY_DAY:
|
||||
try {
|
||||
return graphJSON.joinAddressesByDay(serverUUID,
|
||||
query.get("after").map(Long::parseLong).orElse(0L),
|
||||
query.get("before").map(Long::parseLong).orElse(System.currentTimeMillis())
|
||||
);
|
||||
} catch (@Untrusted NumberFormatException e) {
|
||||
throw new BadRequestException("'after' or 'before' is not a epoch millisecond (number)");
|
||||
}
|
||||
return joinAddressGraph(serverUUID, query);
|
||||
default:
|
||||
throw new BadRequestException("Graph type not supported with server-parameter (" + id.name() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> joinAddressGraph(ServerUUID serverUUID, @Untrusted URIQuery query) {
|
||||
try {
|
||||
Long after = query.get("after").map(Long::parseLong).orElse(0L);
|
||||
Long before = query.get("before").map(Long::parseLong).orElse(System.currentTimeMillis());
|
||||
@Untrusted List<String> addressFilter = query.get("addresses").map(s -> StringUtils.split(s, ','))
|
||||
.map(Arrays::asList).orElse(List.of());
|
||||
return graphJSON.joinAddressesByDay(serverUUID, after, before, addressFilter);
|
||||
} catch (@Untrusted NumberFormatException e) {
|
||||
throw new BadRequestException("'after' or 'before' is not a epoch millisecond (number)");
|
||||
}
|
||||
}
|
||||
|
||||
private Object generateGraphDataJSONOfType(DataID id, @Untrusted URIQuery query) {
|
||||
switch (id) {
|
||||
case GRAPH_ACTIVITY:
|
||||
@ -319,23 +327,26 @@ public class GraphsJSONResolver extends JSONResolver {
|
||||
return graphJSON.networkCalendarJSON();
|
||||
case GRAPH_SERVER_PIE:
|
||||
return graphJSON.serverPreferencePieJSONAsMap();
|
||||
case GRAPH_HOSTNAME_PIE:
|
||||
return graphJSON.playerHostnamePieJSONAsMap();
|
||||
case GRAPH_WORLD_MAP:
|
||||
return graphJSON.geolocationGraphsJSONAsMap();
|
||||
case GRAPH_ONLINE_PROXIES:
|
||||
return graphJSON.proxyPlayersOnlineGraphs();
|
||||
case JOIN_ADDRESSES_BY_DAY:
|
||||
try {
|
||||
return graphJSON.joinAddressesByDay(
|
||||
query.get("after").map(Long::parseLong).orElse(0L),
|
||||
query.get("before").map(Long::parseLong).orElse(System.currentTimeMillis())
|
||||
);
|
||||
} catch (@Untrusted NumberFormatException e) {
|
||||
throw new BadRequestException("'after' or 'before' is not a epoch millisecond (number)");
|
||||
}
|
||||
return joinAddressGraph(query);
|
||||
default:
|
||||
throw new BadRequestException("Graph type not supported without server-parameter (" + id.name() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> joinAddressGraph(URIQuery query) {
|
||||
try {
|
||||
Long after = query.get("after").map(Long::parseLong).orElse(0L);
|
||||
Long before = query.get("before").map(Long::parseLong).orElse(System.currentTimeMillis());
|
||||
@Untrusted List<String> addressFilter = query.get("addresses").map(s -> StringUtils.split(s, ','))
|
||||
.map(Arrays::asList).orElse(List.of());
|
||||
return graphJSON.joinAddressesByDay(after, before, addressFilter);
|
||||
} catch (@Untrusted NumberFormatException e) {
|
||||
throw new BadRequestException("'after' or 'before' is not a epoch millisecond (number)");
|
||||
}
|
||||
}
|
||||
}
|
@ -17,11 +17,13 @@
|
||||
package com.djrapitops.plan.delivery.webserver.resolver.json;
|
||||
|
||||
import com.djrapitops.plan.delivery.domain.auth.WebPermission;
|
||||
import com.djrapitops.plan.delivery.domain.datatransfer.PlayerJoinAddresses;
|
||||
import com.djrapitops.plan.delivery.formatting.Formatter;
|
||||
import com.djrapitops.plan.delivery.rendering.json.JSONFactory;
|
||||
import com.djrapitops.plan.delivery.web.resolver.MimeType;
|
||||
import com.djrapitops.plan.delivery.web.resolver.Response;
|
||||
import com.djrapitops.plan.delivery.web.resolver.request.Request;
|
||||
import com.djrapitops.plan.delivery.web.resolver.request.URIQuery;
|
||||
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
|
||||
import com.djrapitops.plan.delivery.webserver.cache.AsyncJSONResolverService;
|
||||
import com.djrapitops.plan.delivery.webserver.cache.DataID;
|
||||
@ -34,6 +36,7 @@ import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.ExampleObject;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import jakarta.ws.rs.GET;
|
||||
@ -42,7 +45,6 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
@ -69,17 +71,27 @@ public class PlayerJoinAddressJSONResolver extends JSONResolver {
|
||||
@Override
|
||||
public boolean canAccess(@Untrusted Request request) {
|
||||
WebUser user = request.getUser().orElse(new WebUser(""));
|
||||
if (request.getQuery().get("server").isPresent()) {
|
||||
return user.hasPermission(WebPermission.PAGE_SERVER_RETENTION);
|
||||
@Untrusted URIQuery query = request.getQuery();
|
||||
Optional<String> listOnly = query.get("listOnly");
|
||||
if (query.get("server").isPresent()) {
|
||||
if (listOnly.isEmpty()) {
|
||||
return user.hasPermission(WebPermission.PAGE_SERVER_RETENTION);
|
||||
} else {
|
||||
return user.hasPermission(WebPermission.PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_TIME);
|
||||
}
|
||||
}
|
||||
if (listOnly.isEmpty()) {
|
||||
return user.hasPermission(WebPermission.PAGE_NETWORK_RETENTION);
|
||||
} else {
|
||||
return user.hasPermission(WebPermission.PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_TIME);
|
||||
}
|
||||
return user.hasPermission(WebPermission.PAGE_NETWORK_RETENTION);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Operation(
|
||||
description = "Get join address information of players for server or network",
|
||||
responses = {
|
||||
@ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON)),
|
||||
@ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON, schema = @Schema(implementation = PlayerJoinAddresses.class))),
|
||||
@ApiResponse(responseCode = "400", description = "If 'server' parameter is not an existing server")
|
||||
},
|
||||
parameters = @Parameter(in = ParameterIn.QUERY, name = "server", description = "Server identifier to get data for (optional)", examples = {
|
||||
@ -105,12 +117,12 @@ public class PlayerJoinAddressJSONResolver extends JSONResolver {
|
||||
if (request.getQuery().get("server").isPresent()) {
|
||||
ServerUUID serverUUID = identifiers.getServerUUID(request);
|
||||
return jsonResolverService.resolve(timestamp, DataID.PLAYER_JOIN_ADDRESSES, serverUUID,
|
||||
theUUID -> Collections.singletonMap("join_address_by_player", jsonFactory.playerJoinAddresses(theUUID))
|
||||
serverUUID1 -> jsonFactory.playerJoinAddresses(serverUUID1, request.getQuery().get("listOnly").isEmpty())
|
||||
);
|
||||
}
|
||||
// Assume network
|
||||
return jsonResolverService.resolve(timestamp, DataID.PLAYER_JOIN_ADDRESSES,
|
||||
() -> Collections.singletonMap("join_address_by_player", jsonFactory.playerJoinAddresses())
|
||||
() -> jsonFactory.playerJoinAddresses(request.getQuery().get("listOnly").isEmpty())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,8 @@ import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Endpoint for getting list of available Plan web permissions.
|
||||
@ -75,7 +77,10 @@ public class WebPermissionJSONResolver implements Resolver {
|
||||
}
|
||||
|
||||
private Response getResponse() {
|
||||
List<String> permissions = dbSystem.getDatabase().query(WebUserQueries.fetchAvailablePermissions());
|
||||
List<String> permissions = dbSystem.getDatabase().query(WebUserQueries.fetchAvailablePermissions())
|
||||
.stream()
|
||||
.filter(Predicate.not(WebPermission::isDeprecated))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
WebPermissionList permissionList = new WebPermissionList(permissions);
|
||||
return Response.builder()
|
||||
|
@ -30,6 +30,7 @@ import java.util.Optional;
|
||||
public class DBOpException extends IllegalStateException implements ExceptionWithContext {
|
||||
|
||||
public static final String CONSTRAINT_VIOLATION = "Constraint Violation";
|
||||
public static final String DUPLICATE_KEY = "Duplicate key";
|
||||
private final ErrorContext context;
|
||||
|
||||
public DBOpException(String message) {
|
||||
@ -77,7 +78,7 @@ public class DBOpException extends IllegalStateException implements ExceptionWit
|
||||
case 1022:
|
||||
case 23001:
|
||||
case 23505:
|
||||
context.related("Duplicate key")
|
||||
context.related(DUPLICATE_KEY)
|
||||
.whatToDo("Report this, duplicate key exists in SQL.");
|
||||
break;
|
||||
// Constraint violation
|
||||
@ -165,4 +166,9 @@ public class DBOpException extends IllegalStateException implements ExceptionWit
|
||||
&& getCause() != null
|
||||
&& getCause().getMessage().contains("user_id");
|
||||
}
|
||||
|
||||
public boolean isDuplicateKeyViolation() {
|
||||
return context != null
|
||||
&& context.getRelated().contains(DBOpException.CONSTRAINT_VIOLATION);
|
||||
}
|
||||
}
|
||||
|
@ -155,7 +155,7 @@ public class StorePlayerBooleanResultTransaction extends ThrowawayTransaction {
|
||||
"FROM plan_extension_providers indb " +
|
||||
"JOIN plan_extension_providers unfulfilled ON unfulfilled.condition_name=" +
|
||||
// This gives the unfulfilled condition, eg. if value is true not_condition is unfulfilled.
|
||||
(value ? "CONCAT('not_', " : "") + "indb.provided_condition" + (value ? ")" : "") +
|
||||
(value ? Sql.concat(dbType, "'not_'", "indb.provided_condition") : "indb.provided_condition") +
|
||||
" AND indb.plugin_id=unfulfilled.plugin_id" +
|
||||
" WHERE indb.id=" + ExtensionProviderTable.STATEMENT_SELECT_PROVIDER_ID +
|
||||
" AND indb.provided_condition IS NOT NULL";
|
||||
@ -181,7 +181,7 @@ public class StorePlayerBooleanResultTransaction extends ThrowawayTransaction {
|
||||
"FROM plan_extension_providers indb " +
|
||||
"JOIN plan_extension_tables unfulfilled ON unfulfilled.condition_name=" +
|
||||
// This gives the unfulfilled condition, eg. if value is true not_condition is unfulfilled.
|
||||
(value ? "CONCAT('not_', " : "") + "indb.provided_condition" + (value ? ")" : "") +
|
||||
(value ? Sql.concat(dbType, "'not_'", "indb.provided_condition") : "indb.provided_condition") +
|
||||
" AND indb.plugin_id=unfulfilled.plugin_id" +
|
||||
" WHERE indb.id=" + ExtensionProviderTable.STATEMENT_SELECT_PROVIDER_ID +
|
||||
" AND indb.provided_condition IS NOT NULL";
|
||||
|
@ -134,7 +134,7 @@ public class StoreServerBooleanResultTransaction extends ThrowawayTransaction {
|
||||
"FROM plan_extension_providers indb " +
|
||||
"JOIN plan_extension_providers unfulfilled ON unfulfilled.condition_name=" +
|
||||
// This gives the unfulfilled condition, eg. if value is true not_condition is unfulfilled.
|
||||
(value ? "CONCAT('not_', " : "") + "indb.provided_condition" + (value ? ")" : "") +
|
||||
(value ? Sql.concat(dbType, "'not_'", "indb.provided_condition") : "indb.provided_condition") +
|
||||
" AND indb.plugin_id=unfulfilled.plugin_id" +
|
||||
" WHERE indb.id=" + ExtensionProviderTable.STATEMENT_SELECT_PROVIDER_ID +
|
||||
" AND indb.provided_condition IS NOT NULL";
|
||||
@ -166,7 +166,7 @@ public class StoreServerBooleanResultTransaction extends ThrowawayTransaction {
|
||||
"FROM plan_extension_providers indb " +
|
||||
"JOIN plan_extension_tables unfulfilled ON unfulfilled.condition_name=" +
|
||||
// This gives the unfulfilled condition, eg. if value is true not_condition is unfulfilled.
|
||||
(value ? "CONCAT('not_', " : "") + "indb.provided_condition" + (value ? ")" : "") +
|
||||
(value ? Sql.concat(dbType, "'not_'", "indb.provided_condition") : "indb.provided_condition") +
|
||||
" AND indb.plugin_id=unfulfilled.plugin_id" +
|
||||
" WHERE indb.id=" + ExtensionProviderTable.STATEMENT_SELECT_PROVIDER_ID +
|
||||
" AND indb.provided_condition IS NOT NULL";
|
||||
|
@ -0,0 +1,94 @@
|
||||
/*
|
||||
* This file is part of Player Analytics (Plan).
|
||||
*
|
||||
* Plan is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License v3 as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Plan 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.djrapitops.plan.gathering;
|
||||
|
||||
import com.djrapitops.plan.settings.config.PlanConfig;
|
||||
import com.djrapitops.plan.settings.config.paths.DataGatheringSettings;
|
||||
import com.djrapitops.plan.utilities.dev.Untrusted;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Utility for validating and sanitizing join addresses.
|
||||
*
|
||||
* @author AuroraLS3
|
||||
*/
|
||||
@Singleton
|
||||
public class JoinAddressValidator {
|
||||
|
||||
private final PlanConfig config;
|
||||
private List<String> filteredAddresses;
|
||||
|
||||
@Inject
|
||||
public JoinAddressValidator(PlanConfig config) {
|
||||
/* Dagger injection constructor */
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
private void prepareFilteredAddresses() {
|
||||
if (filteredAddresses == null) {
|
||||
filteredAddresses = config.get(DataGatheringSettings.FILTER_JOIN_ADDRESSES);
|
||||
if (filteredAddresses == null) filteredAddresses = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
@Untrusted
|
||||
public String sanitize(@Untrusted String address) {
|
||||
if (address == null || config.isFalse(DataGatheringSettings.JOIN_ADDRESSES)) return "";
|
||||
if (!address.isEmpty()) {
|
||||
// Remove port
|
||||
if (address.contains(":")) {
|
||||
address = address.substring(0, address.lastIndexOf(':'));
|
||||
}
|
||||
// Remove data added by Bungeecord/Velocity
|
||||
if (address.contains("\u0000")) {
|
||||
address = address.substring(0, address.indexOf('\u0000'));
|
||||
}
|
||||
// Remove data added by Forge Mod Loader
|
||||
if (address.contains("fml")) {
|
||||
address = address.substring(0, address.lastIndexOf("fml"));
|
||||
}
|
||||
if (config.isFalse(DataGatheringSettings.PRESERVE_JOIN_ADDRESS_CASE)) {
|
||||
address = StringUtils.lowerCase(address);
|
||||
}
|
||||
prepareFilteredAddresses();
|
||||
if (filteredAddresses.contains(address)) {
|
||||
address = "";
|
||||
}
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
public boolean isValid(@Untrusted String address) {
|
||||
if (address.isEmpty()) return false;
|
||||
if (config.isTrue(DataGatheringSettings.PRESERVE_INVALID_JOIN_ADDRESS)) return true;
|
||||
try {
|
||||
URI uri = new URI(address);
|
||||
String path = uri.getPath();
|
||||
return path != null && path.indexOf('.') != -1;
|
||||
} catch (URISyntaxException uriSyntaxException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -16,6 +16,8 @@
|
||||
*/
|
||||
package com.djrapitops.plan.gathering.domain;
|
||||
|
||||
import com.djrapitops.plan.utilities.dev.Untrusted;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
@ -24,8 +26,10 @@ public interface PlatformPlayerData {
|
||||
|
||||
UUID getUUID();
|
||||
|
||||
@Untrusted
|
||||
String getName();
|
||||
|
||||
@Untrusted
|
||||
default Optional<String> getDisplayName() {
|
||||
return Optional.empty();
|
||||
}
|
||||
@ -38,6 +42,7 @@ public interface PlatformPlayerData {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Untrusted
|
||||
default Optional<String> getJoinAddress() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
@ -64,6 +64,13 @@ public class PlayerJoin {
|
||||
return time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get address used to join the server.
|
||||
*
|
||||
* @return Join address of the player.
|
||||
* @deprecated {@link com.djrapitops.plan.gathering.JoinAddressValidator} should be used when looking at join address.
|
||||
*/
|
||||
@Deprecated(since = "2024-04-27")
|
||||
public String getJoinAddress() {
|
||||
return player.getJoinAddress().orElse(JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP);
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import com.djrapitops.plan.delivery.domain.ServerName;
|
||||
import com.djrapitops.plan.delivery.export.Exporter;
|
||||
import com.djrapitops.plan.extension.CallEvents;
|
||||
import com.djrapitops.plan.extension.ExtensionSvc;
|
||||
import com.djrapitops.plan.gathering.JoinAddressValidator;
|
||||
import com.djrapitops.plan.gathering.cache.NicknameCache;
|
||||
import com.djrapitops.plan.gathering.cache.SessionCache;
|
||||
import com.djrapitops.plan.gathering.domain.ActiveSession;
|
||||
@ -38,7 +39,6 @@ import com.djrapitops.plan.storage.database.DBSystem;
|
||||
import com.djrapitops.plan.storage.database.sql.tables.JoinAddressTable;
|
||||
import com.djrapitops.plan.storage.database.transactions.Transaction;
|
||||
import com.djrapitops.plan.storage.database.transactions.events.*;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
@ -52,6 +52,7 @@ public class PlayerJoinEventConsumer {
|
||||
private final PlanConfig config;
|
||||
private final DBSystem dbSystem;
|
||||
|
||||
private final JoinAddressValidator joinAddressValidator;
|
||||
private final GeolocationCache geolocationCache;
|
||||
private final SessionCache sessionCache;
|
||||
private final NicknameCache nicknameCache;
|
||||
@ -63,7 +64,7 @@ public class PlayerJoinEventConsumer {
|
||||
public PlayerJoinEventConsumer(
|
||||
Processing processing,
|
||||
PlanConfig config,
|
||||
DBSystem dbSystem,
|
||||
DBSystem dbSystem, JoinAddressValidator joinAddressValidator,
|
||||
GeolocationCache geolocationCache,
|
||||
SessionCache sessionCache,
|
||||
NicknameCache nicknameCache,
|
||||
@ -73,6 +74,7 @@ public class PlayerJoinEventConsumer {
|
||||
this.processing = processing;
|
||||
this.config = config;
|
||||
this.dbSystem = dbSystem;
|
||||
this.joinAddressValidator = joinAddressValidator;
|
||||
this.geolocationCache = geolocationCache;
|
||||
this.sessionCache = sessionCache;
|
||||
this.nicknameCache = nicknameCache;
|
||||
@ -110,7 +112,8 @@ public class PlayerJoinEventConsumer {
|
||||
|
||||
private void storeJoinAddress(PlayerJoin join) {
|
||||
join.getPlayer().getJoinAddress()
|
||||
.map(joinAddress -> config.isTrue(DataGatheringSettings.PRESERVE_JOIN_ADDRESS_CASE) ? joinAddress : StringUtils.lowerCase(joinAddress))
|
||||
.map(joinAddressValidator::sanitize)
|
||||
.filter(joinAddressValidator::isValid)
|
||||
.map(StoreJoinAddressTransaction::new)
|
||||
.ifPresent(dbSystem.getDatabase()::executeTransaction);
|
||||
}
|
||||
@ -141,7 +144,10 @@ public class PlayerJoinEventConsumer {
|
||||
|
||||
private CompletableFuture<?> storeGamePlayer(PlayerJoin join) {
|
||||
long registerDate = getRegisterDate(join);
|
||||
String joinAddress = join.getPlayer().getJoinAddress().orElse(JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP);
|
||||
String joinAddress = join.getPlayer().getJoinAddress()
|
||||
.map(joinAddressValidator::sanitize)
|
||||
.filter(joinAddressValidator::isValid)
|
||||
.orElse(JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP);
|
||||
Transaction transaction = new StoreServerPlayerTransaction(
|
||||
join.getPlayerUUID(), registerDate, join.getPlayer().getName(), join.getServerUUID(), joinAddress
|
||||
);
|
||||
@ -171,12 +177,16 @@ public class PlayerJoinEventConsumer {
|
||||
}
|
||||
|
||||
private ActiveSession mapToActiveSession(PlayerJoin join) {
|
||||
String joinAddress = join.getPlayer().getJoinAddress()
|
||||
.map(joinAddressValidator::sanitize)
|
||||
.filter(joinAddressValidator::isValid)
|
||||
.orElse(JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP);
|
||||
ActiveSession session = new ActiveSession(join.getPlayerUUID(), join.getServerUUID(), join.getTime(),
|
||||
join.getPlayer().getCurrentWorld().orElse(null),
|
||||
join.getPlayer().getCurrentGameMode().orElse(null));
|
||||
session.getExtraData().put(PlayerName.class, new PlayerName(join.getPlayer().getName()));
|
||||
session.getExtraData().put(ServerName.class, new ServerName(join.getServer().isProxy() ? join.getServer().getName() : "Proxy Server"));
|
||||
session.getExtraData().put(JoinAddress.class, new JoinAddress(config.isTrue(DataGatheringSettings.PRESERVE_JOIN_ADDRESS_CASE) ? join.getJoinAddress() : StringUtils.lowerCase(join.getJoinAddress())));
|
||||
session.getExtraData().put(JoinAddress.class, new JoinAddress(joinAddress));
|
||||
return session;
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ import com.djrapitops.plan.storage.database.DBSystem;
|
||||
import com.djrapitops.plan.storage.database.Database;
|
||||
import com.djrapitops.plan.storage.database.queries.containers.ContainerFetchQueries;
|
||||
import com.djrapitops.plan.utilities.dev.Untrusted;
|
||||
import net.playeranalytics.plugin.server.PluginLogger;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@ -54,15 +55,18 @@ public final class PlanPlaceholders {
|
||||
|
||||
private final DBSystem dbSystem;
|
||||
private final Identifiers identifiers;
|
||||
private final PluginLogger logger;
|
||||
|
||||
@Inject
|
||||
public PlanPlaceholders(
|
||||
DBSystem dbSystem,
|
||||
Set<Placeholders> placeholderRegistries,
|
||||
Identifiers identifiers
|
||||
Identifiers identifiers,
|
||||
PluginLogger logger
|
||||
) {
|
||||
this.dbSystem = dbSystem;
|
||||
this.identifiers = identifiers;
|
||||
this.logger = logger;
|
||||
|
||||
this.playerPlaceholders = new HashMap<>();
|
||||
this.staticPlaceholders = new HashMap<>();
|
||||
@ -118,7 +122,8 @@ public final class PlanPlaceholders {
|
||||
if (dbSystem.getDatabase().getState() == Database.State.CLOSED) {
|
||||
return "Plan Bug #3020, please report";
|
||||
}
|
||||
throw e;
|
||||
logger.warn("Failed to get data for placeholder '" + placeholder + "', " + e.getMessage(), e);
|
||||
return "db error";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ package com.djrapitops.plan.placeholder;
|
||||
import com.djrapitops.plan.commands.use.Arguments;
|
||||
import com.djrapitops.plan.delivery.formatting.Formatter;
|
||||
import com.djrapitops.plan.delivery.formatting.Formatters;
|
||||
import com.djrapitops.plan.gathering.ServerUptimeCalculator;
|
||||
import com.djrapitops.plan.identification.Server;
|
||||
import com.djrapitops.plan.identification.ServerInfo;
|
||||
import com.djrapitops.plan.identification.ServerUUID;
|
||||
@ -58,18 +59,20 @@ public class ServerPlaceHolders implements Placeholders {
|
||||
private final DBSystem dbSystem;
|
||||
private final ServerInfo serverInfo;
|
||||
private final Formatters formatters;
|
||||
private final ServerUptimeCalculator serverUptimeCalculator;
|
||||
|
||||
@Inject
|
||||
public ServerPlaceHolders(
|
||||
PlanConfig config,
|
||||
DBSystem dbSystem,
|
||||
ServerInfo serverInfo,
|
||||
Formatters formatters
|
||||
Formatters formatters, ServerUptimeCalculator serverUptimeCalculator
|
||||
) {
|
||||
this.config = config;
|
||||
this.dbSystem = dbSystem;
|
||||
this.serverInfo = serverInfo;
|
||||
this.formatters = formatters;
|
||||
this.serverUptimeCalculator = serverUptimeCalculator;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -204,6 +207,11 @@ public class ServerPlaceHolders implements Placeholders {
|
||||
placeholders.registerStatic("server_name",
|
||||
() -> serverInfo.getServer().getName());
|
||||
|
||||
placeholders.registerStatic("server_uptime",
|
||||
parameters -> serverUptimeCalculator.getServerUptimeMillis(getServerUUID(parameters))
|
||||
.map(String::valueOf)
|
||||
.orElse("-"));
|
||||
|
||||
placeholders.registerStatic("server_uuid",
|
||||
serverInfo::getServerUUID);
|
||||
|
||||
|
@ -22,6 +22,7 @@ package com.djrapitops.plan.settings;
|
||||
* @author AuroraLS3
|
||||
*/
|
||||
public enum Permissions {
|
||||
USE_COMMAND("plan.command"),
|
||||
SERVER("plan.server"),
|
||||
SERVERS("plan.servers"),
|
||||
NETWORK("plan.network"),
|
||||
|
@ -200,6 +200,14 @@ public class ConfigNode {
|
||||
return key;
|
||||
}
|
||||
|
||||
private String getEnvironmentVariableKey() {
|
||||
String deepKey = parent != null ? parent.getKey(true) + "." + key : "";
|
||||
if (deepKey.startsWith(".")) {
|
||||
deepKey = deepKey.substring(1);
|
||||
}
|
||||
return "PLAN_" + StringUtils.replaceChars(StringUtils.upperCase(deepKey), '.', '_');
|
||||
}
|
||||
|
||||
public void sort() {
|
||||
Collections.sort(nodeOrder);
|
||||
}
|
||||
@ -255,32 +263,51 @@ public class ConfigNode {
|
||||
this.comment = comment;
|
||||
}
|
||||
|
||||
private String getEnvironmentVariable() {
|
||||
String key = getEnvironmentVariableKey();
|
||||
String variable = System.getenv(key);
|
||||
Map<String, String> env = System.getenv();
|
||||
return variable;
|
||||
}
|
||||
|
||||
public List<String> getStringList() {
|
||||
String environmentVariable = getEnvironmentVariable();
|
||||
if (environmentVariable != null) return new ConfigValueParser.StringListParser().compose(environmentVariable);
|
||||
return value == null ? Collections.emptyList()
|
||||
: new ConfigValueParser.StringListParser().compose(value);
|
||||
}
|
||||
|
||||
public Integer getInteger() {
|
||||
String environmentVariable = getEnvironmentVariable();
|
||||
if (environmentVariable != null) return new ConfigValueParser.IntegerParser().compose(environmentVariable);
|
||||
return value == null ? null
|
||||
: new ConfigValueParser.IntegerParser().compose(value);
|
||||
}
|
||||
|
||||
public Long getLong() {
|
||||
String environmentVariable = getEnvironmentVariable();
|
||||
if (environmentVariable != null) return new ConfigValueParser.LongParser().compose(environmentVariable);
|
||||
return value == null ? null
|
||||
: new ConfigValueParser.LongParser().compose(value);
|
||||
}
|
||||
|
||||
public String getString() {
|
||||
String environmentVariable = getEnvironmentVariable();
|
||||
if (environmentVariable != null) return new ConfigValueParser.StringParser().compose(environmentVariable);
|
||||
return value == null ? null
|
||||
: new ConfigValueParser.StringParser().compose(value);
|
||||
}
|
||||
|
||||
public Double getDouble() {
|
||||
String environmentVariable = getEnvironmentVariable();
|
||||
if (environmentVariable != null) return new ConfigValueParser.DoubleParser().compose(environmentVariable);
|
||||
return value == null ? null
|
||||
: new ConfigValueParser.DoubleParser().compose(value);
|
||||
}
|
||||
|
||||
public boolean getBoolean() {
|
||||
String environmentVariable = getEnvironmentVariable();
|
||||
if (environmentVariable != null) return new ConfigValueParser.BooleanParser().compose(environmentVariable);
|
||||
return new ConfigValueParser.BooleanParser().compose(value);
|
||||
}
|
||||
|
||||
|
@ -39,6 +39,8 @@ public interface ConfigValueParser<T> {
|
||||
return new BooleanParser();
|
||||
} else if (Long.class.isAssignableFrom(type)) {
|
||||
return new LongParser();
|
||||
} else if (Double.class.isAssignableFrom(type)) {
|
||||
return new DoubleParser();
|
||||
} else if (Integer.class.isAssignableFrom(type)) {
|
||||
return new IntegerParser();
|
||||
}
|
||||
|
@ -177,6 +177,9 @@ public class ConfigUpdater {
|
||||
new ConfigChange.Removed("Plugin.Use_Legacy_Frontend"),
|
||||
new ConfigChange.Removed("Customized_files.Enable_web_dev_mode"),
|
||||
new ConfigChange.Removed("Customized_files.Plan"),
|
||||
|
||||
new ConfigChange.Moved("Data_gathering.Preserve_join_address_case", "Data_gathering.Join_addresses.Preserve_case"),
|
||||
new ConfigChange.Moved("Data_gathering.Preserve_invalid_join_addresses", "Data_gathering.Join_addresses.Preserve_invalid"),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -18,8 +18,11 @@ package com.djrapitops.plan.settings.config.paths;
|
||||
|
||||
import com.djrapitops.plan.settings.config.paths.key.BooleanSetting;
|
||||
import com.djrapitops.plan.settings.config.paths.key.Setting;
|
||||
import com.djrapitops.plan.settings.config.paths.key.StringListSetting;
|
||||
import com.djrapitops.plan.settings.config.paths.key.StringSetting;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link Setting} values that are in "Data_gathering" section.
|
||||
*
|
||||
@ -34,7 +37,10 @@ public class DataGatheringSettings {
|
||||
public static final Setting<Boolean> DISK_SPACE = new BooleanSetting("Data_gathering.Disk_space");
|
||||
public static final Setting<Boolean> LOG_UNKNOWN_COMMANDS = new BooleanSetting("Data_gathering.Commands.Log_unknown");
|
||||
public static final Setting<Boolean> COMBINE_COMMAND_ALIASES = new BooleanSetting("Data_gathering.Commands.Log_aliases_as_main_command");
|
||||
public static final Setting<Boolean> PRESERVE_JOIN_ADDRESS_CASE = new BooleanSetting("Data_gathering.Preserve_join_address_case");
|
||||
public static final Setting<Boolean> JOIN_ADDRESSES = new BooleanSetting("Data_gathering.Join_addresses.Enabled");
|
||||
public static final Setting<Boolean> PRESERVE_JOIN_ADDRESS_CASE = new BooleanSetting("Data_gathering.Join_addresses.Preserve_case");
|
||||
public static final Setting<Boolean> PRESERVE_INVALID_JOIN_ADDRESS = new BooleanSetting("Data_gathering.Join_addresses.Preserve_invalid");
|
||||
public static final Setting<List<String>> FILTER_JOIN_ADDRESSES = new StringListSetting("Data_gathering.Join_addresses.Filter_out_from_data");
|
||||
|
||||
private DataGatheringSettings() {
|
||||
/* static variable class */
|
||||
|
@ -30,7 +30,7 @@ public enum LangCode {
|
||||
CS("\u010de\u0161tina", "Shadowhackercz, QuakyCZ, MrFriggo & WolverStones"),
|
||||
DE("Deutsch", "Eyremba, fuzzlemann, Morsmorse, hallo1142 & DubHacker"),
|
||||
FI("suomi", "AuroraLS3, KasperiP"),
|
||||
FR("français", "CyanTech, Aurelien & Nogapra"),
|
||||
FR("français", "CyanTech, Aurelien, Nogapra & Sniper_TVmc"),
|
||||
IT("Italiano", "Malachiel & Mastory_Md5"),
|
||||
JA("\u65E5\u672C\u8A9E", "yukieji, inductor, lis2a, yu_solt , Jumala9163 & ringoXD"),
|
||||
KO("\uD55C\uAD6D\uC5B4", "Guinness_Akihiko"),
|
||||
|
@ -108,7 +108,7 @@ public class LocaleSystem implements SubSystem {
|
||||
HtmlLang.values(),
|
||||
JSLang.values(),
|
||||
PluginLang.values(),
|
||||
WebPermission.values(),
|
||||
WebPermission.nonDeprecatedValues(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -215,7 +215,7 @@ public class LocaleSystem implements SubSystem {
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Locale> loadSettingLocale() {
|
||||
public Optional<Locale> loadSettingLocale() {
|
||||
try {
|
||||
String setting = config.get(PluginSettings.LOCALE);
|
||||
if ("write-all".equalsIgnoreCase(setting)) {
|
||||
|
@ -219,6 +219,8 @@ public enum HtmlLang implements Lang {
|
||||
LABEL_TITLE_SERVER_CALENDAR("html.label.serverCalendar", "Server Calendar"),
|
||||
LABEL_TITLE_NETWORK_CALENDAR("html.label.networkCalendar", "Network Calendar"),
|
||||
LABEL_LABEL_JOIN_ADDRESS("html.label.joinAddress", "Join Address"),
|
||||
LABEL_ADD_JOIN_ADDRESS_GROUP("html.label.addJoinAddressGroup", "Add address group"),
|
||||
LABEL_ADDRESS_GROUP("html.label.addressGroup", "Address group {{n}}"),
|
||||
LABEL_LABEL_SESSION_MEDIAN("html.label.medianSessionLength", "Median Session Length"),
|
||||
LABEL_LABEL_KDR("html.label.kdr", "KDR"),
|
||||
LABEL_TITLE_INSIGHTS("html.label.insights", "Insights"),
|
||||
@ -336,6 +338,7 @@ public enum HtmlLang implements Lang {
|
||||
QUERY_SERVERS_TWO("html.query.label.servers.two", "using data of 2 servers"),
|
||||
QUERY_SERVERS_MANY("html.query.label.servers.many", "using data of {number} servers"),
|
||||
QUERY_SHOW_FULL_QUERY("html.query.label.showFullQuery", "Show Full Query"),
|
||||
QUERY_EDIT_QUERY("html.query.label.editQuery", "Edit Query"),
|
||||
|
||||
HELP_TEST_RESULT("html.label.help.testResult", "Test result"),
|
||||
HELP_TEST_IT_OUT("html.label.help.testPrompt", "Test it out:"),
|
||||
|
@ -28,6 +28,8 @@ import com.djrapitops.plan.storage.database.sql.tables.ServerTable;
|
||||
import com.djrapitops.plan.storage.database.sql.tables.SessionsTable;
|
||||
import com.djrapitops.plan.storage.database.sql.tables.UsersTable;
|
||||
import com.djrapitops.plan.utilities.dev.Untrusted;
|
||||
import org.apache.commons.text.TextStringBuilder;
|
||||
import org.jetbrains.annotations.VisibleForTesting;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
@ -43,6 +45,7 @@ public class JoinAddressQueries {
|
||||
/* Static method class */
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static Query<Map<String, Integer>> latestJoinAddresses() {
|
||||
String selectLatestJoinAddresses = SELECT +
|
||||
"COUNT(1) as total," +
|
||||
@ -66,6 +69,7 @@ public class JoinAddressQueries {
|
||||
joinAddresses.put(UUID.fromString(set.getString(UsersTable.USER_UUID)), set.getString(JoinAddressTable.JOIN_ADDRESS));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static Query<Map<String, Integer>> latestJoinAddresses(ServerUUID serverUUID) {
|
||||
String selectLatestSessionStarts = SELECT + SessionsTable.USER_ID + ",MAX(" + SessionsTable.SESSION_START + ") as max_start" +
|
||||
FROM + SessionsTable.TABLE_NAME + " max_s" +
|
||||
@ -141,6 +145,28 @@ public class JoinAddressQueries {
|
||||
};
|
||||
}
|
||||
|
||||
public static QueryStatement<List<String>> allJoinAddresses(ServerUUID serverUUID) {
|
||||
String sql = SELECT + DISTINCT + JoinAddressTable.JOIN_ADDRESS +
|
||||
FROM + JoinAddressTable.TABLE_NAME + " j" +
|
||||
INNER_JOIN + SessionsTable.TABLE_NAME + " s ON s." + SessionsTable.JOIN_ADDRESS_ID + "=j." + JoinAddressTable.ID +
|
||||
WHERE + SessionsTable.SERVER_ID + "=" + ServerTable.SELECT_SERVER_ID +
|
||||
ORDER_BY + JoinAddressTable.JOIN_ADDRESS + " ASC";
|
||||
|
||||
return new QueryStatement<>(sql, 100) {
|
||||
@Override
|
||||
public void prepare(PreparedStatement statement) throws SQLException {
|
||||
statement.setString(1, serverUUID.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> processResults(ResultSet set) throws SQLException {
|
||||
List<String> joinAddresses = new ArrayList<>();
|
||||
while (set.next()) joinAddresses.add(set.getString(JoinAddressTable.JOIN_ADDRESS));
|
||||
return joinAddresses;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Query<List<String>> uniqueJoinAddresses() {
|
||||
return db -> {
|
||||
List<String> addresses = db.query(allJoinAddresses());
|
||||
@ -151,6 +177,16 @@ public class JoinAddressQueries {
|
||||
};
|
||||
}
|
||||
|
||||
public static Query<List<String>> uniqueJoinAddresses(ServerUUID serverUUID) {
|
||||
return db -> {
|
||||
List<String> addresses = db.query(allJoinAddresses(serverUUID));
|
||||
if (!addresses.contains(JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP)) {
|
||||
addresses.add(JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP);
|
||||
}
|
||||
return addresses;
|
||||
};
|
||||
}
|
||||
|
||||
public static Query<Set<Integer>> userIdsOfPlayersWithJoinAddresses(@Untrusted List<String> joinAddresses) {
|
||||
String sql = SELECT + DISTINCT + SessionsTable.USER_ID +
|
||||
FROM + JoinAddressTable.TABLE_NAME + " j" +
|
||||
@ -162,21 +198,27 @@ public class JoinAddressQueries {
|
||||
return db -> db.querySet(sql, RowExtractors.getInt(SessionsTable.USER_ID), joinAddresses.toArray());
|
||||
}
|
||||
|
||||
public static Query<List<DateObj<Map<String, Integer>>>> joinAddressesPerDay(ServerUUID serverUUID, long timezoneOffset, long after, long before) {
|
||||
public static Query<List<DateObj<Map<String, Integer>>>> joinAddressesPerDay(ServerUUID serverUUID, long timezoneOffset, long after, long before, @Untrusted List<String> addressFilter) {
|
||||
return db -> {
|
||||
Sql sql = db.getSql();
|
||||
|
||||
List<Integer> ids = db.query(joinAddressIds(addressFilter));
|
||||
if (ids != null && ids.isEmpty()) return List.of();
|
||||
|
||||
String selectAddresses = SELECT +
|
||||
sql.dateToEpochSecond(sql.dateToDayStamp(sql.epochSecondToDate('(' + SessionsTable.SESSION_START + "+?)/1000"))) +
|
||||
"*1000 as date," +
|
||||
JoinAddressTable.JOIN_ADDRESS +
|
||||
JoinAddressTable.JOIN_ADDRESS + ',' +
|
||||
SessionsTable.USER_ID +
|
||||
", COUNT(1) as count" +
|
||||
FROM + SessionsTable.TABLE_NAME + " s" +
|
||||
LEFT_JOIN + JoinAddressTable.TABLE_NAME + " j on s." + SessionsTable.JOIN_ADDRESS_ID + "=j." + JoinAddressTable.ID +
|
||||
WHERE + SessionsTable.SERVER_ID + "=" + ServerTable.SELECT_SERVER_ID +
|
||||
AND + SessionsTable.SESSION_START + ">?" +
|
||||
AND + SessionsTable.SESSION_START + "<=?" +
|
||||
GROUP_BY + "date,j." + JoinAddressTable.JOIN_ADDRESS;
|
||||
(ids == null ? "" : AND + "j." + JoinAddressTable.ID +
|
||||
" IN (" + new TextStringBuilder().appendWithSeparators(ids, ",").build() + ")") +
|
||||
GROUP_BY + "date,j." + JoinAddressTable.JOIN_ADDRESS + ',' + SessionsTable.USER_ID;
|
||||
|
||||
return db.query(new QueryStatement<>(selectAddresses, 1000) {
|
||||
@Override
|
||||
@ -193,9 +235,9 @@ public class JoinAddressQueries {
|
||||
while (set.next()) {
|
||||
long date = set.getLong("date");
|
||||
String joinAddress = set.getString(JoinAddressTable.JOIN_ADDRESS);
|
||||
int count = set.getInt("count");
|
||||
Map<String, Integer> joinAddresses = addressesByDate.computeIfAbsent(date, k -> new TreeMap<>());
|
||||
joinAddresses.put(joinAddress, count);
|
||||
// We ignore the count and get the number of players instead of sessions
|
||||
joinAddresses.compute(joinAddress, (key, oldValue) -> oldValue != null ? oldValue + 1 : 1);
|
||||
}
|
||||
|
||||
return addressesByDate.entrySet()
|
||||
@ -206,20 +248,37 @@ public class JoinAddressQueries {
|
||||
};
|
||||
}
|
||||
|
||||
public static Query<List<DateObj<Map<String, Integer>>>> joinAddressesPerDay(long timezoneOffset, long after, long before) {
|
||||
public static Query<List<Integer>> joinAddressIds(@Untrusted List<String> addresses) {
|
||||
return db -> {
|
||||
if (addresses.isEmpty()) return null;
|
||||
|
||||
String selectJoinAddressIds = SELECT + JoinAddressTable.ID +
|
||||
FROM + JoinAddressTable.TABLE_NAME +
|
||||
WHERE + JoinAddressTable.JOIN_ADDRESS + " IN (" + Sql.nParameters(addresses.size()) + ")";
|
||||
return db.queryList(selectJoinAddressIds, set -> set.getInt(JoinAddressTable.ID), addresses);
|
||||
};
|
||||
}
|
||||
|
||||
public static Query<List<DateObj<Map<String, Integer>>>> joinAddressesPerDay(long timezoneOffset, long after, long before, @Untrusted List<String> addressFilter) {
|
||||
return db -> {
|
||||
Sql sql = db.getSql();
|
||||
|
||||
List<Integer> ids = db.query(joinAddressIds(addressFilter));
|
||||
if (ids != null && ids.isEmpty()) return List.of();
|
||||
|
||||
String selectAddresses = SELECT +
|
||||
sql.dateToEpochSecond(sql.dateToDayStamp(sql.epochSecondToDate('(' + SessionsTable.SESSION_START + "+?)/1000"))) +
|
||||
"*1000 as date," +
|
||||
JoinAddressTable.JOIN_ADDRESS +
|
||||
JoinAddressTable.JOIN_ADDRESS + ',' +
|
||||
SessionsTable.USER_ID +
|
||||
", COUNT(1) as count" +
|
||||
FROM + SessionsTable.TABLE_NAME + " s" +
|
||||
LEFT_JOIN + JoinAddressTable.TABLE_NAME + " j on s." + SessionsTable.JOIN_ADDRESS_ID + "=j." + JoinAddressTable.ID +
|
||||
WHERE + SessionsTable.SESSION_START + ">?" +
|
||||
AND + SessionsTable.SESSION_START + "<=?" +
|
||||
GROUP_BY + "date,j." + JoinAddressTable.JOIN_ADDRESS;
|
||||
(ids == null ? "" : AND + "j." + JoinAddressTable.ID +
|
||||
" IN (" + new TextStringBuilder().appendWithSeparators(ids, ",").build() + ")") +
|
||||
GROUP_BY + "date,j." + JoinAddressTable.JOIN_ADDRESS + ',' + SessionsTable.USER_ID;
|
||||
|
||||
return db.query(new QueryStatement<>(selectAddresses, 1000) {
|
||||
@Override
|
||||
@ -235,9 +294,9 @@ public class JoinAddressQueries {
|
||||
while (set.next()) {
|
||||
long date = set.getLong("date");
|
||||
String joinAddress = set.getString(JoinAddressTable.JOIN_ADDRESS);
|
||||
int count = set.getInt("count");
|
||||
Map<String, Integer> joinAddresses = addressesByDate.computeIfAbsent(date, k -> new TreeMap<>());
|
||||
joinAddresses.put(joinAddress, count);
|
||||
// We ignore the count and get the number of players instead of sessions
|
||||
joinAddresses.compute(joinAddress, (key, oldValue) -> oldValue != null ? oldValue + 1 : 1);
|
||||
}
|
||||
|
||||
return addressesByDate.entrySet()
|
||||
|
@ -16,6 +16,7 @@
|
||||
*/
|
||||
package com.djrapitops.plan.storage.database.sql.building;
|
||||
|
||||
import com.djrapitops.plan.storage.database.DBType;
|
||||
import org.apache.commons.text.TextStringBuilder;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
@ -96,6 +97,15 @@ public abstract class Sql {
|
||||
}
|
||||
}
|
||||
|
||||
public static String concat(DBType dbType, String one, String two) {
|
||||
if (dbType == DBType.MYSQL) {
|
||||
return "CONCAT(" + one + ',' + two + ")";
|
||||
} else if (dbType == DBType.SQLITE) {
|
||||
return one + " || " + two;
|
||||
}
|
||||
return one + two;
|
||||
}
|
||||
|
||||
public abstract String epochSecondToDate(String sql);
|
||||
|
||||
public abstract String dateToEpochSecond(String sql);
|
||||
@ -108,6 +118,8 @@ public abstract class Sql {
|
||||
|
||||
public abstract String dateToHour(String sql);
|
||||
|
||||
public abstract String insertOrIgnore();
|
||||
|
||||
// https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html
|
||||
public static class MySQL extends Sql {
|
||||
|
||||
@ -140,6 +152,11 @@ public abstract class Sql {
|
||||
public String dateToHour(String sql) {
|
||||
return "HOUR(" + sql + ") % 24";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String insertOrIgnore() {
|
||||
return "INSERT IGNORE INTO ";
|
||||
}
|
||||
}
|
||||
|
||||
// https://sqlite.org/lang_datefunc.html
|
||||
@ -174,5 +191,10 @@ public abstract class Sql {
|
||||
public String dateToHour(String sql) {
|
||||
return "strftime('%H'," + sql + ')';
|
||||
}
|
||||
|
||||
@Override
|
||||
public String insertOrIgnore() {
|
||||
return "INSERT OR IGNORE INTO ";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,10 @@ public class WebPermissionTable {
|
||||
/* Static information class */
|
||||
}
|
||||
|
||||
public static String safeInsertSQL(DBType dbType) {
|
||||
return dbType.getSql().insertOrIgnore() + TABLE_NAME + " (" + PERMISSION + ") VALUES (?)";
|
||||
}
|
||||
|
||||
public static String createTableSQL(DBType dbType) {
|
||||
return CreateTableBuilder.create(TABLE_NAME, dbType)
|
||||
.column(ID, Sql.INT).primaryKey()
|
||||
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* This file is part of Player Analytics (Plan).
|
||||
*
|
||||
* Plan is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License v3 as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Plan 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.djrapitops.plan.storage.database.transactions.commands;
|
||||
|
||||
import com.djrapitops.plan.storage.database.sql.tables.webuser.SecurityTable;
|
||||
import com.djrapitops.plan.storage.database.sql.tables.webuser.WebGroupTable;
|
||||
import com.djrapitops.plan.storage.database.sql.tables.webuser.WebGroupToPermissionTable;
|
||||
import com.djrapitops.plan.storage.database.sql.tables.webuser.WebUserPreferencesTable;
|
||||
import com.djrapitops.plan.storage.database.transactions.Transaction;
|
||||
|
||||
/**
|
||||
* Transaction that removes all web groups from the database.
|
||||
*
|
||||
* @author AuroraLS3
|
||||
*/
|
||||
public class RemoveWebGroupsTransaction extends Transaction {
|
||||
|
||||
@Override
|
||||
protected void performOperations() {
|
||||
clearTable(WebUserPreferencesTable.TABLE_NAME);
|
||||
clearTable(SecurityTable.TABLE_NAME);
|
||||
clearTable(WebGroupToPermissionTable.TABLE_NAME);
|
||||
clearTable(WebGroupTable.TABLE_NAME);
|
||||
}
|
||||
|
||||
private void clearTable(String tableName) {
|
||||
execute("DELETE FROM " + tableName);
|
||||
}
|
||||
}
|
@ -57,7 +57,7 @@ public class StoreRequestTransaction extends ThrowawayTransaction {
|
||||
@Override
|
||||
public void prepare(PreparedStatement statement) throws SQLException {
|
||||
statement.setLong(1, timestamp);
|
||||
statement.setString(2, accessAddress);
|
||||
statement.setString(2, StringUtils.truncate(accessAddress, 45));
|
||||
statement.setString(3, method);
|
||||
statement.setString(4, url);
|
||||
statement.setInt(5, responseCode);
|
||||
|
@ -51,6 +51,8 @@ public class CorrectWrongCharacterEncodingPatch extends Patch {
|
||||
if (dbType != DBType.MYSQL) return true;
|
||||
|
||||
correctionSqlQueries = query(getBadTableCorrectionQueries());
|
||||
// Fix for MariaDB mysql.user table being a view
|
||||
correctionSqlQueries.removeIf(sql -> sql.startsWith("ALTER TABLE `user`"));
|
||||
return correctionSqlQueries.isEmpty();
|
||||
}
|
||||
|
||||
@ -61,6 +63,7 @@ public class CorrectWrongCharacterEncodingPatch extends Patch {
|
||||
String selectTablesWithWrongCharset = "SELECT CONCAT('ALTER TABLE `', table_name, '` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;')\n" +
|
||||
"FROM information_schema.TABLES AS T, information_schema.`COLLATION_CHARACTER_SET_APPLICABILITY` AS C\n" +
|
||||
"WHERE C.collation_name = T.table_collation\n" +
|
||||
"AND T.table_name LIKE 'plan\\_%'\n" +
|
||||
"AND T.table_schema = '" + databaseName + "'\n" +
|
||||
"AND\n" +
|
||||
"(\n" +
|
||||
@ -72,6 +75,7 @@ public class CorrectWrongCharacterEncodingPatch extends Patch {
|
||||
String selectColumnsWithWrongCharset = "SELECT CONCAT('ALTER TABLE `', table_name, '` MODIFY `', column_name, '` ', DATA_TYPE, '(', CHARACTER_MAXIMUM_LENGTH, ') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci', (CASE WHEN IS_NULLABLE = 'NO' THEN ' NOT NULL' ELSE '' END), ';')\n" +
|
||||
"FROM information_schema.COLUMNS \n" +
|
||||
"WHERE TABLE_SCHEMA = '" + databaseName + "'\n" +
|
||||
"AND table_name LIKE 'plan\\_%'\n" +
|
||||
"AND DATA_TYPE = 'varchar'\n" +
|
||||
"AND\n" +
|
||||
"(\n" +
|
||||
|
@ -17,7 +17,7 @@
|
||||
package com.djrapitops.plan.storage.database.transactions.patches;
|
||||
|
||||
import com.djrapitops.plan.exceptions.database.DBOpException;
|
||||
import com.djrapitops.plan.storage.database.DBType;
|
||||
import com.djrapitops.plan.storage.database.sql.building.Sql;
|
||||
import com.djrapitops.plan.storage.database.sql.tables.webuser.SecurityTable;
|
||||
import com.djrapitops.plan.storage.database.sql.tables.webuser.WebGroupTable;
|
||||
|
||||
@ -71,7 +71,7 @@ public class SecurityTableGroupPatch extends Patch {
|
||||
SecurityTable.USERNAME + ',' +
|
||||
SecurityTable.LINKED_TO + ',' +
|
||||
SecurityTable.SALT_PASSWORD_HASH + ',' +
|
||||
"(" + SELECT + WebGroupTable.ID + FROM + WebGroupTable.TABLE_NAME + WHERE + WebGroupTable.NAME + "=" + (dbType == DBType.SQLITE ? "'legacy_level_' || permission_level" : "CONCAT('legacy_level_', permission_level)") + ")" +
|
||||
"(" + SELECT + WebGroupTable.ID + FROM + WebGroupTable.TABLE_NAME + WHERE + WebGroupTable.NAME + "=" + Sql.concat(dbType, "'legacy_level_'", "permission_level") + ")" +
|
||||
FROM + tempTableName
|
||||
);
|
||||
|
||||
|
@ -39,7 +39,7 @@ public class UpdateWebPermissionsPatch extends Patch {
|
||||
|
||||
@Override
|
||||
public boolean hasBeenApplied() {
|
||||
List<String> defaultPermissions = Arrays.stream(WebPermission.values())
|
||||
List<String> defaultPermissions = Arrays.stream(WebPermission.nonDeprecatedValues())
|
||||
.map(WebPermission::getPermission)
|
||||
.collect(Collectors.toList());
|
||||
List<String> storedPermissions = query(WebUserQueries.fetchAvailablePermissions());
|
||||
@ -55,7 +55,11 @@ public class UpdateWebPermissionsPatch extends Patch {
|
||||
|
||||
@Override
|
||||
protected void applyPatch() {
|
||||
execute(new ExecBatchStatement(WebPermissionTable.INSERT_STATEMENT) {
|
||||
storeMissing();
|
||||
}
|
||||
|
||||
private void storeMissing() {
|
||||
execute(new ExecBatchStatement(WebPermissionTable.safeInsertSQL(dbType)) {
|
||||
@Override
|
||||
public void prepare(PreparedStatement statement) throws SQLException {
|
||||
for (String permission : missingPermissions) {
|
||||
|
@ -50,7 +50,7 @@ public class StoreMissingWebPermissionsTransaction extends Transaction {
|
||||
missingPermissions.add(permission);
|
||||
}
|
||||
}
|
||||
execute(new ExecBatchStatement(WebPermissionTable.INSERT_STATEMENT) {
|
||||
execute(new ExecBatchStatement(WebPermissionTable.safeInsertSQL(dbType)) {
|
||||
@Override
|
||||
public void prepare(PreparedStatement statement) throws SQLException {
|
||||
for (String permission : missingPermissions) {
|
||||
|
@ -114,8 +114,14 @@ Data_gathering:
|
||||
Geolocation_Download_URL: "https://geodb.playeranalytics.net/GeoLite2-Country.mmdb"
|
||||
Ping: true
|
||||
Disk_space: true
|
||||
# Does not affect already gathered data
|
||||
Preserve_join_address_case: false
|
||||
Join_addresses:
|
||||
Enabled: true
|
||||
# Does not affect already gathered data
|
||||
Preserve_case: false
|
||||
Preserve_invalid: false
|
||||
# Replaces these join addresses with unknown
|
||||
Filter_out_from_data:
|
||||
- "play.example.com"
|
||||
# -----------------------------------------------------
|
||||
# Supported time units: MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS
|
||||
# -----------------------------------------------------
|
||||
|
@ -118,8 +118,14 @@ Data_gathering:
|
||||
Commands:
|
||||
Log_unknown: false
|
||||
Log_aliases_as_main_command: true
|
||||
# Does not affect already gathered data
|
||||
Preserve_join_address_case: false
|
||||
Join_addresses:
|
||||
Enabled: true
|
||||
# Does not affect already gathered data
|
||||
Preserve_case: false
|
||||
Preserve_invalid: false
|
||||
# Replaces these join addresses with unknown
|
||||
Filter_out_from_data:
|
||||
- "play.example.com"
|
||||
# -----------------------------------------------------
|
||||
# Supported time units: MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS
|
||||
# -----------------------------------------------------
|
||||
|
@ -290,17 +290,19 @@ html:
|
||||
active: "活跃"
|
||||
activePlaytime: "活跃时间"
|
||||
activityIndex: "活跃指数"
|
||||
addJoinAddressGroup: "添加地址组"
|
||||
addressGroup: "地址组 {{n}}"
|
||||
afk: "挂机"
|
||||
afkTime: "挂机时间"
|
||||
all: "全部"
|
||||
allTime: "所有时间"
|
||||
allowed: "Allowed"
|
||||
allowlist: "Allowlist"
|
||||
allowlistBounces: "Allowlist Bounces"
|
||||
allowed: "允许"
|
||||
allowlist: "白名单"
|
||||
allowlistBounces: "白名单退回"
|
||||
alphabetical: "按字母顺序"
|
||||
apply: "应用"
|
||||
asNumbers: "数据"
|
||||
attempts: "Attempts"
|
||||
attempts: "尝试"
|
||||
average: "平均"
|
||||
averageActivePlaytime: "平均活跃时间"
|
||||
averageAfkTime: "平均挂机时间"
|
||||
@ -321,7 +323,7 @@ html:
|
||||
banned: "已被封禁"
|
||||
bestPeak: "历史最高峰值"
|
||||
bestPing: "最低延迟"
|
||||
blocked: "Blocked"
|
||||
blocked: "阻止"
|
||||
calendar: " 日历"
|
||||
comparing7days: "对比 7 天的情况"
|
||||
connectionInfo: "连接信息"
|
||||
@ -435,10 +437,10 @@ html:
|
||||
last24hours: "过去 24 小时"
|
||||
last30days: "过去 30 天"
|
||||
last7days: "过去 7 天"
|
||||
lastAllowed: "Last Allowed"
|
||||
lastBlocked: "Last Blocked"
|
||||
lastAllowed: "最后允许"
|
||||
lastBlocked: "最后阻止"
|
||||
lastConnected: "最后连接时间"
|
||||
lastKnownAttempt: "Last Known Attempt"
|
||||
lastKnownAttempt: "最后一次已知的尝试"
|
||||
lastPeak: "上次在线峰值"
|
||||
lastSeen: "最后在线时间"
|
||||
latestJoinAddresses: "上一次加入地址"
|
||||
@ -664,7 +666,6 @@ html:
|
||||
page_network_geolocations_ping_per_country: "查看按国家划分的Ping表"
|
||||
page_network_join_addresses: "查看加入地址 - 选项卡"
|
||||
page_network_join_addresses_graphs: "查看加入地址图表"
|
||||
page_network_join_addresses_graphs_pie: "查看最新加入地址图表"
|
||||
page_network_join_addresses_graphs_time: "查看加入地址随时间变化的图表"
|
||||
page_network_overview: "查看网络总览 - 选项卡"
|
||||
page_network_overview_graphs: "查看网络总览图表"
|
||||
@ -694,13 +695,12 @@ html:
|
||||
page_player_sessions: "查看玩家会话 - 选项卡"
|
||||
page_player_versus: "查看PvP和PvE - 选项卡"
|
||||
page_server: "查看所有服务器页面"
|
||||
page_server_allowlist_bounce: "See list of Game allowlist bounces"
|
||||
page_server_allowlist_bounce: "查看游戏白名单退回列表"
|
||||
page_server_geolocations: "查看服务器地理位置 - 选项卡"
|
||||
page_server_geolocations_map: "查看服务器地理位置地图"
|
||||
page_server_geolocations_ping_per_country: "查看按国家划分的延迟表"
|
||||
page_server_join_addresses: "查看服务器加入地址 - 选项卡"
|
||||
page_server_join_addresses_graphs: "查看服务器加入地址图表"
|
||||
page_server_join_addresses_graphs_pie: "查看最新加入地址图表"
|
||||
page_server_join_addresses_graphs_time: "查看服务器加入地址随时间变化的图表"
|
||||
page_server_online_activity: "查看在线活动 - 选项卡"
|
||||
page_server_online_activity_graphs: "查看在线活动图表"
|
||||
@ -794,6 +794,7 @@ html:
|
||||
generic:
|
||||
are: "`是`"
|
||||
label:
|
||||
editQuery: "修改查询"
|
||||
from: ">从 </label>"
|
||||
makeAnother: "进行另一个查询"
|
||||
servers:
|
||||
@ -906,7 +907,7 @@ plugin:
|
||||
no: "否"
|
||||
today: "'今天'"
|
||||
unavailable: "不可用"
|
||||
unknown: "位置"
|
||||
unknown: "未知"
|
||||
yes: "是"
|
||||
yesterday: "'昨天'"
|
||||
localeReloaded: "自定义 locale.yml 已被修改,因此已重新加载并且现在正在使用。"
|
||||
|
@ -290,6 +290,8 @@ html:
|
||||
active: "Aktivní"
|
||||
activePlaytime: "Aktivní herní čas"
|
||||
activityIndex: "Index aktivity"
|
||||
addJoinAddressGroup: "Add address group"
|
||||
addressGroup: "Address group {{n}}"
|
||||
afk: "AFK"
|
||||
afkTime: "AFK čas"
|
||||
all: "Vše"
|
||||
@ -664,7 +666,6 @@ html:
|
||||
page_network_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_network_join_addresses: "See Join Addresses -tab"
|
||||
page_network_join_addresses_graphs: "See Join Address graphs"
|
||||
page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_network_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_network_overview: "See Network Overview -tab"
|
||||
page_network_overview_graphs: "See Network Overview graphs"
|
||||
@ -700,7 +701,6 @@ html:
|
||||
page_server_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_server_join_addresses: "See Join Addresses -tab"
|
||||
page_server_join_addresses_graphs: "See Join Address graphs"
|
||||
page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_server_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_server_online_activity: "See Online Activity -tab"
|
||||
page_server_online_activity_graphs: "See Online Activity graphs"
|
||||
@ -794,6 +794,7 @@ html:
|
||||
generic:
|
||||
are: "` jsou`"
|
||||
label:
|
||||
editQuery: "Edit Query"
|
||||
from: ">od</label>"
|
||||
makeAnother: "Vytvořit další dotaz"
|
||||
servers:
|
||||
|
@ -290,6 +290,8 @@ html:
|
||||
active: "Aktiv"
|
||||
activePlaytime: "Aktive Spielzeit"
|
||||
activityIndex: "Aktivitätsindex"
|
||||
addJoinAddressGroup: "Add address group"
|
||||
addressGroup: "Address group {{n}}"
|
||||
afk: "AFK"
|
||||
afkTime: "AFK Zeit"
|
||||
all: "Gesamt"
|
||||
@ -664,7 +666,6 @@ html:
|
||||
page_network_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_network_join_addresses: "See Join Addresses -tab"
|
||||
page_network_join_addresses_graphs: "See Join Address graphs"
|
||||
page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_network_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_network_overview: "See Network Overview -tab"
|
||||
page_network_overview_graphs: "See Network Overview graphs"
|
||||
@ -700,7 +701,6 @@ html:
|
||||
page_server_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_server_join_addresses: "See Join Addresses -tab"
|
||||
page_server_join_addresses_graphs: "See Join Address graphs"
|
||||
page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_server_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_server_online_activity: "See Online Activity -tab"
|
||||
page_server_online_activity_graphs: "See Online Activity graphs"
|
||||
@ -794,6 +794,7 @@ html:
|
||||
generic:
|
||||
are: "`are`"
|
||||
label:
|
||||
editQuery: "Edit Query"
|
||||
from: ">from</label>"
|
||||
makeAnother: "Make another query"
|
||||
servers:
|
||||
|
@ -290,6 +290,8 @@ html:
|
||||
active: "Active"
|
||||
activePlaytime: "Active Playtime"
|
||||
activityIndex: "Activity Index"
|
||||
addJoinAddressGroup: "Add address group"
|
||||
addressGroup: "Address group {{n}}"
|
||||
afk: "AFK"
|
||||
afkTime: "AFK Time"
|
||||
all: "All"
|
||||
@ -664,7 +666,6 @@ html:
|
||||
page_network_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_network_join_addresses: "See Join Addresses -tab"
|
||||
page_network_join_addresses_graphs: "See Join Address graphs"
|
||||
page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_network_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_network_overview: "See Network Overview -tab"
|
||||
page_network_overview_graphs: "See Network Overview graphs"
|
||||
@ -700,7 +701,6 @@ html:
|
||||
page_server_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_server_join_addresses: "See Join Addresses -tab"
|
||||
page_server_join_addresses_graphs: "See Join Address graphs"
|
||||
page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_server_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_server_online_activity: "See Online Activity -tab"
|
||||
page_server_online_activity_graphs: "See Online Activity graphs"
|
||||
@ -794,6 +794,7 @@ html:
|
||||
generic:
|
||||
are: "`are`"
|
||||
label:
|
||||
editQuery: "Edit Query"
|
||||
from: ">from</label>"
|
||||
makeAnother: "Make another query"
|
||||
servers:
|
||||
|
@ -290,6 +290,8 @@ html:
|
||||
active: "Activo"
|
||||
activePlaytime: "Tiempo de juego activo"
|
||||
activityIndex: "Índice de actividad"
|
||||
addJoinAddressGroup: "Add address group"
|
||||
addressGroup: "Address group {{n}}"
|
||||
afk: "AFK"
|
||||
afkTime: "Tiempo AFK"
|
||||
all: "Todo"
|
||||
@ -664,7 +666,6 @@ html:
|
||||
page_network_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_network_join_addresses: "See Join Addresses -tab"
|
||||
page_network_join_addresses_graphs: "See Join Address graphs"
|
||||
page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_network_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_network_overview: "See Network Overview -tab"
|
||||
page_network_overview_graphs: "See Network Overview graphs"
|
||||
@ -700,7 +701,6 @@ html:
|
||||
page_server_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_server_join_addresses: "See Join Addresses -tab"
|
||||
page_server_join_addresses_graphs: "See Join Address graphs"
|
||||
page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_server_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_server_online_activity: "See Online Activity -tab"
|
||||
page_server_online_activity_graphs: "See Online Activity graphs"
|
||||
@ -794,6 +794,7 @@ html:
|
||||
generic:
|
||||
are: "`are`"
|
||||
label:
|
||||
editQuery: "Edit Query"
|
||||
from: ">de</label>"
|
||||
makeAnother: "Realiza otra consulta"
|
||||
servers:
|
||||
|
@ -290,6 +290,8 @@ html:
|
||||
active: "Aktiivinen"
|
||||
activePlaytime: "Aktiivinen peliaika"
|
||||
activityIndex: "Aktiivisuus Indeksi"
|
||||
addJoinAddressGroup: "Add address group"
|
||||
addressGroup: "Address group {{n}}"
|
||||
afk: "AFK"
|
||||
afkTime: "Aika AFK:ina"
|
||||
all: "Kaikki"
|
||||
@ -664,7 +666,6 @@ html:
|
||||
page_network_geolocations_ping_per_country: "Näkee Viive per Maa -taulun"
|
||||
page_network_join_addresses: "Näkee Liittymäosoitteet osion"
|
||||
page_network_join_addresses_graphs: "Näkee Liittymisosoite kaaviot"
|
||||
page_network_join_addresses_graphs_pie: "Näkee kaavion Viimeisimmistä liittymisosoitteista"
|
||||
page_network_join_addresses_graphs_time: "Näkee kaavion Liittymisosoitteista ajan yli"
|
||||
page_network_overview: "Näkee Verkoston katsaus osion"
|
||||
page_network_overview_graphs: "Näkee Verkoston katsaus kaaviot"
|
||||
@ -700,7 +701,6 @@ html:
|
||||
page_server_geolocations_ping_per_country: "Näkee Viive per Maa -taulun"
|
||||
page_server_join_addresses: "Näkee Liittymäosoitteet osion"
|
||||
page_server_join_addresses_graphs: "Näkee Liittymisosoite kaaviot"
|
||||
page_server_join_addresses_graphs_pie: "Näkee kaavion Viimeisimmistä liittymisosoitteista"
|
||||
page_server_join_addresses_graphs_time: "Näkee kaavion Liittymisosoitteista ajan yli"
|
||||
page_server_online_activity: "Näkee Online Aktiivisuus osion"
|
||||
page_server_online_activity_graphs: "Näkee Online Aktiivisuus kaaviot"
|
||||
@ -794,6 +794,7 @@ html:
|
||||
generic:
|
||||
are: "`ovat`"
|
||||
label:
|
||||
editQuery: "Edit Query"
|
||||
from: ">tästä</label>"
|
||||
makeAnother: "Tee toinen kysely"
|
||||
servers:
|
||||
|
@ -24,8 +24,8 @@ command:
|
||||
description: "Nom de la fonctionnalité à désactiver : ${0}"
|
||||
name: "fonctionnalité"
|
||||
group:
|
||||
description: "Web Permission Group, case sensitive."
|
||||
name: "group"
|
||||
description: "Groupe de permissions Web, sensible à la casse."
|
||||
name: "groupe"
|
||||
importKind: "type d'importation"
|
||||
nameOrUUID:
|
||||
description: "Nom ou UUID d'un joueur"
|
||||
@ -67,16 +67,16 @@ command:
|
||||
failSameServer: "Impossible de marquer ce serveur comme désinstallé (vous y êtes)."
|
||||
hotswap: "§eN'oubliez pas de passer à la nouvelle base de données (/plan db hotswap ${0}) & de recharger Plan."
|
||||
importers: "Importateurs :"
|
||||
preparing: "Preparing.."
|
||||
progress: "${0} / ${1} traité(s).."
|
||||
preparing: "Préparation..."
|
||||
progress: "${0} / ${1} traité(s)."
|
||||
start: "> §2Traitement des données..."
|
||||
success: "> §aSuccès !"
|
||||
playerRemoval: "Suppression des données de ${0} depuis ${1}.."
|
||||
removal: "Suppression des données de Plan depuis ${0}.."
|
||||
playerRemoval: "Suppression des données de ${0} depuis ${1}."
|
||||
removal: "Suppression des données de Plan depuis ${0}."
|
||||
serverUninstalled: "§aSi le serveur est toujours installé, il se définira automatiquement comme dans la base de données."
|
||||
unregister: "Désenregistrement de '${0}'.."
|
||||
warnDbNotOpen: "§eLa base de données est : ${0} - Cela pourrait prendre plus de temps que prévu..."
|
||||
write: "Écriture à ${0}.."
|
||||
unregister: "Désenregistrement de '${0}'."
|
||||
warnDbNotOpen: "§eLa base de données est : ${0} - Cela pourrait prendre plus de temps que prévu."
|
||||
write: "Écriture à ${0}."
|
||||
fail:
|
||||
emptyString: "La chaîne de recherche ne peut pas être vide"
|
||||
invalidArguments: "Accepte les éléments suivants comme ${0} : ${1}"
|
||||
@ -172,7 +172,7 @@ command:
|
||||
description: "Déconnecter les autres utilisateurs du panel"
|
||||
inDepth: "Donnez un nom d'utilisateur à déconnecter du panel, donnez * comme argument pour déconnecter tout le monde."
|
||||
migrateToOnlineUuids:
|
||||
description: "Migrate offline uuid data to online uuids"
|
||||
description: "Migrer les données des uuid hors ligne vers les uuids en ligne"
|
||||
network:
|
||||
description: "Visualiser la page du réseau"
|
||||
inDepth: "Obtient un lien vers la page /network, uniquement sur les réseaux."
|
||||
@ -189,7 +189,7 @@ command:
|
||||
description: "Recharger Plan"
|
||||
inDepth: "Désactive et active le plugin pour recharger tout changement dans la configuration."
|
||||
removejoinaddresses:
|
||||
description: "Remove join addresses of a specified server"
|
||||
description: "Supprimer les adresses de connexion d'un serveur spécifié"
|
||||
search:
|
||||
description: "Rechercher un joueur"
|
||||
inDepth: "Liste tous les noms de joueurs correspondant à une partie donnée d'un nom."
|
||||
@ -200,8 +200,8 @@ command:
|
||||
description: "Obtenir la liste des serveurs dans la base de données"
|
||||
inDepth: "Liste les ids, noms et uuids des serveurs de la base de données."
|
||||
setgroup:
|
||||
description: "Change users web permission group."
|
||||
inDepth: "Allows you to change a users web permission group to an existing web group. Use /plan groups for list of available groups."
|
||||
description: "Changer le groupe de permissions des utilisateur du site web."
|
||||
inDepth: "Permet de changer un groupe de permissions web d'un utilisateur par un groupe web existant. Utilisez /plan groups pour obtenir la liste des groupes disponibles."
|
||||
unregister:
|
||||
description: "Désenregistrer un utilisateur du site de Plan"
|
||||
inDepth: "Utilisez sans argument pour désenregistrer un utilisateur lié à un joueur, ou avec un nom d'utilisateur pour désenregistrer un autre utilisateur."
|
||||
@ -248,17 +248,17 @@ html:
|
||||
unique: "Unique :"
|
||||
description:
|
||||
newPlayerRetention: "Cette valeur est une prédiction basée sur les joueurs précédents."
|
||||
noData24h: "Server has not sent data for over 24 hours."
|
||||
noData30d: "Server has not sent data for over 30 days."
|
||||
noData7d: "Server has not sent data for over 7 days."
|
||||
noData24h: "Le serveur n'a pas envoyé de données depuis plus de 24 heures."
|
||||
noData30d: "Le serveur n'a pas envoyé de données depuis plus de 30 jours."
|
||||
noData7d: "Le serveur n'a pas envoyé de données depuis plus de 7 jours."
|
||||
noGameServers: "Certaines données nécessitent l'installation de Plan sur les serveurs de jeu."
|
||||
noGeolocations: "La collecte de la géolocalisation doit être activée dans la configuration (Accepter l'EULA de GeoLite2)."
|
||||
noServerOnlinActivity: "Aucun serveur pour afficher l'activité en ligne."
|
||||
noServers: "Il n'y a pas de serveur dans la base de données."
|
||||
noServersLong: 'Il semblerait que Plan ne soit installé sur aucun des serveurs de jeu ou qu'il ne soit pas connecté à la même base de données. Voir <a href="https://github.com/plan-player-analytics/Plan/wiki">wiki</a> pour un tutoriel sur la mise en place d'un Réseau.'
|
||||
noServersLong: 'Il semblerait que Plan ne soit installé sur aucun des serveurs de jeu ou qu''il ne soit pas connecté à la même base de données. Voir <a href="https://github.com/plan-player-analytics/Plan/wiki">wiki</a> pour un tutoriel sur la mise en place d''un Réseau.'
|
||||
noSpongeChunks: "Chunks indisponibles sur Sponge"
|
||||
noUptimeCalculation: "Server is offline, or has never restarted with Plan installed."
|
||||
performanceNoGameServers: "TPS, Entity or Chunk data is not gathered from proxy servers since they don't have game tick loop."
|
||||
noUptimeCalculation: "Le serveur est hors ligne ou n'a jamais redémarré avec Plan installé."
|
||||
performanceNoGameServers: "Les données TPS, Entité ou Chunk ne sont pas collectées à partir des serveurs proxy car ils n'ont pas de boucle de tick de jeu."
|
||||
predictedNewPlayerRetention: "Cette valeur est une prédiction basée sur les anciennes données du joueur."
|
||||
error:
|
||||
401Unauthorized: "Non autorisé."
|
||||
@ -275,173 +275,175 @@ html:
|
||||
groupNotFound: "Web Permission Group does not exist"
|
||||
loginFailed: "L'utilisateur et le mot de passe ne correspondent pas"
|
||||
noCookie: "Cookie de l'utilisateur non présent"
|
||||
noPermissionGroup: "Registration failed, player did not have any 'plan.webgroup.{name}' permission"
|
||||
noPermissionGroup: "L'enregistrement a échoué, le joueur n'avait pas la permission 'plan.webgroup.{name}'"
|
||||
registrationFailed: "Enregistrement échoué, veuillez réessayer (Le code expire au bout de 15 minutes)"
|
||||
userNotFound: "Cet utilisateur n'existe pas"
|
||||
authFailed: "Authentification échouée."
|
||||
authFailedTips: "- Assurez-vous d'avoir enregistré un utilisateur avec :<b>'/plan register'.</b><br>- Vérifiez que le nom d'utilisateur et le mot de passe soient corrects.<br>- Le nom d'utilisateur et le mot de passe sont sensibles au format majuscule/minuscule.<br><br>Si vous avez oublié votre mot de passe, demandez à un membre du staff de supprimer votre ancien utilisateur puis de vous réinscrire."
|
||||
noServersOnline: "Aucun serveur en ligne pour exécuter la demande."
|
||||
playerNotSeen: "Cet utilisateur ne s'est jamais connecté sur ce serveur."
|
||||
serverNotExported: "Server doesn't exist, its data might not have been exported yet."
|
||||
serverNotSeen: "Server doesn't exist"
|
||||
serverNotExported: "Le serveur n'existe pas, ses données n'ont peut-être pas encore été exportées."
|
||||
serverNotSeen: "Le serveur n'existe pas"
|
||||
generic:
|
||||
none: "Vide"
|
||||
label:
|
||||
active: "Actif"
|
||||
activePlaytime: "Temps Actif"
|
||||
activityIndex: "Indice d'Activité"
|
||||
addJoinAddressGroup: "Ajouter un groupe d'adresses"
|
||||
addressGroup: "Groupe d'adresses {{n}}"
|
||||
afk: "AFK"
|
||||
afkTime: "Temps AFK"
|
||||
all: "Tout"
|
||||
allTime: "Tout le Temps"
|
||||
allowed: "Allowed"
|
||||
allowlist: "Allowlist"
|
||||
allowlistBounces: "Allowlist Bounces"
|
||||
alphabetical: "Alphabetical"
|
||||
apply: "Apply"
|
||||
allowed: "Autorisé"
|
||||
allowlist: "Liste d'autorisés"
|
||||
allowlistBounces: "Rebonds de la liste d'autorisés"
|
||||
alphabetical: "Alphabétique"
|
||||
apply: "Appliquer"
|
||||
asNumbers: "en Chiffres"
|
||||
attempts: "Attempts"
|
||||
average: "Average first session length"
|
||||
attempts: "Tentatives"
|
||||
average: "Durée moyenne de la première session"
|
||||
averageActivePlaytime: "Temps Actif moyen"
|
||||
averageAfkTime: "Temps AFK moyen"
|
||||
averageChunks: "Quantité moyenne de Chunks"
|
||||
averageCpuUsage: "Average CPU Usage"
|
||||
averageCpuUsage: "Utilisation moyenne du CPU"
|
||||
averageEntities: "Quantité moyenne d'Entités"
|
||||
averageKdr: "Ratio - Kills / Morts - moyen"
|
||||
averageMobKdr: "Ratio - Kills / Morts - de Mobs moyen"
|
||||
averagePing: "Latence moyenne"
|
||||
averagePlayers: "Average Players"
|
||||
averagePlayers: "Moyenne des joueurs"
|
||||
averagePlaytime: "Temps de Jeu moyen"
|
||||
averageRamUsage: "Average RAM Usage"
|
||||
averageServerDowntime: "Average Downtime / Server"
|
||||
averageRamUsage: "Utilisation moyenne de la RAM"
|
||||
averageServerDowntime: "Temps d'arrêt moyen par serveur"
|
||||
averageSessionLength: "Durée moyenne d'une Session"
|
||||
averageSessions: "Quantité moyenne de Sessions"
|
||||
averageTps: "TPS moyen"
|
||||
averageTps7days: "Average TPS (7 days)"
|
||||
averageTps7days: "TPS moyen (7 days)"
|
||||
banned: "Banni(e)"
|
||||
bestPeak: "Pic maximal de Joueurs en Ligne"
|
||||
bestPing: "Meilleure Latence"
|
||||
blocked: "Blocked"
|
||||
blocked: "Bloqué"
|
||||
calendar: " Calendrier"
|
||||
comparing7days: "Comparaison des 7 derniers Jours"
|
||||
connectionInfo: "Renseignements sur la Connexion"
|
||||
country: "Pays"
|
||||
cpu: "CPU"
|
||||
cpuRam: "CPU & RAM"
|
||||
cpuUsage: "CPU Usage"
|
||||
cpuUsage: "Utilisation du CPU"
|
||||
currentPlayerbase: "Base de Joueurs actuelle"
|
||||
currentUptime: "Current Uptime"
|
||||
currentlyInstalledPlugins: "Currently Installed Plugins"
|
||||
currentUptime: "Période d'activité actuelle"
|
||||
currentlyInstalledPlugins: "Plugins actuellement installés"
|
||||
dayByDay: "Jour par Jour"
|
||||
dayOfweek: "Jour de la Semaine"
|
||||
deadliestWeapon: "1ère Arme de Combat (la plus mortelle)"
|
||||
deaths: "Morts"
|
||||
disk: "Espace Disque"
|
||||
diskSpace: "Espace Disque disponible"
|
||||
docs: "Swagger Docs"
|
||||
docs: "Docs Swagger"
|
||||
downtime: "Temps Hors-Ligne"
|
||||
duringLowTps: "Pendant les pics de TPS bas :"
|
||||
entities: "Entités"
|
||||
errors: "Plan Error Logs"
|
||||
export: "Export"
|
||||
exported: "Data export time"
|
||||
errors: "Journaux d'erreurs de Plan"
|
||||
export: "Exportation"
|
||||
exported: "Temps d'exportation des données"
|
||||
favoriteServer: "Serveur Favori"
|
||||
firstSession: "Première session"
|
||||
firstSessionLength:
|
||||
average: "Average first session length"
|
||||
median: "Median first session length"
|
||||
average: "Durée moyenne de la première session"
|
||||
median: "Durée médiane de la première session"
|
||||
geoProjection:
|
||||
dropdown: "Select projection"
|
||||
dropdown: "Sélectionner la projection"
|
||||
equalEarth: "Equal Earth"
|
||||
mercator: "Mercator"
|
||||
miller: "Miller"
|
||||
ortographic: "Ortographic"
|
||||
ortographic: "Ortographique"
|
||||
geolocations: "Géolocalisation"
|
||||
groupPermissions: "Manage Groups"
|
||||
groupUsers: "Manage Group Users"
|
||||
groupPermissions: "Gérer les groupes"
|
||||
groupUsers: "Gérer les utilisateurs d'un groupe"
|
||||
help:
|
||||
activityIndexBasis: "Activity index is based on non-AFK playtime in the past 3 weeks (21 days). Each week is considered separately."
|
||||
activityIndexExample1: "If someone plays as much as threshold every week, they are given activity index ~3."
|
||||
activityIndexExample2: "Very active is ~2x the threshold (y ≥ 3.75)."
|
||||
activityIndexExample3: "The index approaches 5 indefinitely."
|
||||
activityIndexVisual: "Here is a visualization of the curve where y = activity index, and x = playtime per week / threshold."
|
||||
activityIndexWeek: "Week {}"
|
||||
examples: "Examples"
|
||||
activityIndexBasis: "L'indice d'activité est basé sur le temps de jeu non-AFK au cours des 3 dernières semaines (21 jours). Chaque semaine est considérée séparément."
|
||||
activityIndexExample1: "Si une personne joue autant que le seuil fixé chaque semaine, elle se voit attribuer un indice d'activité de ~3."
|
||||
activityIndexExample2: "Très actif est ~2x le seuil (y ≥ 3,75)."
|
||||
activityIndexExample3: "L'indice se rapproche indéfiniment de 5."
|
||||
activityIndexVisual: "Voici une visualisation de la courbe où y = indice d'activité, et x = temps de jeu par semaine / seuil."
|
||||
activityIndexWeek: "Semaine {}"
|
||||
examples: "Exemples"
|
||||
graph:
|
||||
labels: "You can hide/show a group by clicking on the label at the bottom."
|
||||
title: "Graph"
|
||||
zoom: "You can Zoom in by click + dragging on the graph."
|
||||
labels: "Vous pouvez masquer/afficher un groupe en cliquant sur l'étiquette en bas de page."
|
||||
title: "Graphique"
|
||||
zoom: "Vous pouvez zoomer en cliquant sur le graphique et en le faisant glisser."
|
||||
manage:
|
||||
groups:
|
||||
line-1: "This view allows you to modify web group permissions."
|
||||
line-10: "<1>{{permission}}</> permissions determine what parts of the page are visible. These permissions also limit requests to the related data endpoints."
|
||||
line-11: "<1>{{permission1}}</1> permissions are not required for data: <2>{{permission2}}</2> allows request to /v1/network/overview even without <3>{{permission3}}</3>."
|
||||
line-12: "Saving changes"
|
||||
line-13: "When you add a group or delete a group that action is saved immediately after confirm (no undo)."
|
||||
line-14: "When you modify permissions those changes need to be saved by pressing the Save-button"
|
||||
line-15: "Documentation can be found from <1>{{link}}</1>"
|
||||
line-2: "User's web group is determined during <1>{{command}}</1> by checking if Player has <2>{{permission}}</2> permission."
|
||||
line-3: "You can use <1>{{command}}</1> to change permission group after registering."
|
||||
line-4: "<1>{{icon}}</1> If you ever accidentally delete all groups with <2>{{permission}}</2> permission just <3>{{command}}</3>."
|
||||
line-5: "Permission inheritance"
|
||||
line-6: "Permissions follow inheritance model, where higher level permission grants all lower ones, eg. <1>{{permission1}}</1> also gives <2>{{permission2}}</2>, etc."
|
||||
line-7: "Access vs Page -permissions"
|
||||
line-8: "You need to assign both access and page permissions for users."
|
||||
line-9: "<1>{{permission1}}</1> permissions allow user make the request to specific address, eg. <2>{{permission2}}</2> allows request to /network."
|
||||
playtimeUnit: "hours"
|
||||
line-1: "Cette vue vous permet de modifier les autorisations des groupes web."
|
||||
line-10: "<1>{{permission}}</> déterminent quelles parties de la page sont visibles. Ces autorisations limitent également les requêtes vers les points d'accès aux données connexes."
|
||||
line-11: "<1>{{permission1}}</1> ne sont pas nécessaires pour les données: <2>{{permission2}}</2> permet de faire une requête à /v1/network/overview même sans <3>{{permission3}}</3>."
|
||||
line-12: "Sauvegarder les modifications"
|
||||
line-13: "Lorsque vous ajoutez ou supprimez un groupe, l'action est sauvegardée immédiatement après confirmation (pas d'annulation)."
|
||||
line-14: "Lorsque vous modifiez les autorisations, ces changements doivent être enregistrés en cliquant sur le bouton Enregistrer."
|
||||
line-15: "La documentation est disponible à l'adresse suivante <1>{{link}}</1>"
|
||||
line-2: "Le groupe web de l'utilisateur est déterminé pendant <1>{{command}}</1> en vérifiant si le joueur a la permission <2>{{permission}}</2>."
|
||||
line-3: "Vous pouvez utiliser <1>{{command}}</1> pour changer de groupe d'autorisation après l'enregistrement."
|
||||
line-4: "<1>{{icon}}</1> Si vous supprimez accidentellement tous les groupes avec la permission <2>{{permission}}</2> utilisez juste <3>{{command}}</3>."
|
||||
line-5: "Héritage de permissions"
|
||||
line-6: "Les autorisations suivent le modèle de l'héritage, où l'autorisation de niveau supérieur accorde toutes les autorisations de niveau inférieur, par exemple. <1>{{permission1}}</1> donne également <2>{{permission2}}</2>, etc."
|
||||
line-7: "Accès vs. Permissions d'accès aux pages"
|
||||
line-8: "Vous devez attribuer des droits d'accès et des droits de page aux utilisateurs."
|
||||
line-9: "<1>{{permission1}}</1> les autorisations permettent à l'utilisateur d'adresser une demande à une adresse spécifique, par exemple. <2>{{permission2}}</2> permet de faire une requête à /network."
|
||||
playtimeUnit: "heures"
|
||||
retention:
|
||||
calculationStep1: "First the data is filtered using '<>' option. Any players with 'registerDate' outside the time range are ignored."
|
||||
calculationStep2: "Then it is grouped into groups of players using '<0>' option, eg. With '<1>': All players who registered in January 2023, February 2023, etc"
|
||||
calculationStep3: "Then the '<0>' and '<1>' options select which visualization to render."
|
||||
calculationStep4: "'<>' controls how many points the graph has, eg. 'Days' has one point per day."
|
||||
calculationStep5: "On each calculated point all players are checked for the condition."
|
||||
calculationStep6: "Select X Axis below to see conditions."
|
||||
calculationStepDate: "This visualization shows the different groups of players that are still playing on your server. The visualization uses lastSeen date. If x < lastSeenDate, the player is visible on the graph."
|
||||
calculationStepDeltas: "This visualization is most effective using Player Count as the Y Axis. The visualization shows net gain of players (How many players joined minus players who stopped playing). The visualization uses both registered and lastSeen dates. If registerDate < x < lastSeenDate, the player is visible on the graph."
|
||||
calculationStepPlaytime: "This visualization tells how long the gameplay loop keeps players engaged on your server. The visualization uses playtime. If x < playtime, the player is visible on the graph."
|
||||
calculationStepTime: "This visualization tells how long people keep coming back to play on the server after they join the first time. The visualization uses timeDifference. If x < timeDifference, the player is visible on the graph."
|
||||
compareJoinAddress: "Grouping by join address allows measuring advertising campaigns on different sites."
|
||||
compareMonths: "You can compare different months by changing the '<0>' option to '<1>'"
|
||||
calculationStep1: "Les données sont d'abord filtrées à l'aide de l'option '<>'. Tous les joueurs avec 'registerDate' en dehors de l'intervalle de temps sont ignorés."
|
||||
calculationStep2: "Il est ensuite réparti en groupes de joueurs à l'aide de l'option '<0>', par exemple. Avec '<1>': Tous les joueurs qui se sont inscrits en janvier 2023, février 2023, etc."
|
||||
calculationStep3: "Les options '<0>' et '<1>' permettent ensuite de sélectionner la visualisation à restituer."
|
||||
calculationStep4: "'<>' détermine le nombre de points du graphique, par exemple 'Jours' correspond à un point par jour."
|
||||
calculationStep5: "A chaque point calculé, tous les joueurs sont contrôlés pour la condition."
|
||||
calculationStep6: "Sélectionnez l'axe X ci-dessous pour voir les conditions."
|
||||
calculationStepDate: "Cette visualisation montre les différents groupes de joueurs qui jouent encore sur votre serveur. La visualisation utilise la date lastSeen. Si x < lastSeenDate, le joueur est visible sur le graphique."
|
||||
calculationStepDeltas: "Cette visualisation est plus efficace si l'on utilise le nombre de joueurs comme axe des ordonnées. La visualisation montre le gain net de joueurs (combien de joueurs se sont inscrits moins les joueurs qui ont arrêté de jouer). La visualisation utilise les dates d'inscription et de dernière visite. Si registerDate < x < lastSeenDate, le joueur est visible sur le graphique."
|
||||
calculationStepPlaytime: "Cette visualisation indique combien de temps la boucle de gameplay maintient l'attention des joueurs sur votre serveur. La visualisation utilise le temps de jeu. Si x < temps de jeu, le joueur est visible sur le graphique."
|
||||
calculationStepTime: "Cette visualisation indique combien de temps les gens reviennent jouer sur le serveur après leur première inscription. La visualisation utilise timeDifference. Si x < timeDifference, le joueur est visible sur le graphique."
|
||||
compareJoinAddress: "Le regroupement par adresse de jointure permet de mesurer les campagnes publicitaires sur différents sites."
|
||||
compareMonths: "Vous pouvez comparer plusieurs mois en remplaçant l'option '<0>' par '<1>'"
|
||||
examples:
|
||||
adCampaign: "Comparing player gain of different ad campaigns using different Join Addresses (anonymized)"
|
||||
deltas: "<> shows net gain of players."
|
||||
pattern: "A general pattern emerges when all players start leaving the server at the same time"
|
||||
plateau: "Comparing player gain of different months. Plateaus suggest there were players Plan doesn't know about. In this example Plan was installed in January 2022."
|
||||
playtime: "Playtime tells how long the gameplay loop keeps players engaged on your server."
|
||||
stack: "Cumulative player gain can be checked with stacked player count as Y axis"
|
||||
howIsItCalculated: "How it is calculated"
|
||||
howIsItCalculatedData: "The graph is generated from player data:"
|
||||
options: "Select the options to analyze different aspects of Player Retention."
|
||||
retentionBasis: "New player retention is calculated based on session data. If a registered player has played within latter half of the timespan, they are considered retained."
|
||||
testPrompt: "Test it out:"
|
||||
testResult: "Test result"
|
||||
threshold: "Threshold"
|
||||
thresholdUnit: "hours / week"
|
||||
tips: "Tips"
|
||||
usingTheGraph: "Using the Graph"
|
||||
adCampaign: "Comparaison des gains de joueurs de différentes campagnes publicitaires utilisant différentes adresses de jointure (anonymisées)"
|
||||
deltas: "<> indique le gain net de joueurs."
|
||||
pattern: "Un schéma général se dessine lorsque tous les joueurs commencent à quitter le serveur en même temps"
|
||||
plateau: "Comparaison des gains de joueurs au cours des différents mois. Les plateaux suggèrent qu'il y a des joueurs dont Plan n'a pas connaissance. Dans cet exemple, Plan a été installé en janvier 2022."
|
||||
playtime: "Le temps de jeu indique combien de temps la boucle de gameplay maintient l'attention des joueurs sur votre serveur."
|
||||
stack: "Le gain cumulatif de joueurs peut être vérifié en utilisant le nombre de joueurs empilés comme axe Y."
|
||||
howIsItCalculated: "Mode de calcul"
|
||||
howIsItCalculatedData: "Le graphique est généré à partir des données des joueurs:"
|
||||
options: "Sélectionnez les options pour analyser les différents aspects de la fidélisation des joueurs."
|
||||
retentionBasis: "La fidélisation des nouveaux joueurs est calculée sur la base des données de session. Si un joueur enregistré a joué dans la seconde moitié de la période, il est considéré comme fidélisé."
|
||||
testPrompt: "Testez-le:"
|
||||
testResult: "Résultat du test"
|
||||
threshold: "Seuil"
|
||||
thresholdUnit: "heures / semaine"
|
||||
tips: "Conseils"
|
||||
usingTheGraph: "Utilisation du graphique"
|
||||
hourByHour: "Heure par Heure"
|
||||
inactive: "Inactif(ve)"
|
||||
indexInactive: "Inactif"
|
||||
indexRegular: "Régulier"
|
||||
information: "INFORMATIONS"
|
||||
insights: "Insights"
|
||||
insights: "Perspectives"
|
||||
insights30days: "Perspectives sur 30 jours"
|
||||
installed: "Installed"
|
||||
installed: "Installé"
|
||||
irregular: "Irrégulier"
|
||||
joinAddress: "Join Address"
|
||||
joinAddress: "Adresse de Connexion"
|
||||
joinAddresses: "Adresses de Connexion"
|
||||
kdr: "KDR"
|
||||
killed: "Tué(e)"
|
||||
last24hours: "24 Dernières heures"
|
||||
last30days: "30 Derniers jours"
|
||||
last7days: "7 Derniers jours"
|
||||
lastAllowed: "Last Allowed"
|
||||
lastBlocked: "Last Blocked"
|
||||
lastConnected: "Dernier Connecté"
|
||||
lastKnownAttempt: "Last Known Attempt"
|
||||
lastAllowed: "Dernier autorisé"
|
||||
lastBlocked: "Dernier bloqué"
|
||||
lastConnected: "Dernier connecté"
|
||||
lastKnownAttempt: "Dernière tentative connue"
|
||||
lastPeak: "Dernier pic de Joueurs en Ligne"
|
||||
lastSeen: "Dernière Connexion"
|
||||
latestJoinAddresses: "Latest Join Addresses"
|
||||
latestJoinAddresses: "Dernières adresses d'adhésion"
|
||||
length: " Longueur"
|
||||
links: "LIENS"
|
||||
loadedChunks: "Chunks Chargés"
|
||||
@ -450,53 +452,53 @@ html:
|
||||
loneNewbieJoins: "Connexions de Débutants Seuls"
|
||||
longestSession: "Session la plus Longue"
|
||||
lowTpsSpikes: "Pics de TPS bas"
|
||||
lowTpsSpikes7days: "Low TPS Spikes (7 days)"
|
||||
manage: "Manage"
|
||||
lowTpsSpikes7days: "Pics de TPS bas (7 jours)"
|
||||
manage: "Gérer"
|
||||
managePage:
|
||||
addGroup:
|
||||
header: "Add group"
|
||||
invalidName: "Group name can be 100 characters maximum."
|
||||
name: "Name of the group"
|
||||
header: "Ajouter un groupe"
|
||||
invalidName: "Le nom du groupe peut comporter 100 caractères au maximum."
|
||||
name: "Nom du groupe"
|
||||
alert:
|
||||
groupAddFail: "Failed to add group: {{error}}"
|
||||
groupAddSuccess: "Added group '{{groupName}}'"
|
||||
groupDeleteFail: "Failed to delete group: {{error}}"
|
||||
groupDeleteSuccess: "Deleted group '{{groupName}}'"
|
||||
saveFail: "Failed to save changes: {{error}}"
|
||||
saveSuccess: "Changes saved successfully!"
|
||||
groupAddFail: "Échec de l'ajout d'un groupe: {{error}}"
|
||||
groupAddSuccess: "Groupe ajouté '{{groupName}}'"
|
||||
groupDeleteFail: "Échec de la suppression du groupe: {{error}}"
|
||||
groupDeleteSuccess: "Groupe supprimé '{{groupName}}'"
|
||||
saveFail: "Échec de l'enregistrement des modifications: {{error}}"
|
||||
saveSuccess: "Les modifications ont été enregistrées avec succès!"
|
||||
changes:
|
||||
discard: "Discard Changes"
|
||||
save: "Save"
|
||||
unsaved: "Unsaved changes"
|
||||
discard: "Rejeter les modifications"
|
||||
save: "Sauvegarder"
|
||||
unsaved: "Modifications non sauvegardées"
|
||||
deleteGroup:
|
||||
confirm: "Confirm & Delete {{groupName}}"
|
||||
confirmDescription: "This will move all users of '{{groupName}}' to group '{{moveTo}}'. There is no undo!"
|
||||
header: "Delete '{{groupName}}'"
|
||||
moveToSelect: "Move remaining users to group"
|
||||
groupHeader: "Manage Group Permissions"
|
||||
groupPermissions: "Permissions of {{groupName}}"
|
||||
confirm: "Confirmer et supprimer {{groupName}}"
|
||||
confirmDescription: "Cette action déplacera tous les utilisateurs de '{{nomdugroupe}}' vers le groupe '{{moveTo}}'. Il n'y a pas d'annulation possible !"
|
||||
header: "Supprimer '{{groupName}}'"
|
||||
moveToSelect: "Déplacer les utilisateurs restants vers le groupe"
|
||||
groupHeader: "Gérer les permissions du groupe"
|
||||
groupPermissions: "Permissions de {{groupName}}"
|
||||
maxFreeDisk: "Espace Disque MAX disponible"
|
||||
medianSessionLength: "Median Session Length"
|
||||
medianSessionLength: "Durée médiane de la session"
|
||||
minFreeDisk: "Espace Disque MIN disponible"
|
||||
mobDeaths: "Morts causées par un Mob"
|
||||
mobKdr: "Ratio - Kills / Morts de Mobs -"
|
||||
mobKills: "Kills de Mobs"
|
||||
modified: "Modified"
|
||||
mobKills: "Mobs Tués"
|
||||
modified: "Modifié"
|
||||
mostActiveGamemode: "Mode de Jeu le plus utilisé"
|
||||
mostPlayedWorld: "Monde le plus Fréquenté"
|
||||
name: "Nom"
|
||||
network: "Réseau"
|
||||
networkAsNumbers: "Réseau en Chiffres"
|
||||
networkCalendar: "Network Calendar"
|
||||
networkCalendar: "Calendrier du réseau"
|
||||
networkOnlineActivity: "Activité en Ligne du Réseau"
|
||||
networkOverview: "Aperçu du Réseau"
|
||||
networkPage: "Page du Réseau"
|
||||
new: "Nouveau(elle)"
|
||||
newPlayerRetention: "Rétention des nouveaux Joueurs"
|
||||
newPlayers: "Nouveaux Joueurs"
|
||||
newPlayers7days: "New Players (7 days)"
|
||||
newPlayers7days: "Nouveaux joueurs (7 jours)"
|
||||
nickname: "Surnom"
|
||||
noDataToDisplay: "No Data to Display"
|
||||
noDataToDisplay: "Pas de données à afficher"
|
||||
now: "Maintenant"
|
||||
onlineActivity: "Activité en ligne"
|
||||
onlineActivityAsNumbers: "Activité en ligne en Chiffres"
|
||||
@ -512,23 +514,23 @@ html:
|
||||
player: "Joueur"
|
||||
playerDeaths: "Décès causés par le Joueur"
|
||||
playerKills: "Kills de Joueurs"
|
||||
playerKillsVictimIndicator: "Player was killed within 24h of first time they were seen (Time since registered: <>)."
|
||||
playerKillsVictimIndicator: "Le joueur a été tué dans les 24 heures suivant sa première apparition (Temps écoulé depuis l'enregistrement : <>)."
|
||||
playerList: "Liste des Joueurs"
|
||||
playerOverview: "Aperçu des Joueurs"
|
||||
playerPage: "Page du Joueur"
|
||||
playerRetention: "Player Retention"
|
||||
playerRetention: "Fidélisation des joueurs"
|
||||
playerbase: "Base de Joueurs"
|
||||
playerbaseDevelopment: "Évolution de la base de Joueurs"
|
||||
playerbaseOverview: "Playerbase Overview"
|
||||
playerbaseOverview: "Aperçu de la base de joueurs"
|
||||
players: "Joueurs"
|
||||
playersOnline: "Joueurs en Ligne"
|
||||
playersOnlineNow: "Players Online (Now)"
|
||||
playersOnlineNow: "Joueurs en ligne (maintenant)"
|
||||
playersOnlineOverview: "Aperçu de l'Activité en Ligne"
|
||||
playtime: "Temps de Jeu"
|
||||
pluginHistory: "Plugin History"
|
||||
pluginVersionHistory: "Plugin Version History"
|
||||
pluginHistory: "Historique des plugins"
|
||||
pluginVersionHistory: "Historique de la version du plugin"
|
||||
plugins: "Plugins"
|
||||
pluginsOverview: "Plugins Overview"
|
||||
pluginsOverview: "Aperçu des plugins"
|
||||
punchcard: "Carte Perforée"
|
||||
punchcard30days: "Carte perforée sur 30 jours"
|
||||
pvpPve: "PvP & PvE"
|
||||
@ -547,19 +549,19 @@ html:
|
||||
regularPlayers: "Joueurs Réguliers"
|
||||
relativeJoinActivity: "Activité de Connexion relative"
|
||||
retention:
|
||||
groupByNone: "No grouping"
|
||||
groupByTime: "Group registered by"
|
||||
inAnytime: "any time"
|
||||
inLast180d: "in the last 6 months"
|
||||
inLast30d: "in the last 30 days"
|
||||
inLast365d: "in the last 12 months"
|
||||
inLast730d: "in the last 24 months"
|
||||
inLast7d: "in the last 7 days"
|
||||
inLast90d: "in the last 3 months"
|
||||
playersRegisteredInTime: "Players who registered"
|
||||
retainedPlayersPercentage: "Retained Players %"
|
||||
timeSinceRegistered: "Time since register date"
|
||||
timeStep: "Time step"
|
||||
groupByNone: "Pas de regroupement"
|
||||
groupByTime: "Groupe enregistré par"
|
||||
inAnytime: "à tout moment"
|
||||
inLast180d: "au cours des 6 derniers mois"
|
||||
inLast30d: "au cours des 30 derniers jours"
|
||||
inLast365d: "au cours des 12 derniers mois"
|
||||
inLast730d: "au cours des 24 derniers mois"
|
||||
inLast7d: "au cours des 7 derniers jours"
|
||||
inLast90d: "au cours des 3 derniers mois"
|
||||
playersRegisteredInTime: "Joueurs inscrits"
|
||||
retainedPlayersPercentage: "Joueurs retenus %"
|
||||
timeSinceRegistered: "Temps écoulé depuis la date d'enregistrement"
|
||||
timeStep: "Pas de temps"
|
||||
secondDeadliestWeapon: "2ᵉ Arme de Combat"
|
||||
seenNicknames: "Surnoms vus"
|
||||
server: "Serveur"
|
||||
@ -582,26 +584,26 @@ html:
|
||||
sessionMedian: "Session Médiane"
|
||||
sessionStart: "Début de la Session"
|
||||
sessions: "Sessions"
|
||||
sortBy: "Sort By"
|
||||
stacked: "Stacked"
|
||||
sortBy: "Trier par"
|
||||
stacked: "Empilés"
|
||||
table:
|
||||
showNofM: "Showing {{n}} of {{m}} entries"
|
||||
showPerPage: "Show per page"
|
||||
visibleColumns: "Visible columns"
|
||||
showNofM: "Afficher {{n}} de {{m}} entrées"
|
||||
showPerPage: "Afficher par page"
|
||||
visibleColumns: "Colonnes visibles"
|
||||
themeSelect: "Sélection du Thème"
|
||||
thirdDeadliestWeapon: "3ᵉ Arme de Combat"
|
||||
thirtyDays: "30 jours"
|
||||
thirtyDaysAgo: "Il y a 30 jours"
|
||||
time:
|
||||
date: "Date"
|
||||
day: "Day"
|
||||
days: "Days"
|
||||
hours: "Hours"
|
||||
month: "Month"
|
||||
months: "Months"
|
||||
week: "Week"
|
||||
weeks: "Weeks"
|
||||
year: "Year"
|
||||
day: "Jour"
|
||||
days: "Jours"
|
||||
hours: "Heures"
|
||||
month: "Mois"
|
||||
months: "Mois"
|
||||
week: "Semaine"
|
||||
weeks: "Seamines"
|
||||
year: "Année"
|
||||
timesKicked: "Nombre d'Éjections"
|
||||
toMainPage: "Retour à la page principale"
|
||||
total: "Total"
|
||||
@ -610,17 +612,17 @@ html:
|
||||
totalPlayers: "Joueurs Totaux"
|
||||
totalPlayersOld: "Joueurs Totaux"
|
||||
totalPlaytime: "Temps de Jeu Total"
|
||||
totalServerDowntime: "Total Server Downtime"
|
||||
totalServerDowntime: "Temps d'arrêt total du serveur"
|
||||
tps: "TPS"
|
||||
trend: "Tendances"
|
||||
trends30days: "Tendances sur 30 Jours"
|
||||
uninstalled: "Uninstalled"
|
||||
uninstalled: "Désinstallé"
|
||||
uniquePlayers: "Joueurs Uniques"
|
||||
uniquePlayers7days: "Unique Players (7 days)"
|
||||
uniquePlayers7days: "Joueurs uniques (7 jours)"
|
||||
unit:
|
||||
percentage: "Percentage"
|
||||
playerCount: "Player Count"
|
||||
users: "Manage Users"
|
||||
percentage: "Pourcentage"
|
||||
playerCount: "Nombre de joueurs"
|
||||
users: "Gérer les utilisateurs"
|
||||
version: "Version"
|
||||
veryActive: "Très Actif"
|
||||
weekComparison: "Comparaison Hebdomadaire"
|
||||
@ -645,90 +647,88 @@ html:
|
||||
manage:
|
||||
permission:
|
||||
description:
|
||||
access: "Controls access to pages"
|
||||
access_docs: "Allows accessing /docs page"
|
||||
access_errors: "Allows accessing /errors page"
|
||||
access_network: "Allows accessing /network page"
|
||||
access_player: "Allows accessing any /player pages"
|
||||
access_player_self: "Allows accessing own /player page"
|
||||
access_players: "Allows accessing /players page"
|
||||
access_query: "Allows accessing /query and Query results pages"
|
||||
access_raw_player_data: "Allows accessing /player/{uuid}/raw json data. Follows 'access.player' permissions."
|
||||
access_server: "Allows accessing all /server pages"
|
||||
manage_groups: "Allows modifying group permissions & Access to /manage/groups page"
|
||||
manage_users: "Allows modifying what users belong to what group"
|
||||
page: "Controls what is visible on pages"
|
||||
page_network: "See all of network page"
|
||||
page_network_geolocations: "See Geolocations tab"
|
||||
page_network_geolocations_map: "See Geolocations Map"
|
||||
page_network_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_network_join_addresses: "See Join Addresses -tab"
|
||||
page_network_join_addresses_graphs: "See Join Address graphs"
|
||||
page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_network_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_network_overview: "See Network Overview -tab"
|
||||
page_network_overview_graphs: "See Network Overview graphs"
|
||||
page_network_overview_graphs_calendar: "See Network calendar"
|
||||
page_network_overview_graphs_day_by_day: "See Day by Day graph"
|
||||
page_network_overview_graphs_hour_by_hour: "See Hour by Hour graph"
|
||||
page_network_overview_graphs_online: "See Players Online graph"
|
||||
page_network_overview_numbers: "See Network Overview numbers"
|
||||
page_network_performance: "See network Performance tab"
|
||||
page_network_playerbase: "See Playerbase Overview -tab"
|
||||
page_network_playerbase_graphs: "See Playerbase Overview graphs"
|
||||
page_network_playerbase_overview: "See Playerbase Overview numbers"
|
||||
page_network_players: "See Player list -tab"
|
||||
page_network_plugin_history: "See Plugin History across the network"
|
||||
page_network_plugins: "See Plugins tab of Proxy"
|
||||
page_network_retention: "See Player Retention -tab"
|
||||
page_network_server_list: "See list of servers"
|
||||
page_network_sessions: "See Sessions tab"
|
||||
page_network_sessions_list: "See list of sessions"
|
||||
page_network_sessions_overview: "See Session insights"
|
||||
page_network_sessions_server_pie: "See Server Pie graph"
|
||||
page_network_sessions_world_pie: "See World Pie graph"
|
||||
page_player: "See all of player page"
|
||||
page_player_overview: "See Player Overview -tab"
|
||||
page_player_plugins: "See Plugins -tabs"
|
||||
page_player_servers: "See Servers -tab"
|
||||
page_player_sessions: "See Player Sessions -tab"
|
||||
page_player_versus: "See PvP & PvE -tab"
|
||||
page_server: "See all of server page"
|
||||
page_server_allowlist_bounce: "See list of Game allowlist bounces"
|
||||
page_server_geolocations: "See Geolocations tab"
|
||||
page_server_geolocations_map: "See Geolocations Map"
|
||||
page_server_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_server_join_addresses: "See Join Addresses -tab"
|
||||
page_server_join_addresses_graphs: "See Join Address graphs"
|
||||
page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_server_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_server_online_activity: "See Online Activity -tab"
|
||||
page_server_online_activity_graphs: "See Online Activity graphs"
|
||||
page_server_online_activity_graphs_calendar: "See Server calendar"
|
||||
page_server_online_activity_graphs_day_by_day: "See Day by Day graph"
|
||||
page_server_online_activity_graphs_hour_by_hour: "See Hour by Hour graph"
|
||||
page_server_online_activity_graphs_punchcard: "See Punchcard graph"
|
||||
page_server_online_activity_overview: "See Online Activity numbers"
|
||||
page_server_overview: "See Server Overview -tab"
|
||||
page_server_overview_numbers: "See Server Overview numbers"
|
||||
page_server_overview_players_online_graph: "See Players Online graph"
|
||||
page_server_performance: "See Performance tab"
|
||||
page_server_performance_graphs: "See Performance graphs"
|
||||
page_server_performance_overview: "See Performance numbers"
|
||||
page_server_player_versus: "See PvP & PvE -tab"
|
||||
page_server_player_versus_kill_list: "See Player kill and death lists"
|
||||
page_server_player_versus_overview: "See PvP & PvE numbers"
|
||||
page_server_playerbase: "See Playerbase Overview -tab"
|
||||
page_server_playerbase_graphs: "See Playerbase Overview graphs"
|
||||
page_server_playerbase_overview: "See Playerbase Overview numbers"
|
||||
page_server_players: "See Player list -tab"
|
||||
page_server_plugin_history: "See Plugin History"
|
||||
page_server_plugins: "See Plugins -tabs of servers"
|
||||
page_server_retention: "See Player Retention -tab"
|
||||
page_server_sessions: "See Sessions tab"
|
||||
page_server_sessions_list: "See list of sessions"
|
||||
page_server_sessions_overview: "See Session insights"
|
||||
page_server_sessions_world_pie: "See World Pie graph"
|
||||
access: "Contrôle de l'accès aux pages"
|
||||
access_docs: "Permet d'accéder à la page /docs"
|
||||
access_errors: "Permet d'accéder à la page /erreurs"
|
||||
access_network: "Permet d'accéder à la page /réseau"
|
||||
access_player: "Permet d'accéder à toutes les pages /player"
|
||||
access_player_self: "Permet d'accéder à sa propre page /joueur"
|
||||
access_players: "Permet d'accéder à la page /joueurs"
|
||||
access_query: "Permet d'accéder aux pages /query et Query results"
|
||||
access_raw_player_data: "Permet d'accéder aux données json brutes de /player/{uuid}. Suit les permissions 'access.player'."
|
||||
access_server: "Permet d'accéder à toutes les pages /server"
|
||||
manage_groups: "Permet de modifier les permissions des groupes et d'accéder à la page /manage/groups"
|
||||
manage_users: "Permet de modifier quels utilisateurs appartiennent à quel groupe"
|
||||
page: "Contrôle ce qui est visible sur les pages"
|
||||
page_network: "Voir toute la page du réseau"
|
||||
page_network_geolocations: "Voir l'onglet Géolocalisations"
|
||||
page_network_geolocations_map: "Voir la carte des géolocalisations"
|
||||
page_network_geolocations_ping_per_country: "Voir le tableau Ping par pays"
|
||||
page_network_join_addresses: "Voir les adresses de jointure -tab"
|
||||
page_network_join_addresses_graphs: "Voir les graphiques des adresses de connexion"
|
||||
page_network_join_addresses_graphs_time: "Voir le graphique des adresses de connexion dans le temps"
|
||||
page_network_overview: "Voir Aperçu du réseau -tab"
|
||||
page_network_overview_graphs: "Voir les graphiques de l'aperçu du réseau"
|
||||
page_network_overview_graphs_calendar: "Voir le calendrier du réseau"
|
||||
page_network_overview_graphs_day_by_day: "Voir le graphique jour par jour"
|
||||
page_network_overview_graphs_hour_by_hour: "Voir le graphique heure par heure"
|
||||
page_network_overview_graphs_online: "Voir le graphique des joueurs en ligne"
|
||||
page_network_overview_numbers: "Voir les numéros de l'aperçu du réseau"
|
||||
page_network_performance: "Voir l'onglet Performances du réseau"
|
||||
page_network_playerbase: "Voir l'onglet Aperçu de la base de joueurs"
|
||||
page_network_playerbase_graphs: "Voir les graphiques de la base de joueurs"
|
||||
page_network_playerbase_overview: "Voir les chiffres de l'aperçu de la base de joueurs"
|
||||
page_network_players: "Voir la liste des lecteurs -tab"
|
||||
page_network_plugin_history: "Voir l'historique des plugins sur le réseau"
|
||||
page_network_plugins: "Voir l'onglet Plugins de Proxy"
|
||||
page_network_retention: "Voir l'onglet Rétention des joueurs"
|
||||
page_network_server_list: "Voir la liste des serveurs"
|
||||
page_network_sessions: "Voir l'onglet Sessions"
|
||||
page_network_sessions_list: "Voir la liste des sessions"
|
||||
page_network_sessions_overview: "Voir les perspectives de la session"
|
||||
page_network_sessions_server_pie: "Voir le graphique à secteurs du serveur"
|
||||
page_network_sessions_world_pie: "Voir le graphique de la carte du monde"
|
||||
page_player: "Voir toute la page du joueur"
|
||||
page_player_overview: "Voir l'aperçu des joueurs -tab"
|
||||
page_player_plugins: "Voir les plugins -tabs"
|
||||
page_player_servers: "Voir les serveurs -tab"
|
||||
page_player_sessions: "Voir les sessions des joueurs -tab"
|
||||
page_player_versus: "Voir PvP & PvE -tab"
|
||||
page_server: "Voir toute la page du serveur"
|
||||
page_server_allowlist_bounce: "Voir la liste des rebonds de Game allowlist"
|
||||
page_server_geolocations: "Voir l'onglet Géolocalisations"
|
||||
page_server_geolocations_map: "Voir la carte des géolocalisations"
|
||||
page_server_geolocations_ping_per_country: "Voir le tableau Ping par pays"
|
||||
page_server_join_addresses: "Voir les adresses de jointure -tab"
|
||||
page_server_join_addresses_graphs: "Voir les graphiques des adresses de connexion"
|
||||
page_server_join_addresses_graphs_time: "Voir le graphique des adresses de connexion dans le temps"
|
||||
page_server_online_activity: "Voir l'activité en ligne -tab"
|
||||
page_server_online_activity_graphs: "Voir les graphiques de l'activité en ligne"
|
||||
page_server_online_activity_graphs_calendar: "Voir Calendrier des serveurs"
|
||||
page_server_online_activity_graphs_day_by_day: "Voir le graphique jour par jour"
|
||||
page_server_online_activity_graphs_hour_by_hour: "Voir le graphique heure par heure"
|
||||
page_server_online_activity_graphs_punchcard: "Voir graphique Punchcard"
|
||||
page_server_online_activity_overview: "Voir les numéros d'activité en ligne"
|
||||
page_server_overview: "Voir l'aperçu du serveur -tab"
|
||||
page_server_overview_numbers: "Voir les numéros de la vue d'ensemble du serveur"
|
||||
page_server_overview_players_online_graph: "Voir le graphique des joueurs en ligne"
|
||||
page_server_performance: "Voir l'onglet Performance"
|
||||
page_server_performance_graphs: "Voir les graphiques de performance"
|
||||
page_server_performance_overview: "Voir les chiffres de performance"
|
||||
page_server_player_versus: "Voir PvP & PvE -tab"
|
||||
page_server_player_versus_kill_list: "Voir Liste des joueurs tués ou morts"
|
||||
page_server_player_versus_overview: "Voir les chiffres de PvP & PvE"
|
||||
page_server_playerbase: "Voir l'aperçu de la base de joueurs -tab"
|
||||
page_server_playerbase_graphs: "Voir les graphiques de la base de joueurs"
|
||||
page_server_playerbase_overview: "Voir les chiffres de l'aperçu de la base de joueurs"
|
||||
page_server_players: "Voir la liste des joueurs -tab"
|
||||
page_server_plugin_history: "Voir l'historique du plugin"
|
||||
page_server_plugins: "Voir les onglets Plugins des serveurs"
|
||||
page_server_retention: "Voir l'onglet Rétention des joueurs"
|
||||
page_server_sessions: "Voir l'onglet Sessions"
|
||||
page_server_sessions_list: "Voir la liste des sessions"
|
||||
page_server_sessions_overview: "Voir les perspectives de la session"
|
||||
page_server_sessions_world_pie: "Voir le graphique de la carte du monde"
|
||||
modal:
|
||||
info:
|
||||
bugs: "Rapport de bugs"
|
||||
@ -759,17 +759,17 @@ html:
|
||||
name: "Statut de Bannissement"
|
||||
banned: "Banni(e)"
|
||||
country:
|
||||
text: "have joined from country"
|
||||
text: "a rejoint à partir du pays"
|
||||
generic:
|
||||
allPlayers: "Tous les Joueurs"
|
||||
and: "et"
|
||||
start: "des Joueurs qui"
|
||||
hasPlayedOnServers:
|
||||
name: "Has played on one of servers"
|
||||
text: "have played on at least one of"
|
||||
name: "A joué sur l'un des serveurs"
|
||||
text: "A joué sur au moins un de"
|
||||
hasPluginBooleanValue:
|
||||
name: "Has plugin boolean value"
|
||||
text: "have Plugin boolean value"
|
||||
name: "A la valeur booléenne du plugin"
|
||||
text: "avoir une valeur booléenne de plugin"
|
||||
joinAddress:
|
||||
text: "ont rejoint avec l'adresse"
|
||||
nonOperators: "Non Opérateur(trice)"
|
||||
@ -784,24 +784,25 @@ html:
|
||||
text: "sont dans le groupe {group} de ${plugin}"
|
||||
registeredBetween:
|
||||
text: "Enregistrés entre"
|
||||
skipped: "Skipped"
|
||||
skipped: "Sautée"
|
||||
title:
|
||||
activityGroup: "Groupe d'Activité actuel"
|
||||
view: " Vue :"
|
||||
filters:
|
||||
add: "Ajouter un filtre.."
|
||||
loading: "Chargement des Filtres.."
|
||||
add: "Ajouter un filtre."
|
||||
loading: "Chargement des Filtres."
|
||||
generic:
|
||||
are: "`sont`"
|
||||
label:
|
||||
editQuery: "Modifier la requête"
|
||||
from: ">de</label>"
|
||||
makeAnother: "Faire une autre Requête"
|
||||
servers:
|
||||
all: "using data of all servers"
|
||||
many: "using data of {number} servers"
|
||||
single: "using data of 1 server"
|
||||
two: "using data of 2 servers"
|
||||
showFullQuery: "Show Full Query"
|
||||
all: "en utilisant les données de tous les serveurs"
|
||||
many: "en utilisant les données de {nombre} serveurs"
|
||||
single: "en utilisant les données d'un serveur"
|
||||
two: "en utilisant les données de 2 serveurs"
|
||||
showFullQuery: "Afficher la requête complète"
|
||||
to: ">à</label>"
|
||||
view: "Visualiser une vue"
|
||||
performQuery: "Exécuter la Requête !"
|
||||
@ -821,7 +822,7 @@ html:
|
||||
completion3: "Utilisez la commande suivante en jeu pour terminer l'enregistrement :"
|
||||
completion4: "Ou en utilisant la console :"
|
||||
createNewUser: "Créer un nouvel utilisateur"
|
||||
disabled: "Registering new users has been disabled in the config."
|
||||
disabled: "L'enregistrement de nouveaux utilisateurs a été désactivé dans la configuration."
|
||||
error:
|
||||
checkFailed: "La vérification de l'état de l'enregistrement a échoué : "
|
||||
failed: "Enregistrement échoué : "
|
||||
@ -831,11 +832,11 @@ html:
|
||||
login: "Vous avez déjà un compte ? Connectez-vous !"
|
||||
passwordTip: "Le Mot de Passe devrait comporter plus de 8 caractères, mais il n'y a aucune limite."
|
||||
register: "Enregistrer"
|
||||
success: "Registered a new user successfully! You can now login."
|
||||
success: "Enregistrement réussi d'un nouvel utilisateur ! Vous pouvez maintenant vous connecter."
|
||||
usernameTip: "Le Nom d'Utilisateur peut comporter jusqu'à 50 caractères."
|
||||
text:
|
||||
click: "Click for more"
|
||||
clickAndDrag: "Click and Drag for more"
|
||||
click: "Cliquez ici pour en savoir plus"
|
||||
clickAndDrag: "Cliquer et faire glisser pour en savoir plus"
|
||||
clickToExpand: "Cliquez pour agrandir"
|
||||
comparing15days: "Comparaison des 15 derniers Jours"
|
||||
comparing30daysAgo: "Comparaison des 60 derniers Jours"
|
||||
@ -866,11 +867,11 @@ plugin:
|
||||
database: "Traitement des tâches critiques inachevées... (${0})"
|
||||
disabled: "Plan a été désactivé."
|
||||
processingComplete: "Traitement complété."
|
||||
savingSessions: "Sauvegarde des sessions inachevées..."
|
||||
savingSessions: "Sauvegarde des sessions inachevées."
|
||||
savingSessionsTimeout: "Timeout atteint - stockage des sessions non terminées lors du prochain démarrage."
|
||||
waitingDb: "En attente de la finalisation des requêtes pour éviter que SQLite ne plante la JVM.."
|
||||
waitingDb: "En attente de la finalisation des requêtes pour éviter que SQLite ne plante la JVM."
|
||||
waitingDbComplete: "Connexion SQLite fermée."
|
||||
waitingTransactions: "En attente des transactions non terminées pour éviter la perte de données.."
|
||||
waitingTransactions: "En attente des transactions non terminées pour éviter la perte de données."
|
||||
waitingTransactionsComplete: "File d'attente des transactions fermée."
|
||||
webserver: "Le serveur Web a été désactivé."
|
||||
enable:
|
||||
@ -887,9 +888,9 @@ plugin:
|
||||
emptyIP: "L'adresse IP située dans le fichier 'server.properties' est vide et l'option 'Alternative_IP' n'est pas utilisée. Attention, des liens incorrects seront donnés !"
|
||||
geoDisabled: "La Géolocalisation n'est pas active. (Data.Geolocations: false)"
|
||||
geoInternetRequired: "Plan nécessite un accès à Internet lors de sa première utilisation pour télécharger la base de données 'GeoLite2 Geolocation'."
|
||||
proxyAddress: "Proxy server detected in the database - Proxy Webserver address is '${0}'."
|
||||
proxyDisabledWebserver: "Disabling Webserver on this server - You can override this behavior by setting '${0}' to false."
|
||||
settingChange: "Note: Set '${0}' to ${1}"
|
||||
proxyAddress: "Serveur proxy détecté dans la base de données - L'adresse du serveur web proxy est '${0}'."
|
||||
proxyDisabledWebserver: "Désactivation du serveur web sur ce serveur - Vous pouvez ignorer ce comportement en réglant '${0}' sur false."
|
||||
settingChange: "Note : Réglez '${0}' sur ${1}."
|
||||
storeSessions: "Stockage des sessions ayant été préservées lors de l'arrêt précédent."
|
||||
webserverDisabled: "Le serveur Web n'a pas été initialisé. (WebServer.DisableWebServer: true)"
|
||||
webserver: "Le serveur Web communique à travers le port ${0} ( ${1} )."
|
||||
@ -900,16 +901,16 @@ plugin:
|
||||
dbNotifySQLiteWAL: "Le mode WAL de SQLite n'est pas pris en charge sur cette version du serveur, en utilisant le mode par défaut. Cela peut possiblement affecter les performances."
|
||||
dbPatchesAlreadyApplied: "Tous les correctifs pour la base de données ont déjà été appliqués."
|
||||
dbPatchesApplied: "Tous les correctifs pour la base de données ont été appliqués avec succès."
|
||||
dbSchemaPatch: "Database: Making sure schema is up to date.."
|
||||
loadedServerInfo: "Server identifier loaded: ${0}"
|
||||
loadingServerInfo: "Loading server identifying information"
|
||||
dbSchemaPatch: "Base de données : S'assurer que le schéma est à jour."
|
||||
loadedServerInfo: "Identifiant de serveur chargé : ${0}"
|
||||
loadingServerInfo: "Chargement des informations d'identification du serveur"
|
||||
no: "Non"
|
||||
today: "'Aujourd''hui'"
|
||||
unavailable: "Indisponible"
|
||||
unknown: "Inconnu"
|
||||
yes: "Oui"
|
||||
yesterday: "'Hier'"
|
||||
localeReloaded: "Custom locale.yml was modified so it was reloaded and is now in use."
|
||||
localeReloaded: "Le fichier locale.yml personnalisé a été modifié, il a donc été rechargé et est maintenant utilisé."
|
||||
version:
|
||||
checkFail: "Impossible de vérifier le dernier numéro de la version"
|
||||
checkFailGithub: "Les informations de la version n'ont pas pu être chargées depuis Github/versions.txt"
|
||||
@ -926,10 +927,10 @@ plugin:
|
||||
notify:
|
||||
authDisabledConfig: "Serveur Web : Authentification d'utilisateur désactivée ! (dans la configuration)"
|
||||
authDisabledNoHTTPS: "Serveur Web : Authentification utilisateur désactivée ! (Non sécurisée avec HTTP)"
|
||||
certificateExpiresOn: "Webserver: Loaded certificate is valid until ${0}."
|
||||
certificateExpiresPassed: "Webserver: Certificate has expired, consider renewing the certificate."
|
||||
certificateExpiresSoon: "Webserver: Certificate expires in ${0}, consider renewing the certificate."
|
||||
certificateNoSuchAlias: "Webserver: Certificate with alias '${0}' was not found inside the keystore file '${1}'."
|
||||
certificateExpiresOn: "Webserver: Le certificat chargé est valable jusqu'à ${0}."
|
||||
certificateExpiresPassed: "Webserver: Le certificat a expiré, pensez à renouveler le certificat."
|
||||
certificateExpiresSoon: "Webserver: Le certificat expire dans ${0}, pensez à renouveler le certificat."
|
||||
certificateNoSuchAlias: "Webserver: Le certificat avec l'alias '${0}' n'a pas été trouvé dans le fichier keystore '${1}'."
|
||||
http: "Serveur Web : Aucun certificat -> Utilisation du serveur HTTP pour la visualisation."
|
||||
ipWhitelist: "Serveur Web : La liste blanche d'adresses IP n'est pas activée."
|
||||
ipWhitelistBlock: "Serveur Web : ${0} n'a pas pu accéder à '${1}'. (pas sur la liste blanche)"
|
||||
|
@ -290,6 +290,8 @@ html:
|
||||
active: "Attivo"
|
||||
activePlaytime: "Active Playtime"
|
||||
activityIndex: "Indice Inattività"
|
||||
addJoinAddressGroup: "Add address group"
|
||||
addressGroup: "Address group {{n}}"
|
||||
afk: "AFK"
|
||||
afkTime: "Tempo AFK"
|
||||
all: "Tutto"
|
||||
@ -664,7 +666,6 @@ html:
|
||||
page_network_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_network_join_addresses: "See Join Addresses -tab"
|
||||
page_network_join_addresses_graphs: "See Join Address graphs"
|
||||
page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_network_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_network_overview: "See Network Overview -tab"
|
||||
page_network_overview_graphs: "See Network Overview graphs"
|
||||
@ -700,7 +701,6 @@ html:
|
||||
page_server_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_server_join_addresses: "See Join Addresses -tab"
|
||||
page_server_join_addresses_graphs: "See Join Address graphs"
|
||||
page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_server_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_server_online_activity: "See Online Activity -tab"
|
||||
page_server_online_activity_graphs: "See Online Activity graphs"
|
||||
@ -794,6 +794,7 @@ html:
|
||||
generic:
|
||||
are: "`are`"
|
||||
label:
|
||||
editQuery: "Edit Query"
|
||||
from: ">from</label>"
|
||||
makeAnother: "Make another query"
|
||||
servers:
|
||||
|
@ -290,6 +290,8 @@ html:
|
||||
active: "よくログインしている"
|
||||
activePlaytime: "アクティブなプレイ時間"
|
||||
activityIndex: "活動指数"
|
||||
addJoinAddressGroup: "Add address group"
|
||||
addressGroup: "Address group {{n}}"
|
||||
afk: "離席"
|
||||
afkTime: "離席時間"
|
||||
all: "全て"
|
||||
@ -664,7 +666,6 @@ html:
|
||||
page_network_geolocations_ping_per_country: "国ごとのPing表を表示"
|
||||
page_network_join_addresses: "参加アドレスタブを表示"
|
||||
page_network_join_addresses_graphs: "参加アドレスのグラフを表示"
|
||||
page_network_join_addresses_graphs_pie: "最後に参加したアドレスのグラフを表示"
|
||||
page_network_join_addresses_graphs_time: "参加アドレスの経時変化のグラフを表示"
|
||||
page_network_overview: "ネットワークの概要タブを表示"
|
||||
page_network_overview_graphs: "ネットワークの概要グラフを表示"
|
||||
@ -700,7 +701,6 @@ html:
|
||||
page_server_geolocations_ping_per_country: "国ごとのPing表を表示"
|
||||
page_server_join_addresses: "参加アドレスタブを表示"
|
||||
page_server_join_addresses_graphs: "参加アドレスグラフを表示"
|
||||
page_server_join_addresses_graphs_pie: "最後に参加したアドレスのグラフを表示"
|
||||
page_server_join_addresses_graphs_time: "参加アドレスの経時変化のグラフを表示"
|
||||
page_server_online_activity: "オンラインアクティビティタブを表示"
|
||||
page_server_online_activity_graphs: "オンラインアクティビティグラフを表示"
|
||||
@ -794,6 +794,7 @@ html:
|
||||
generic:
|
||||
are: "`それは`"
|
||||
label:
|
||||
editQuery: "Edit Query"
|
||||
from: ">から</label>"
|
||||
makeAnother: "別のクエリを作る"
|
||||
servers:
|
||||
|
@ -290,6 +290,8 @@ html:
|
||||
active: "활동적인"
|
||||
activePlaytime: "Active Playtime"
|
||||
activityIndex: "활동 색인"
|
||||
addJoinAddressGroup: "Add address group"
|
||||
addressGroup: "Address group {{n}}"
|
||||
afk: "AFK"
|
||||
afkTime: "AFK 시간"
|
||||
all: "모두"
|
||||
@ -664,7 +666,6 @@ html:
|
||||
page_network_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_network_join_addresses: "See Join Addresses -tab"
|
||||
page_network_join_addresses_graphs: "See Join Address graphs"
|
||||
page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_network_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_network_overview: "See Network Overview -tab"
|
||||
page_network_overview_graphs: "See Network Overview graphs"
|
||||
@ -700,7 +701,6 @@ html:
|
||||
page_server_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_server_join_addresses: "See Join Addresses -tab"
|
||||
page_server_join_addresses_graphs: "See Join Address graphs"
|
||||
page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_server_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_server_online_activity: "See Online Activity -tab"
|
||||
page_server_online_activity_graphs: "See Online Activity graphs"
|
||||
@ -794,6 +794,7 @@ html:
|
||||
generic:
|
||||
are: "`are`"
|
||||
label:
|
||||
editQuery: "Edit Query"
|
||||
from: ">from</label>"
|
||||
makeAnother: "Make another query"
|
||||
servers:
|
||||
|
@ -290,6 +290,8 @@ html:
|
||||
active: "Actief"
|
||||
activePlaytime: "Actieve Speeltijd"
|
||||
activityIndex: "Activiteitsindex"
|
||||
addJoinAddressGroup: "Add address group"
|
||||
addressGroup: "Address group {{n}}"
|
||||
afk: "AFK"
|
||||
afkTime: "AFK Tijd"
|
||||
all: "Alle"
|
||||
@ -664,7 +666,6 @@ html:
|
||||
page_network_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_network_join_addresses: "See Join Addresses -tab"
|
||||
page_network_join_addresses_graphs: "See Join Address graphs"
|
||||
page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_network_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_network_overview: "See Network Overview -tab"
|
||||
page_network_overview_graphs: "See Network Overview graphs"
|
||||
@ -700,7 +701,6 @@ html:
|
||||
page_server_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_server_join_addresses: "See Join Addresses -tab"
|
||||
page_server_join_addresses_graphs: "See Join Address graphs"
|
||||
page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_server_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_server_online_activity: "See Online Activity -tab"
|
||||
page_server_online_activity_graphs: "See Online Activity graphs"
|
||||
@ -794,6 +794,7 @@ html:
|
||||
generic:
|
||||
are: "`zijn`"
|
||||
label:
|
||||
editQuery: "Edit Query"
|
||||
from: ">van</label>"
|
||||
makeAnother: "Maak nog een query"
|
||||
servers:
|
||||
|
@ -290,6 +290,8 @@ html:
|
||||
active: "Ativo"
|
||||
activePlaytime: "Active Playtime"
|
||||
activityIndex: "Índice de Atividade"
|
||||
addJoinAddressGroup: "Add address group"
|
||||
addressGroup: "Address group {{n}}"
|
||||
afk: "AFK"
|
||||
afkTime: "AFK Time"
|
||||
all: "Todos"
|
||||
@ -664,7 +666,6 @@ html:
|
||||
page_network_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_network_join_addresses: "See Join Addresses -tab"
|
||||
page_network_join_addresses_graphs: "See Join Address graphs"
|
||||
page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_network_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_network_overview: "See Network Overview -tab"
|
||||
page_network_overview_graphs: "See Network Overview graphs"
|
||||
@ -700,7 +701,6 @@ html:
|
||||
page_server_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_server_join_addresses: "See Join Addresses -tab"
|
||||
page_server_join_addresses_graphs: "See Join Address graphs"
|
||||
page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_server_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_server_online_activity: "See Online Activity -tab"
|
||||
page_server_online_activity_graphs: "See Online Activity graphs"
|
||||
@ -794,6 +794,7 @@ html:
|
||||
generic:
|
||||
are: "`are`"
|
||||
label:
|
||||
editQuery: "Edit Query"
|
||||
from: ">from</label>"
|
||||
makeAnother: "Make another query"
|
||||
servers:
|
||||
|
@ -290,6 +290,8 @@ html:
|
||||
active: "Активный"
|
||||
activePlaytime: "Активное время игры"
|
||||
activityIndex: "Индекс активности"
|
||||
addJoinAddressGroup: "Add address group"
|
||||
addressGroup: "Address group {{n}}"
|
||||
afk: "AFK"
|
||||
afkTime: "Время AFK"
|
||||
all: "Все"
|
||||
@ -664,7 +666,6 @@ html:
|
||||
page_network_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_network_join_addresses: "See Join Addresses -tab"
|
||||
page_network_join_addresses_graphs: "See Join Address graphs"
|
||||
page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_network_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_network_overview: "See Network Overview -tab"
|
||||
page_network_overview_graphs: "See Network Overview graphs"
|
||||
@ -700,7 +701,6 @@ html:
|
||||
page_server_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_server_join_addresses: "See Join Addresses -tab"
|
||||
page_server_join_addresses_graphs: "See Join Address graphs"
|
||||
page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_server_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_server_online_activity: "See Online Activity -tab"
|
||||
page_server_online_activity_graphs: "See Online Activity graphs"
|
||||
@ -794,6 +794,7 @@ html:
|
||||
generic:
|
||||
are: "``"
|
||||
label:
|
||||
editQuery: "Edit Query"
|
||||
from: ">с</label>"
|
||||
makeAnother: "Сделать другой запрос"
|
||||
servers:
|
||||
|
@ -290,6 +290,8 @@ html:
|
||||
active: "Aktivite"
|
||||
activePlaytime: "Aktif Oyun Süresi"
|
||||
activityIndex: "Aktivite göstergesi"
|
||||
addJoinAddressGroup: "Add address group"
|
||||
addressGroup: "Address group {{n}}"
|
||||
afk: "AFK"
|
||||
afkTime: "AFK Süresi"
|
||||
all: "Tamamı"
|
||||
@ -664,7 +666,6 @@ html:
|
||||
page_network_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_network_join_addresses: "See Join Addresses -tab"
|
||||
page_network_join_addresses_graphs: "See Join Address graphs"
|
||||
page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_network_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_network_overview: "See Network Overview -tab"
|
||||
page_network_overview_graphs: "See Network Overview graphs"
|
||||
@ -700,7 +701,6 @@ html:
|
||||
page_server_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_server_join_addresses: "See Join Addresses -tab"
|
||||
page_server_join_addresses_graphs: "See Join Address graphs"
|
||||
page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_server_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_server_online_activity: "See Online Activity -tab"
|
||||
page_server_online_activity_graphs: "See Online Activity graphs"
|
||||
@ -794,6 +794,7 @@ html:
|
||||
generic:
|
||||
are: "`vardır`"
|
||||
label:
|
||||
editQuery: "Edit Query"
|
||||
from: ">dan</label>"
|
||||
makeAnother: "Başka bir sorgu yap"
|
||||
servers:
|
||||
|
@ -290,6 +290,8 @@ html:
|
||||
active: "Активний"
|
||||
activePlaytime: "Активний час гри"
|
||||
activityIndex: "Індекс активності"
|
||||
addJoinAddressGroup: "Add address group"
|
||||
addressGroup: "Address group {{n}}"
|
||||
afk: "AFK"
|
||||
afkTime: "Час AFK"
|
||||
all: "Всі"
|
||||
@ -664,7 +666,6 @@ html:
|
||||
page_network_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_network_join_addresses: "See Join Addresses -tab"
|
||||
page_network_join_addresses_graphs: "See Join Address graphs"
|
||||
page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_network_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_network_overview: "See Network Overview -tab"
|
||||
page_network_overview_graphs: "See Network Overview graphs"
|
||||
@ -700,7 +701,6 @@ html:
|
||||
page_server_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_server_join_addresses: "See Join Addresses -tab"
|
||||
page_server_join_addresses_graphs: "See Join Address graphs"
|
||||
page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_server_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_server_online_activity: "See Online Activity -tab"
|
||||
page_server_online_activity_graphs: "See Online Activity graphs"
|
||||
@ -794,6 +794,7 @@ html:
|
||||
generic:
|
||||
are: "``"
|
||||
label:
|
||||
editQuery: "Edit Query"
|
||||
from: ">з</label>"
|
||||
makeAnother: "Зробити інший запит"
|
||||
servers:
|
||||
|
@ -290,6 +290,8 @@ html:
|
||||
active: "活躍"
|
||||
activePlaytime: "活躍時間"
|
||||
activityIndex: "活躍指數"
|
||||
addJoinAddressGroup: "Add address group"
|
||||
addressGroup: "Address group {{n}}"
|
||||
afk: "掛機"
|
||||
afkTime: "掛機時間"
|
||||
all: "全部"
|
||||
@ -664,7 +666,6 @@ html:
|
||||
page_network_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_network_join_addresses: "See Join Addresses -tab"
|
||||
page_network_join_addresses_graphs: "See Join Address graphs"
|
||||
page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_network_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_network_overview: "See Network Overview -tab"
|
||||
page_network_overview_graphs: "See Network Overview graphs"
|
||||
@ -700,7 +701,6 @@ html:
|
||||
page_server_geolocations_ping_per_country: "See Ping Per Country table"
|
||||
page_server_join_addresses: "See Join Addresses -tab"
|
||||
page_server_join_addresses_graphs: "See Join Address graphs"
|
||||
page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph"
|
||||
page_server_join_addresses_graphs_time: "See Join Addresses over time graph"
|
||||
page_server_online_activity: "See Online Activity -tab"
|
||||
page_server_online_activity_graphs: "See Online Activity graphs"
|
||||
@ -794,6 +794,7 @@ html:
|
||||
generic:
|
||||
are: "`是`"
|
||||
label:
|
||||
editQuery: "Edit Query"
|
||||
from: ">從 </label>"
|
||||
makeAnother: "進行另一個查詢"
|
||||
servers:
|
||||
|
@ -3,6 +3,7 @@ author: AuroraLS3
|
||||
main: com.djrapitops.plan.Plan
|
||||
version: @version@
|
||||
api-version: 1.13
|
||||
folia-supported: true
|
||||
softdepend:
|
||||
- AAC
|
||||
- ASkyBlock
|
||||
|
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* This file is part of Player Analytics (Plan).
|
||||
*
|
||||
* Plan is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License v3 as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Plan 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.djrapitops.plan.delivery.domain.auth;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebPermission}.
|
||||
*
|
||||
* @author AuroraLS3
|
||||
*/
|
||||
class WebPermissionTest {
|
||||
|
||||
@Test
|
||||
void webPermissionIsFound() {
|
||||
String permission = "access.player.self";
|
||||
WebPermission found = WebPermission.findByPermission(permission).orElseThrow(AssertionError::new);
|
||||
WebPermission expected = WebPermission.ACCESS_PLAYER_SELF;
|
||||
assertEquals(expected, found);
|
||||
}
|
||||
|
||||
@Test
|
||||
void webPermissionIsDetectedAsDeprecated() {
|
||||
String permission = "page.server.join.addresses.graphs.pie";
|
||||
assertTrue(WebPermission.isDeprecated(permission));
|
||||
}
|
||||
|
||||
@Test
|
||||
void webPermissionIsDetectedAsNonDeprecated() {
|
||||
String permission = "access.player.self";
|
||||
assertFalse(WebPermission.isDeprecated(permission));
|
||||
}
|
||||
|
||||
@Test
|
||||
void customWebPermissionIsDetectedAsNonDeprecated() {
|
||||
String permission = "custom.permission";
|
||||
assertFalse(WebPermission.isDeprecated(permission));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* This file is part of Player Analytics (Plan).
|
||||
*
|
||||
* Plan is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License v3 as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Plan 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.djrapitops.plan.delivery.formatting;
|
||||
|
||||
import com.djrapitops.plan.delivery.formatting.time.SecondFormatter;
|
||||
import com.djrapitops.plan.delivery.formatting.time.YearFormatter;
|
||||
import com.djrapitops.plan.settings.config.PlanConfig;
|
||||
import com.djrapitops.plan.settings.config.paths.FormatSettings;
|
||||
import com.djrapitops.plan.settings.config.paths.PluginSettings;
|
||||
import com.djrapitops.plan.settings.locale.LangCode;
|
||||
import com.djrapitops.plan.settings.locale.Locale;
|
||||
import com.djrapitops.plan.settings.locale.LocaleSystem;
|
||||
import extension.FullSystemExtension;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.DynamicTest;
|
||||
import org.junit.jupiter.api.TestFactory;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
|
||||
/**
|
||||
* Tests against formatting issues.
|
||||
*
|
||||
* @author AuroraLS3
|
||||
*/
|
||||
@ExtendWith(FullSystemExtension.class)
|
||||
class FormattersTest {
|
||||
|
||||
@TestFactory
|
||||
@DisplayName("Locale for recent day names does not cause formatting issues.")
|
||||
Stream<DynamicTest> localeFormatRegressionTest(PlanConfig config, LocaleSystem localeSystem) {
|
||||
config.set(FormatSettings.DATE_RECENT_DAYS, true);
|
||||
config.set(FormatSettings.DATE_FULL, "MMM d YYYY, HH:mm:ss");
|
||||
config.set(FormatSettings.DATE_NO_SECONDS, "MMM d YYYY, HH:mm");
|
||||
config.set(FormatSettings.DATE_RECENT_DAYS_PATTERN, "MMM d YYYY");
|
||||
return Arrays.stream(LangCode.values())
|
||||
.filter(langCode -> langCode != LangCode.CUSTOM)
|
||||
.map(langCode ->
|
||||
DynamicTest.dynamicTest("Lang " + langCode.name() + " does not cause formatting issues with plugin.generic.today or plugin.generic.yesterday lang keys.", () -> {
|
||||
config.set(PluginSettings.LOCALE, langCode.name());
|
||||
Locale locale = localeSystem.loadSettingLocale().orElseThrow(() -> new AssertionError("Could not load " + langCode + "locale"));
|
||||
YearFormatter yearFormatter = new YearFormatter(config, locale);
|
||||
SecondFormatter secondFormatter = new SecondFormatter(config, locale);
|
||||
long today = System.currentTimeMillis();
|
||||
assertDoesNotThrow(() -> yearFormatter.apply(today), "plugin.generic.today is causing Formatting issues for lang " + langCode);
|
||||
assertDoesNotThrow(() -> secondFormatter.apply(today), "plugin.generic.today is causing Formatting issues for lang " + langCode);
|
||||
long yesterday = today - TimeUnit.DAYS.toMillis(1L) - 1000L;
|
||||
assertDoesNotThrow(() -> yearFormatter.apply(yesterday), "plugin.generic.yesterday is causing Formatting issues for lang " + langCode);
|
||||
assertDoesNotThrow(() -> secondFormatter.apply(yesterday), "plugin.generic.yesterday is causing Formatting issues for lang " + langCode);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@ -95,7 +95,6 @@ class AccessControlTest {
|
||||
Arguments.of("/v1/graph?type=aggregatedPing&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_PERFORMANCE_GRAPHS, 200, 403),
|
||||
Arguments.of("/v1/graph?type=worldPie&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_SESSIONS_WORLD_PIE, 200, 403),
|
||||
Arguments.of("/v1/graph?type=activity&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_PLAYERBASE_GRAPHS, 200, 403),
|
||||
Arguments.of("/v1/graph?type=joinAddressPie&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_PIE, 200, 403),
|
||||
Arguments.of("/v1/graph?type=geolocation&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_GEOLOCATIONS_MAP, 200, 403),
|
||||
Arguments.of("/v1/graph?type=uniqueAndNew&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_DAY_BY_DAY, 200, 403),
|
||||
Arguments.of("/v1/graph?type=hourlyUniqueAndNew&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_HOUR_BY_HOUR, 200, 403),
|
||||
@ -107,7 +106,10 @@ class AccessControlTest {
|
||||
Arguments.of("/v1/pingTable?server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_GEOLOCATIONS_PING_PER_COUNTRY, 200, 403),
|
||||
Arguments.of("/v1/sessions?server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_SESSIONS_LIST, 200, 403),
|
||||
Arguments.of("/v1/retention?server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_RETENTION, 200, 403),
|
||||
Arguments.of("/v1/joinAddresses", WebPermission.PAGE_NETWORK_RETENTION, 200, 403),
|
||||
Arguments.of("/v1/joinAddresses?listOnly=true", WebPermission.PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_TIME, 200, 403),
|
||||
Arguments.of("/v1/joinAddresses?server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_RETENTION, 200, 403),
|
||||
Arguments.of("/v1/joinAddresses?server=" + TestConstants.SERVER_UUID_STRING + "&listOnly=true", WebPermission.PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_TIME, 200, 403),
|
||||
Arguments.of("/network", WebPermission.ACCESS_NETWORK, 302, 403),
|
||||
Arguments.of("/v1/network/overview", WebPermission.PAGE_NETWORK_OVERVIEW_NUMBERS, 200, 403),
|
||||
Arguments.of("/v1/network/servers", WebPermission.PAGE_NETWORK_SERVER_LIST, 200, 403),
|
||||
@ -119,7 +121,6 @@ class AccessControlTest {
|
||||
Arguments.of("/v1/graph?type=hourlyUniqueAndNew", WebPermission.PAGE_NETWORK_OVERVIEW_GRAPHS_HOUR_BY_HOUR, 200, 403),
|
||||
Arguments.of("/v1/graph?type=serverCalendar", WebPermission.PAGE_NETWORK_OVERVIEW_GRAPHS_CALENDAR, 200, 403),
|
||||
Arguments.of("/v1/graph?type=serverPie", WebPermission.PAGE_NETWORK_SESSIONS_SERVER_PIE, 200, 403),
|
||||
Arguments.of("/v1/graph?type=joinAddressPie", WebPermission.PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_PIE, 200, 403),
|
||||
Arguments.of("/v1/graph?type=activity", WebPermission.PAGE_NETWORK_PLAYERBASE_GRAPHS, 200, 403),
|
||||
Arguments.of("/v1/graph?type=geolocation", WebPermission.PAGE_NETWORK_GEOLOCATIONS_MAP, 200, 403),
|
||||
Arguments.of("/v1/network/pingTable", WebPermission.PAGE_NETWORK_GEOLOCATIONS_PING_PER_COUNTRY, 200, 403),
|
||||
|
@ -30,6 +30,7 @@ import com.djrapitops.plan.settings.config.paths.WebserverSettings;
|
||||
import com.djrapitops.plan.storage.database.Database;
|
||||
import com.djrapitops.plan.storage.database.queries.objects.ServerQueries;
|
||||
import com.djrapitops.plan.storage.database.transactions.StoreServerInformationTransaction;
|
||||
import com.djrapitops.plan.storage.database.transactions.commands.RemoveWebGroupsTransaction;
|
||||
import com.djrapitops.plan.storage.database.transactions.commands.StoreWebUserTransaction;
|
||||
import com.djrapitops.plan.storage.database.transactions.events.StoreServerPlayerTransaction;
|
||||
import com.djrapitops.plan.storage.database.transactions.webuser.StoreWebGroupTransaction;
|
||||
@ -94,6 +95,8 @@ class AccessControlVisibilityTest {
|
||||
|
||||
@AfterEach
|
||||
void tearDownTest(WebDriver driver) {
|
||||
String address = "https://localhost:" + TEST_PORT_NUMBER + "/auth/logout";
|
||||
driver.get(address);
|
||||
SeleniumExtension.newTab(driver);
|
||||
driver.manage().deleteAllCookies();
|
||||
}
|
||||
@ -141,8 +144,7 @@ class AccessControlVisibilityTest {
|
||||
Arguments.arguments(WebPermission.PAGE_SERVER_PLAYERBASE_OVERVIEW, "playerbase-insights", "playerbase"),
|
||||
Arguments.arguments(WebPermission.PAGE_SERVER_PLAYERBASE_GRAPHS, "playerbase-graph", "playerbase"),
|
||||
Arguments.arguments(WebPermission.PAGE_SERVER_PLAYERBASE_GRAPHS, "playerbase-current", "playerbase"),
|
||||
Arguments.arguments(WebPermission.PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_TIME, "join-address-graph", "join-addresses"),
|
||||
Arguments.arguments(WebPermission.PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_PIE, "join-address-groups", "join-addresses"),
|
||||
Arguments.arguments(WebPermission.PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_TIME, "server-join-addresses", "join-addresses"),
|
||||
Arguments.arguments(WebPermission.PAGE_SERVER_RETENTION, "retention-graph", "retention"),
|
||||
Arguments.arguments(WebPermission.PAGE_SERVER_PLAYERS, "players-table", "players"),
|
||||
Arguments.arguments(WebPermission.PAGE_SERVER_GEOLOCATIONS_MAP, "geolocations", "geolocations"),
|
||||
@ -172,8 +174,7 @@ class AccessControlVisibilityTest {
|
||||
Arguments.arguments(WebPermission.PAGE_NETWORK_PLAYERBASE_OVERVIEW, "playerbase-insights", "playerbase"),
|
||||
Arguments.arguments(WebPermission.PAGE_NETWORK_PLAYERBASE_GRAPHS, "playerbase-graph", "playerbase"),
|
||||
Arguments.arguments(WebPermission.PAGE_NETWORK_PLAYERBASE_GRAPHS, "playerbase-current", "playerbase"),
|
||||
Arguments.arguments(WebPermission.PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_TIME, "join-address-graph", "join-addresses"),
|
||||
Arguments.arguments(WebPermission.PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_PIE, "join-address-groups", "join-addresses"),
|
||||
Arguments.arguments(WebPermission.PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_TIME, "network-join-addresses", "join-addresses"),
|
||||
Arguments.arguments(WebPermission.PAGE_NETWORK_RETENTION, "retention-graph", "retention"),
|
||||
Arguments.arguments(WebPermission.PAGE_NETWORK_PLAYERS, "players-table", "players"),
|
||||
Arguments.arguments(WebPermission.PAGE_NETWORK_GEOLOCATIONS_MAP, "geolocations", "geolocations"),
|
||||
@ -208,6 +209,8 @@ class AccessControlVisibilityTest {
|
||||
@ParameterizedTest(name = "Access with visibility {0} can see element #{1} in /{2}")
|
||||
@MethodSource("pageLevelVisibleCases")
|
||||
void pageVisible(WebPermission permission, String element, String page, Database database, ServerUUID serverUUID, ChromeDriver driver) throws Exception {
|
||||
// TODO Remove after fixing manage/groups making bazillion calls to /v1/permissions
|
||||
database.executeTransaction(new RemoveWebGroupsTransaction()).get();
|
||||
User user = registerUser(database, permission);
|
||||
|
||||
String address = "https://localhost:" + TEST_PORT_NUMBER + "/" + page;
|
||||
@ -287,7 +290,7 @@ class AccessControlVisibilityTest {
|
||||
driver.get(address);
|
||||
login(driver, user);
|
||||
|
||||
SeleniumExtension.waitForElementToBeVisible(By.id("wrapper"), driver);
|
||||
SeleniumExtension.waitForElementToBeVisible(By.className("login-username"), driver);
|
||||
By id = By.id(element);
|
||||
assertThrows(NoSuchElementException.class, () -> driver.findElement(id), () -> "Saw element #" + element + " at " + address + " without permission to");
|
||||
assertNoLogs(driver, address);
|
||||
@ -329,7 +332,7 @@ class AccessControlVisibilityTest {
|
||||
driver.get(address);
|
||||
login(driver, user);
|
||||
|
||||
SeleniumExtension.waitForElementToBeVisible(By.id("wrapper"), driver);
|
||||
SeleniumExtension.waitForElementToBeVisible(By.className("login-username"), driver);
|
||||
By id = By.id(element);
|
||||
assertThrows(NoSuchElementException.class, () -> driver.findElement(id), () -> "Saw element #" + element + " at " + address + " without permission to");
|
||||
assertNoLogs(driver, address);
|
||||
@ -362,7 +365,7 @@ class AccessControlVisibilityTest {
|
||||
driver.get(address);
|
||||
login(driver, user);
|
||||
|
||||
SeleniumExtension.waitForElementToBeVisible(By.id("wrapper"), driver);
|
||||
SeleniumExtension.waitForElementToBeVisible(By.className("login-username"), driver);
|
||||
By id = By.id(element);
|
||||
assertThrows(NoSuchElementException.class, () -> driver.findElement(id), () -> "Saw element #" + element + " at " + address + " without permission to");
|
||||
assertNoLogs(driver, address);
|
||||
@ -408,7 +411,7 @@ class AccessControlVisibilityTest {
|
||||
driver.get(address);
|
||||
login(driver, playerUser);
|
||||
|
||||
SeleniumExtension.waitForElementToBeVisible(By.id("wrapper"), driver);
|
||||
SeleniumExtension.waitForElementToBeVisible(By.className("login-username"), driver);
|
||||
By id = By.id(element);
|
||||
assertThrows(NoSuchElementException.class, () -> driver.findElement(id), () -> "Saw element #" + element + " at /player/" + TestConstants.PLAYER_TWO_UUID + " without permission to");
|
||||
}
|
||||
|
@ -0,0 +1,88 @@
|
||||
/*
|
||||
* This file is part of Player Analytics (Plan).
|
||||
*
|
||||
* Plan is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License v3 as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Plan 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 Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.djrapitops.plan.gathering;
|
||||
|
||||
import com.djrapitops.plan.settings.config.PlanConfig;
|
||||
import com.djrapitops.plan.settings.config.paths.DataGatheringSettings;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* @author AuroraLS3
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class JoinAddressValidatorTest {
|
||||
|
||||
@Mock
|
||||
PlanConfig config;
|
||||
@InjectMocks
|
||||
JoinAddressValidator joinAddressValidator;
|
||||
|
||||
@DisplayName("Join address is valid")
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@CsvSource({
|
||||
"play.domain.com",
|
||||
"12.34.56.78",
|
||||
"sub.play.domain.xz",
|
||||
})
|
||||
void validJoinAddresses(String address) {
|
||||
assertTrue(joinAddressValidator.isValid(address));
|
||||
}
|
||||
|
||||
@DisplayName("Join address is invalid")
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@CsvSource({
|
||||
"123",
|
||||
"kioels8bfbc80hgjpz25uatt5bi1n0ueffoqxvrl+q7wgbgynl9jm2w38pihx1nw", // https://github.com/plan-player-analytics/Plan/issues/3545
|
||||
"play.domain.com:25565",
|
||||
"play.domain.com:25565\u0000ouehfaounrfaeiurgea",
|
||||
"play.domain.com\u0000h59783g9guheorig",
|
||||
"PLAY.DOMAIN.COM:25565",
|
||||
})
|
||||
void invalidJoinAddresses(String address) {
|
||||
when(config.isTrue(DataGatheringSettings.PRESERVE_INVALID_JOIN_ADDRESS)).thenReturn(false);
|
||||
assertFalse(joinAddressValidator.isValid(address));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Empty join address is invalid")
|
||||
void invalidEmptyJoinAddresses() {
|
||||
assertFalse(joinAddressValidator.isValid(""));
|
||||
}
|
||||
|
||||
@DisplayName("Join address sanitization works")
|
||||
@ParameterizedTest(name = "{0} -> {1}")
|
||||
@CsvSource({
|
||||
"play.domain.com:25565, play.domain.com",
|
||||
"play.domain.com:25565\u0000ouehfaounrfaeiurgea, play.domain.com",
|
||||
"play.domain.com\u0000h59783g9guheorig, play.domain.com",
|
||||
"play.domain.comfmlJEI=1.32.5, play.domain.com",
|
||||
"PLAY.DOMAIN.COM:25565, PLAY.DOMAIN.COM", // Preserve case is on in the mocked config
|
||||
})
|
||||
void sanitizationTest(String address, String expected) {
|
||||
assertEquals(expected, joinAddressValidator.sanitize(address));
|
||||
}
|
||||
}
|
@ -264,13 +264,5 @@ class PlayerLeaveEventConsumerTest {
|
||||
List<String> expected = List.of("PLAY.UPPERCASE.COM", "play.uppercase.com", JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP);
|
||||
List<String> result = database.query(JoinAddressQueries.allJoinAddresses());
|
||||
assertEquals(expected, result);
|
||||
|
||||
Map<String, Integer> expectedMap = Map.of("PLAY.UPPERCASE.COM", 1);
|
||||
Map<String, Integer> resultMap = database.query(JoinAddressQueries.latestJoinAddresses(serverUUID));
|
||||
assertEquals(expectedMap, resultMap);
|
||||
|
||||
expectedMap = Map.of("PLAY.UPPERCASE.COM", 1);
|
||||
resultMap = database.query(JoinAddressQueries.latestJoinAddresses());
|
||||
assertEquals(expectedMap, resultMap);
|
||||
}
|
||||
}
|
@ -393,4 +393,52 @@ class ConfigNodeTest {
|
||||
});
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void environmentVariableString() {
|
||||
ConfigNode node = new ConfigNode("Test.Node.String", new ConfigNode("", null, null), null);
|
||||
String expected = "String";
|
||||
String result = node.getString();
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void environmentVariableBoolean() {
|
||||
ConfigNode node = new ConfigNode("Test.Node.Boolean", new ConfigNode("", null, null), null);
|
||||
Boolean expected = true;
|
||||
Boolean result = node.getBoolean();
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void environmentVariableInteger() {
|
||||
ConfigNode node = new ConfigNode("Test.Node.Integer", new ConfigNode("", null, null), null);
|
||||
Integer expected = 5;
|
||||
Integer result = node.getInteger();
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void environmentVariableDouble() {
|
||||
ConfigNode node = new ConfigNode("Test.Node.Double", new ConfigNode("", null, null), null);
|
||||
Double expected = 0.5;
|
||||
Double result = node.getDouble();
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void environmentVariableLong() {
|
||||
ConfigNode node = new ConfigNode("Test.Node.Long", new ConfigNode("", null, null), null);
|
||||
Long expected = Long.MAX_VALUE;
|
||||
Long result = node.getLong();
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void environmentVariableStringList() {
|
||||
ConfigNode node = new ConfigNode("Test.Node.StringList", new ConfigNode("", null, null), null);
|
||||
List<String> expected = List.of("Test", "Another");
|
||||
List<String> result = node.getStringList();
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
}
|
@ -60,7 +60,7 @@ class FileWatcherTest {
|
||||
createAndModifyFile(modified);
|
||||
|
||||
Awaitility.await()
|
||||
.atMost(1, TimeUnit.SECONDS)
|
||||
.atMost(5, TimeUnit.SECONDS)
|
||||
.until(methodWasCalled::get);
|
||||
|
||||
assertTrue(methodWasCalled.get());
|
||||
|
@ -234,6 +234,58 @@ public interface ExtensionsDatabaseTest extends DatabaseTestPreparer {
|
||||
checkThatPlayerDataExists(ConditionalExtension.condition);
|
||||
}
|
||||
|
||||
@Test
|
||||
default void unsatisfiedPlayerConditionalResultsAreCleanedCompletely() {
|
||||
db().executeTransaction(new PlayerRegisterTransaction(playerUUID, System::currentTimeMillis, TestConstants.PLAYER_ONE_NAME));
|
||||
|
||||
ExtensionSvc extensionService = extensionService();
|
||||
|
||||
extensionService.register(new RemovingConditionalExtension());
|
||||
|
||||
RemovingConditionalExtension.condition = true;
|
||||
extensionService.updatePlayerValues(playerUUID, TestConstants.PLAYER_ONE_NAME, CallEvents.MANUAL);
|
||||
|
||||
List<ExtensionData> ofServer = db().query(new ExtensionPlayerDataQuery(playerUUID)).get(serverUUID());
|
||||
assertTrue(ofServer != null && !ofServer.isEmpty() && !ofServer.get(0).getTabs().isEmpty(), "There was no data left");
|
||||
ExtensionTabData tabData = ofServer.get(0).getTabs().get(0);
|
||||
assertEquals(RemovingConditionalExtension.condition, tabData.getString("conditionalValue").isPresent());
|
||||
|
||||
// Reverse condition
|
||||
RemovingConditionalExtension.condition = false;
|
||||
extensionService.updatePlayerValues(playerUUID, TestConstants.PLAYER_ONE_NAME, CallEvents.MANUAL);
|
||||
|
||||
ofServer = db().query(new ExtensionPlayerDataQuery(playerUUID)).get(serverUUID());
|
||||
assertTrue(ofServer != null && !ofServer.isEmpty() && !ofServer.get(0).getTabs().isEmpty(), "There was no data left");
|
||||
tabData = ofServer.get(0).getTabs().get(0);
|
||||
assertEquals(RemovingConditionalExtension.condition, tabData.getString("conditionalValue").isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
default void unsatisfiedServerConditionalResultsAreCleanedCompletely() {
|
||||
db().executeTransaction(new PlayerRegisterTransaction(playerUUID, System::currentTimeMillis, TestConstants.PLAYER_ONE_NAME));
|
||||
|
||||
ExtensionSvc extensionService = extensionService();
|
||||
|
||||
extensionService.register(new RemovingConditionalExtension());
|
||||
|
||||
RemovingConditionalExtension.condition = true;
|
||||
extensionService.updateServerValues(CallEvents.MANUAL);
|
||||
|
||||
List<ExtensionData> ofServer = db().query(new ExtensionServerDataQuery(serverUUID()));
|
||||
assertTrue(ofServer != null && !ofServer.isEmpty() && !ofServer.get(0).getTabs().isEmpty(), "There was no data left");
|
||||
ExtensionTabData tabData = ofServer.get(0).getTabs().get(0);
|
||||
assertEquals(RemovingConditionalExtension.condition, tabData.getString("conditionalValue").isPresent());
|
||||
|
||||
// Reverse condition
|
||||
RemovingConditionalExtension.condition = false;
|
||||
extensionService.updateServerValues(CallEvents.MANUAL);
|
||||
|
||||
ofServer = db().query(new ExtensionServerDataQuery(serverUUID()));
|
||||
assertTrue(ofServer != null && !ofServer.isEmpty() && !ofServer.get(0).getTabs().isEmpty(), "There was no data left");
|
||||
tabData = ofServer.get(0).getTabs().get(0);
|
||||
assertEquals(RemovingConditionalExtension.condition, tabData.getString("conditionalValue").isPresent());
|
||||
}
|
||||
|
||||
default void checkThatPlayerDataExists(boolean condition) {
|
||||
if (condition) { // Condition is true, conditional values exist
|
||||
List<ExtensionData> ofServer = db().query(new ExtensionPlayerDataQuery(playerUUID)).get(serverUUID());
|
||||
@ -437,6 +489,34 @@ public interface ExtensionsDatabaseTest extends DatabaseTestPreparer {
|
||||
}
|
||||
}
|
||||
|
||||
@PluginInfo(name = "ConditionalExtension")
|
||||
class RemovingConditionalExtension implements DataExtension {
|
||||
|
||||
static boolean condition = true;
|
||||
|
||||
@BooleanProvider(text = "a boolean", conditionName = "condition")
|
||||
public boolean isCondition(UUID playerUUID) {
|
||||
return condition;
|
||||
}
|
||||
|
||||
@StringProvider(text = "Conditional Value")
|
||||
@Conditional("condition")
|
||||
public String conditionalValue(UUID playerUUID) {
|
||||
return "Conditional";
|
||||
}
|
||||
|
||||
@BooleanProvider(text = "a boolean", conditionName = "condition")
|
||||
public boolean isCondition() {
|
||||
return condition;
|
||||
}
|
||||
|
||||
@StringProvider(text = "Conditional Value")
|
||||
@Conditional("condition")
|
||||
public String conditionalValue() {
|
||||
return "Conditional";
|
||||
}
|
||||
}
|
||||
|
||||
@PluginInfo(name = "ServerExtension")
|
||||
class ServerExtension implements DataExtension {
|
||||
@NumberProvider(text = "a number")
|
||||
|
@ -16,8 +16,10 @@
|
||||
*/
|
||||
package com.djrapitops.plan.storage.database.queries;
|
||||
|
||||
import com.djrapitops.plan.delivery.domain.DateObj;
|
||||
import com.djrapitops.plan.gathering.domain.FinishedSession;
|
||||
import com.djrapitops.plan.gathering.domain.event.JoinAddress;
|
||||
import com.djrapitops.plan.identification.ServerUUID;
|
||||
import com.djrapitops.plan.settings.config.paths.DataGatheringSettings;
|
||||
import com.djrapitops.plan.storage.database.DatabaseTestPreparer;
|
||||
import com.djrapitops.plan.storage.database.queries.objects.BaseUserQueries;
|
||||
@ -27,12 +29,14 @@ import com.djrapitops.plan.storage.database.sql.tables.JoinAddressTable;
|
||||
import com.djrapitops.plan.storage.database.transactions.commands.RemoveEverythingTransaction;
|
||||
import com.djrapitops.plan.storage.database.transactions.events.*;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import utilities.RandomData;
|
||||
import utilities.TestConstants;
|
||||
import utilities.TestData;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
@ -120,6 +124,55 @@ public interface JoinAddressQueriesTest extends DatabaseTestPreparer {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Join address by day is filtered by addresses")
|
||||
default void joinAddressListIsFilteredByAddress() {
|
||||
db().executeTransaction(TestData.storeServers());
|
||||
|
||||
db().executeTransaction(new StoreWorldNameTransaction(TestConstants.SERVER_TWO_UUID, worlds[0]));
|
||||
db().executeTransaction(new StoreWorldNameTransaction(TestConstants.SERVER_TWO_UUID, worlds[1]));
|
||||
FinishedSession session = RandomData.randomSession(TestConstants.SERVER_TWO_UUID, worlds, playerUUID, player2UUID);
|
||||
String expectedAddress = TestConstants.GET_PLAYER_HOSTNAME.get();
|
||||
session.getExtraData().put(JoinAddress.class, new JoinAddress(expectedAddress));
|
||||
db().executeTransaction(new StoreSessionTransaction(session));
|
||||
|
||||
List<DateObj<Map<String, Integer>>> result = db().query(JoinAddressQueries.joinAddressesPerDay(0, 0, System.currentTimeMillis(), List.of("nonexistent.com")));
|
||||
assertEquals(List.of(), result);
|
||||
|
||||
long startOfDay = session.getDate() - session.getDate() % TimeUnit.DAYS.toMillis(1);
|
||||
|
||||
List<DateObj<Map<String, Integer>>> result2 = db().query(JoinAddressQueries.joinAddressesPerDay(0, 0, System.currentTimeMillis(), List.of(expectedAddress)));
|
||||
assertEquals(List.of(new DateObj<>(startOfDay, Map.of(expectedAddress, 1))), result2);
|
||||
|
||||
List<DateObj<Map<String, Integer>>> result3 = db().query(JoinAddressQueries.joinAddressesPerDay(0, 0, System.currentTimeMillis(), List.of()));
|
||||
assertEquals(List.of(new DateObj<>(startOfDay, Map.of(expectedAddress, 1))), result3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Server join address by day is filtered by addresses")
|
||||
default void serverJoinAddressListIsFilteredByAddress() {
|
||||
db().executeTransaction(TestData.storeServers());
|
||||
|
||||
ServerUUID serverTwoUuid = TestConstants.SERVER_TWO_UUID;
|
||||
db().executeTransaction(new StoreWorldNameTransaction(serverTwoUuid, worlds[0]));
|
||||
db().executeTransaction(new StoreWorldNameTransaction(serverTwoUuid, worlds[1]));
|
||||
FinishedSession session = RandomData.randomSession(serverTwoUuid, worlds, playerUUID, player2UUID);
|
||||
String expectedAddress = TestConstants.GET_PLAYER_HOSTNAME.get();
|
||||
session.getExtraData().put(JoinAddress.class, new JoinAddress(expectedAddress));
|
||||
db().executeTransaction(new StoreSessionTransaction(session));
|
||||
|
||||
List<DateObj<Map<String, Integer>>> result = db().query(JoinAddressQueries.joinAddressesPerDay(serverTwoUuid, 0, 0, System.currentTimeMillis(), List.of("nonexistent.com")));
|
||||
assertEquals(List.of(), result);
|
||||
|
||||
long startOfDay = session.getDate() - session.getDate() % TimeUnit.DAYS.toMillis(1);
|
||||
|
||||
List<DateObj<Map<String, Integer>>> result2 = db().query(JoinAddressQueries.joinAddressesPerDay(serverTwoUuid, 0, 0, System.currentTimeMillis(), List.of(expectedAddress)));
|
||||
assertEquals(List.of(new DateObj<>(startOfDay, Map.of(expectedAddress, 1))), result2);
|
||||
|
||||
List<DateObj<Map<String, Integer>>> result3 = db().query(JoinAddressQueries.joinAddressesPerDay(serverTwoUuid, 0, 0, System.currentTimeMillis(), List.of()));
|
||||
assertEquals(List.of(new DateObj<>(startOfDay, Map.of(expectedAddress, 1))), result3);
|
||||
}
|
||||
|
||||
@Test
|
||||
default void joinAddressIsTruncated() {
|
||||
db().executeTransaction(new StoreWorldNameTransaction(serverUUID(), worlds[0]));
|
||||
@ -201,15 +254,6 @@ public interface JoinAddressQueriesTest extends DatabaseTestPreparer {
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
default void serverJoinAddressQueryHasNoNullValues() {
|
||||
joinAddressCanBeUnknown();
|
||||
|
||||
Map<String, Integer> expected = Collections.singletonMap(JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP, 1);
|
||||
Map<String, Integer> result = db().query(JoinAddressQueries.latestJoinAddresses(serverUUID()));
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
default void joinAddressQueryHasDistinctPlayers() {
|
||||
joinAddressCanBeUnknown();
|
||||
|
@ -20,11 +20,13 @@ import com.djrapitops.plan.PlanSystem;
|
||||
import com.djrapitops.plan.commands.PlanCommand;
|
||||
import com.djrapitops.plan.delivery.DeliveryUtilities;
|
||||
import com.djrapitops.plan.delivery.export.Exporter;
|
||||
import com.djrapitops.plan.delivery.formatting.Formatters;
|
||||
import com.djrapitops.plan.delivery.webserver.Addresses;
|
||||
import com.djrapitops.plan.identification.ServerUUID;
|
||||
import com.djrapitops.plan.settings.ConfigSystem;
|
||||
import com.djrapitops.plan.settings.config.PlanConfig;
|
||||
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
|
||||
import com.djrapitops.plan.settings.locale.LocaleSystem;
|
||||
import com.djrapitops.plan.storage.database.Database;
|
||||
import com.djrapitops.plan.storage.file.PlanFiles;
|
||||
import com.djrapitops.plan.storage.file.PublicHtmlFiles;
|
||||
@ -57,6 +59,8 @@ public class FullSystemExtension implements ParameterResolver, BeforeAllCallback
|
||||
private final Map<Class, Supplier> parameterResolvers;
|
||||
|
||||
public FullSystemExtension() {
|
||||
// You can't use method references here because planSystem is null when this method is initialized.
|
||||
|
||||
this.parameterResolvers = Maps.builder(Class.class, Supplier.class)
|
||||
.put(PlanSystem.class, () -> planSystem)
|
||||
.put(PlanFiles.class, () -> planSystem.getPlanFiles())
|
||||
@ -79,6 +83,8 @@ public class FullSystemExtension implements ParameterResolver, BeforeAllCallback
|
||||
})
|
||||
.put(Database.class, () -> planSystem.getDatabaseSystem().getDatabase())
|
||||
.put(DeliveryUtilities.class, () -> planSystem.getDeliveryUtilities())
|
||||
.put(Formatters.class, () -> planSystem.getDeliveryUtilities().getFormatters())
|
||||
.put(LocaleSystem.class, () -> planSystem.getLocaleSystem())
|
||||
.put(Addresses.class, () -> planSystem.getDeliveryUtilities().getAddresses())
|
||||
.put(PublicHtmlFiles.class, () -> planSystem.getDeliveryUtilities().getPublicHtmlFiles())
|
||||
.put(Webserver.class, () -> planSystem.getWebServerSystem().getWebServer())
|
||||
|
@ -57,7 +57,8 @@ public class SeleniumExtension implements ParameterResolver, BeforeAllCallback,
|
||||
SeleniumExtension.waitForPageLoadForSeconds(5, driver);
|
||||
Awaitility.await("waitForElementToBeVisible " + by.toString())
|
||||
.atMost(5, TimeUnit.SECONDS)
|
||||
.ignoreException(NoSuchElementException.class)
|
||||
.ignoreExceptionsMatching(throwable -> throwable instanceof NoSuchElementException
|
||||
|| throwable instanceof StaleElementReferenceException)
|
||||
.until(() -> driver.findElement(by).isDisplayed());
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ dependencies {
|
||||
shadow 'net.playeranalytics:Extension-Jobs:4.16.3-R1.1'
|
||||
shadow 'net.playeranalytics:Extension-KingdomsX:1.12.6.3.1-R1.4'
|
||||
shadow 'net.playeranalytics:Extension-Lands:6.35.0-R1.2'
|
||||
shadow 'net.playeranalytics:Extension-LibertyBans:1.1.0-R1.4'
|
||||
shadow 'net.playeranalytics:Extension-LibertyBans:1.1.0-R1.5'
|
||||
shadow 'net.playeranalytics:Extension-Litebans:0.4.1-R1.5'
|
||||
shadow 'net.playeranalytics:Extension-LogBlock:1.16.1.2-R1.9'
|
||||
shadow 'net.playeranalytics:Extension-LuckPerms:5.0-R1.6'
|
||||
@ -32,7 +32,7 @@ dependencies {
|
||||
shadow 'net.playeranalytics:Extension-MyPet:3.10-R1.2'
|
||||
shadow 'net.playeranalytics:Extension-Nucleus:3.0.0-R1.0'
|
||||
shadow 'net.playeranalytics:Extension-nuVotifier:2.3.4-R1.3'
|
||||
shadow 'net.playeranalytics:Extension-PlaceholderAPI:2.10.9-R1.5'
|
||||
shadow 'net.playeranalytics:Extension-PlaceholderAPI:2.10.9-R1.6'
|
||||
shadow 'net.playeranalytics:Extension-PlotSquared:6.9.4-R1.3'
|
||||
shadow 'net.playeranalytics:Extension-ProtectionStones:2.8.2-R1.2'
|
||||
shadow 'net.playeranalytics:Extension-ProtocolSupport:1.16.4-R1.3'
|
||||
|
@ -8,10 +8,10 @@ dependencies {
|
||||
|
||||
shadow "net.playeranalytics:platform-abstraction-layer-api:$palVersion"
|
||||
|
||||
minecraft "com.mojang:minecraft:1.20.4"
|
||||
mappings "net.fabricmc:yarn:1.20.4+build.3:v2"
|
||||
modImplementation "net.fabricmc:fabric-loader:0.15.2"
|
||||
modImplementation('me.lucko:fabric-permissions-api:0.3-SNAPSHOT')
|
||||
minecraft "com.mojang:minecraft:1.20.5-rc2"
|
||||
mappings "net.fabricmc:yarn:1.20.5-rc2+build.1:v2"
|
||||
modImplementation "net.fabricmc:fabric-loader:0.15.10"
|
||||
modImplementation('me.lucko:fabric-permissions-api:0.3.1')
|
||||
|
||||
// Fabric API
|
||||
Set<String> apiModules = [
|
||||
@ -24,7 +24,7 @@ dependencies {
|
||||
]
|
||||
|
||||
apiModules.forEach {
|
||||
modImplementation(fabricApi.module(it, "0.91.2+1.20.4"))
|
||||
modImplementation(fabricApi.module(it, "0.97.3+1.20.5"))
|
||||
}
|
||||
|
||||
testImplementation project(path: ":common", configuration: 'testArtifacts')
|
||||
@ -36,7 +36,7 @@ loom {
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.release.set(17)
|
||||
options.release.set(21)
|
||||
}
|
||||
|
||||
processResources {
|
||||
|
@ -27,7 +27,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
@Mixin(ServerHandshakeNetworkHandler.class)
|
||||
public class ClientToServerHandshakePacketMixin {
|
||||
|
||||
@Inject(method = "onHandshake", at = @At(value = "INVOKE", ordinal = 0, target = "Lnet/minecraft/network/ClientConnection;setPacketListener(Lnet/minecraft/network/listener/PacketListener;)V"))
|
||||
@Inject(method = "onHandshake", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/network/ServerHandshakeNetworkHandler;login(Lnet/minecraft/network/packet/c2s/handshake/HandshakeC2SPacket;Z)V"))
|
||||
public void onClientHandshakeFromNetwork(HandshakeC2SPacket packet, CallbackInfo ci) {
|
||||
PlanFabricEvents.ON_HANDSHAKE.invoker().onHandshake(packet);
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
*/
|
||||
package net.playeranalytics.plan.gathering.listeners.fabric;
|
||||
|
||||
import com.djrapitops.plan.gathering.JoinAddressValidator;
|
||||
import com.djrapitops.plan.gathering.cache.JoinAddressCache;
|
||||
import com.djrapitops.plan.gathering.domain.event.PlayerJoin;
|
||||
import com.djrapitops.plan.gathering.domain.event.PlayerLeave;
|
||||
@ -51,6 +52,7 @@ public class PlayerOnlineListener implements FabricListener {
|
||||
private final PlayerJoinEventConsumer joinEventConsumer;
|
||||
private final PlayerLeaveEventConsumer leaveEventConsumer;
|
||||
private final JoinAddressCache joinAddressCache;
|
||||
private final JoinAddressValidator joinAddressValidator;
|
||||
|
||||
private final ServerInfo serverInfo;
|
||||
private final DBSystem dbSystem;
|
||||
@ -66,7 +68,9 @@ public class PlayerOnlineListener implements FabricListener {
|
||||
public PlayerOnlineListener(
|
||||
PlayerJoinEventConsumer joinEventConsumer,
|
||||
PlayerLeaveEventConsumer leaveEventConsumer,
|
||||
JoinAddressCache joinAddressCache, ServerInfo serverInfo,
|
||||
JoinAddressCache joinAddressCache,
|
||||
JoinAddressValidator joinAddressValidator,
|
||||
ServerInfo serverInfo,
|
||||
DBSystem dbSystem,
|
||||
ErrorLogger errorLogger,
|
||||
MinecraftDedicatedServer server
|
||||
@ -74,6 +78,7 @@ public class PlayerOnlineListener implements FabricListener {
|
||||
this.joinEventConsumer = joinEventConsumer;
|
||||
this.leaveEventConsumer = leaveEventConsumer;
|
||||
this.joinAddressCache = joinAddressCache;
|
||||
this.joinAddressValidator = joinAddressValidator;
|
||||
this.serverInfo = serverInfo;
|
||||
this.dbSystem = dbSystem;
|
||||
this.errorLogger = errorLogger;
|
||||
@ -127,10 +132,7 @@ public class PlayerOnlineListener implements FabricListener {
|
||||
private void onHandshake(HandshakeC2SPacket packet) {
|
||||
try {
|
||||
if (packet.intendedState() == ConnectionIntent.LOGIN) {
|
||||
String address = packet.address();
|
||||
if (address != null && address.contains("\u0000")) {
|
||||
address = address.substring(0, address.indexOf('\u0000'));
|
||||
}
|
||||
String address = joinAddressValidator.sanitize(packet.address());
|
||||
joinAddress.set(address);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@ -143,7 +145,10 @@ public class PlayerOnlineListener implements FabricListener {
|
||||
UUID playerUUID = profile.getId();
|
||||
ServerUUID serverUUID = serverInfo.getServerUUID();
|
||||
|
||||
joinAddressCache.put(playerUUID, joinAddress.get());
|
||||
String playerJoinAddress = joinAddress.get();
|
||||
if (joinAddressValidator.isValid(playerJoinAddress)) {
|
||||
joinAddressCache.put(playerUUID, playerJoinAddress);
|
||||
}
|
||||
|
||||
dbSystem.getDatabase().executeTransaction(new BanStatusTransaction(playerUUID, serverUUID, banned));
|
||||
} catch (Exception e) {
|
||||
|
@ -13,7 +13,7 @@
|
||||
"plan.mixins.json"
|
||||
],
|
||||
"depends": {
|
||||
"minecraft": ">=1.19",
|
||||
"minecraft": ">=1.20.5",
|
||||
"java": ">=17",
|
||||
"fabric-api-base": "*",
|
||||
"fabric-command-api-v2": "*",
|
||||
|
11
Plan/folia/build.gradle
Normal file
11
Plan/folia/build.gradle
Normal file
@ -0,0 +1,11 @@
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.release.set(17)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
runtimeOnly "net.playeranalytics:platform-abstraction-layer-folia:$palVersion"
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
configurations = [project.configurations.shadow]
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
@ -158,6 +158,9 @@ public class PlanNukkit extends PluginBase implements PlanPlugin {
|
||||
sender = new NukkitCMDSender(actualSender);
|
||||
}
|
||||
|
||||
if (command.getRequiredPermissions().stream().anyMatch(permission -> !sender.hasPermission(permission))) {
|
||||
return true;
|
||||
}
|
||||
runnableFactory.create(() -> {
|
||||
try {
|
||||
command.getExecutor().accept(sender, new Arguments(args));
|
||||
|
@ -8,6 +8,7 @@ dependencies {
|
||||
shadow project(path: ":sponge")
|
||||
shadow project(path: ":bungeecord")
|
||||
shadow project(path: ":velocity")
|
||||
shadow project(path: ":folia")
|
||||
testImplementation project(path: ":common", configuration: 'testArtifacts')
|
||||
testImplementation project(path: ":bukkit", configuration: 'testArtifacts')
|
||||
testImplementation project(path: ":nukkit", configuration: 'testArtifacts')
|
||||
|
@ -7,41 +7,41 @@
|
||||
"proxy": "http://localhost:8800",
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@fullcalendar/bootstrap": "^6.1.11",
|
||||
"@fullcalendar/core": "^6.1.11",
|
||||
"@fullcalendar/daygrid": "^6.1.11",
|
||||
"@fullcalendar/interaction": "^6.1.11",
|
||||
"@fullcalendar/react": "^6.1.11",
|
||||
"@highcharts/map-collection": "^2.1.0",
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
"@testing-library/react": "^14.2.1",
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@fullcalendar/bootstrap": "^6.1.15",
|
||||
"@fullcalendar/core": "^6.1.15",
|
||||
"@fullcalendar/daygrid": "^6.1.15",
|
||||
"@fullcalendar/interaction": "^6.1.15",
|
||||
"@fullcalendar/react": "^6.1.15",
|
||||
"@highcharts/map-collection": "^2.2.0",
|
||||
"@testing-library/jest-dom": "^6.4.8",
|
||||
"@testing-library/react": "^15.0.7",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"axios": "^1.6.7",
|
||||
"axios": "^1.7.3",
|
||||
"bootstrap": "^5.3.3",
|
||||
"export-to-csv": "^1.2.4",
|
||||
"highcharts": "^10.3.3",
|
||||
"i18next": "^23.10.0",
|
||||
"export-to-csv": "^1.3.0",
|
||||
"highcharts": "^11.4.5",
|
||||
"i18next": "^23.12.2",
|
||||
"i18next-chained-backend": "^4.6.2",
|
||||
"i18next-http-backend": "^2.5.0",
|
||||
"i18next-http-backend": "^2.5.2",
|
||||
"i18next-localstorage-backend": "^4.2.0",
|
||||
"masonry-layout": "^4.2.2",
|
||||
"moment": "^2.30.1",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.10.1",
|
||||
"react": "^18.3.1",
|
||||
"react-bootstrap": "^2.10.4",
|
||||
"react-bootstrap-range-slider": "^3.0.8",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^14.1.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-i18next": "^15.0.0",
|
||||
"react-mcjsonchat": "^1.0.0",
|
||||
"react-router-dom": "6",
|
||||
"sass": "^1.71.1",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"sass": "^1.77.8",
|
||||
"source-map-explorer": "^2.5.2",
|
||||
"swagger-ui": "^5.11.10",
|
||||
"swagger-ui": "^5.17.14",
|
||||
"web-vitals": "^3.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
@ -68,7 +68,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"vite": "^5.1.5"
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"vite": "^5.3.5"
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,14 @@ import {useQueryResultContext} from "../../hooks/queryResultContext";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faFilter} from "@fortawesome/free-solid-svg-icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {Link} from "react-router-dom";
|
||||
|
||||
const QueryPath = () => {
|
||||
const QueryPath = ({newQuery}) => {
|
||||
const {t} = useTranslation();
|
||||
const {result} = useQueryResultContext();
|
||||
const hasResults = result && result.data;
|
||||
const hasResults = Boolean(result?.data);
|
||||
const path = result?.path;
|
||||
if (!path || !path.length) return <></>;
|
||||
if (!path?.length || (newQuery && hasResults)) return <></>;
|
||||
|
||||
|
||||
const getReadableFilterName = kind => {
|
||||
@ -48,6 +49,7 @@ const QueryPath = () => {
|
||||
|
||||
return (
|
||||
<aside id={"result-path"} className={"alert shadow " + (hasResults ? "alert-success" : "alert-warning")}>
|
||||
{!newQuery && <Link className={"link float-end"} to={"/query/new"}>{t('html.query.label.editQuery')}</Link>}
|
||||
{path.map((step, i) => <p key={step.kind + step.size}
|
||||
style={{marginBottom: 0, marginLeft: i * 0.7 + "rem"}}>
|
||||
<FontAwesomeIcon
|
||||
|
@ -0,0 +1,66 @@
|
||||
import {useTranslation} from "react-i18next";
|
||||
import React, {useCallback, useEffect, useState} from "react";
|
||||
import {Card, Form} from "react-bootstrap";
|
||||
import CardHeader from "../CardHeader.jsx";
|
||||
import {faCheck, faList, faPencil} from "@fortawesome/free-solid-svg-icons";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import MultiSelect from "../../input/MultiSelect.jsx";
|
||||
import {faTrashAlt} from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
const AddressGroupCard = ({n, group, editGroup, allAddresses, remove}) => {
|
||||
const {t} = useTranslation();
|
||||
const [selectedIndexes, setSelectedIndexes] = useState([]);
|
||||
const [editingName, setEditingName] = useState(false);
|
||||
const [name, setName] = useState(group.name);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedIndexes.length && allAddresses?.length && group?.addresses?.length) {
|
||||
setSelectedIndexes(group.addresses
|
||||
.map(address => allAddresses.indexOf(address))
|
||||
.filter(index => index !== -1)) // Make sure addresses are not selected that no longer exist
|
||||
}
|
||||
}, [selectedIndexes, group, allAddresses])
|
||||
|
||||
const applySelected = useCallback(() => {
|
||||
editGroup({...group, addresses: allAddresses.filter((a, i) => selectedIndexes.includes(i))})
|
||||
}, [editGroup, group, allAddresses, selectedIndexes]);
|
||||
const editName = useCallback(newName => {
|
||||
editGroup({...group, name: newName});
|
||||
}, [editGroup, group]);
|
||||
useEffect(() => {
|
||||
if (!editingName && name !== group.name) editName(name);
|
||||
}, [editName, editingName, name])
|
||||
|
||||
const selectedAddresses = allAddresses.filter((a, i) => selectedIndexes.includes(i));
|
||||
const isUpToDate = !selectedIndexes.length || selectedAddresses.length === group.addresses.length && selectedAddresses.every((a, i) => a === group.addresses[i]);
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader icon={faList} color={"amber"} label={
|
||||
editingName ?
|
||||
<Form.Control
|
||||
style={{position: "absolute", top: "0.5rem", left: "2.5rem", width: "calc(100% - 3rem)"}}
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}/> : group.name
|
||||
}>
|
||||
<button
|
||||
style={editingName ? {position: "absolute", right: "1rem", top: "1.2rem"} : {marginLeft: "0.5rem"}}
|
||||
onClick={() => setEditingName(!editingName)}>
|
||||
<FontAwesomeIcon icon={editingName ? faCheck : faPencil}/>
|
||||
</button>
|
||||
</CardHeader>
|
||||
<Card.Body>
|
||||
<MultiSelect options={allAddresses} selectedIndexes={selectedIndexes}
|
||||
setSelectedIndexes={setSelectedIndexes}/>
|
||||
<button className={'mt-2 btn ' + (isUpToDate ? 'bg-transparent' : 'bg-theme')}
|
||||
onClick={applySelected} disabled={isUpToDate}>
|
||||
{t('html.label.apply')}
|
||||
</button>
|
||||
<button className={'mt-2 btn btn-outline-secondary float-end'}
|
||||
onClick={remove}>
|
||||
<FontAwesomeIcon icon={faTrashAlt}/>
|
||||
</button>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
export default AddressGroupCard;
|
@ -0,0 +1,32 @@
|
||||
import {Col, Row} from "react-bootstrap";
|
||||
import AddressGroupCard from "./AddressGroupCard.jsx";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faPlus} from "@fortawesome/free-solid-svg-icons";
|
||||
import React from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {useJoinAddressListContext} from "../../../hooks/context/joinAddressListContextHook.jsx";
|
||||
|
||||
const AddressGroupSelectorRow = () => {
|
||||
const {t} = useTranslation();
|
||||
const {list, add, remove, replace, allAddresses} = useJoinAddressListContext();
|
||||
|
||||
return (
|
||||
<Row id={"address-selector"}>
|
||||
{list.map((group, i) =>
|
||||
<Col lg={2} key={group.uuid}>
|
||||
<AddressGroupCard n={i + 1}
|
||||
group={group}
|
||||
editGroup={replacement => replace(replacement, i)}
|
||||
allAddresses={allAddresses}
|
||||
remove={() => remove(i)}/>
|
||||
</Col>)}
|
||||
<Col lg={2}>
|
||||
<button className={"btn bg-theme mb-4"} onClick={add}>
|
||||
<FontAwesomeIcon icon={faPlus}/> {t('html.label.addJoinAddressGroup')}
|
||||
</button>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddressGroupSelectorRow;
|
@ -0,0 +1,27 @@
|
||||
import LoadIn from "../../animation/LoadIn.jsx";
|
||||
import ExtendableRow from "../../layout/extension/ExtendableRow.jsx";
|
||||
import JoinAddressGraphCard from "../server/graphs/JoinAddressGraphCard.jsx";
|
||||
import {Col} from "react-bootstrap";
|
||||
import React from "react";
|
||||
import AddressGroupSelectorRow from "./AddressGroupSelectorRow.jsx";
|
||||
import {JoinAddressListContextProvider} from "../../../hooks/context/joinAddressListContextHook.jsx";
|
||||
import {staticSite} from "../../../service/backendConfiguration.js";
|
||||
|
||||
const JoinAddresses = ({id, seeTime, identifier}) => {
|
||||
return (
|
||||
<LoadIn>
|
||||
{seeTime && <section id={id} className={id}>
|
||||
<JoinAddressListContextProvider identifier={identifier} isAllowed={seeTime}>
|
||||
<ExtendableRow id={`row-${id}-0`}>
|
||||
<Col lg={12}>
|
||||
<JoinAddressGraphCard identifier={identifier}/>
|
||||
</Col>
|
||||
</ExtendableRow>
|
||||
{!staticSite && <AddressGroupSelectorRow/>}
|
||||
</JoinAddressListContextProvider>
|
||||
</section>}
|
||||
</LoadIn>
|
||||
)
|
||||
}
|
||||
|
||||
export default JoinAddresses;
|
@ -0,0 +1,30 @@
|
||||
import LoadIn from "../../animation/LoadIn.jsx";
|
||||
import ExtendableRow from "../../layout/extension/ExtendableRow.jsx";
|
||||
import {Col} from "react-bootstrap";
|
||||
import PlayerRetentionGraphCard from "./PlayerRetentionGraphCard.jsx";
|
||||
import React, {useState} from "react";
|
||||
import {JoinAddressListContextProvider} from "../../../hooks/context/joinAddressListContextHook.jsx";
|
||||
import AddressGroupSelectorRow from "./AddressGroupSelectorRow.jsx";
|
||||
|
||||
const PlayerRetention = ({id, seeRetention, identifier}) => {
|
||||
const [selectedGroupBy, setSelectedGroupBy] = useState('none');
|
||||
return (
|
||||
<LoadIn>
|
||||
{seeRetention && <section id={id} className={id}>
|
||||
<JoinAddressListContextProvider identifier={identifier} isAllowed={seeRetention}
|
||||
loadIndividualAddresses>
|
||||
<ExtendableRow id={`row-${id}-0`}>
|
||||
<Col lg={12}>
|
||||
<PlayerRetentionGraphCard identifier={identifier}
|
||||
selectedGroupBy={selectedGroupBy}
|
||||
setSelectedGroupBy={setSelectedGroupBy}/>
|
||||
</Col>
|
||||
</ExtendableRow>
|
||||
{selectedGroupBy === 'joinAddress' && <AddressGroupSelectorRow/>}
|
||||
</JoinAddressListContextProvider>
|
||||
</section>}
|
||||
</LoadIn>
|
||||
)
|
||||
};
|
||||
|
||||
export default PlayerRetention
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user