diff --git a/src/me/ryanhamshire/GriefPrevention/DataStore.java b/src/me/ryanhamshire/GriefPrevention/DataStore.java index 4dd87ce..92a1b3d 100644 --- a/src/me/ryanhamshire/GriefPrevention/DataStore.java +++ b/src/me/ryanhamshire/GriefPrevention/DataStore.java @@ -19,6 +19,7 @@ package me.ryanhamshire.GriefPrevention; import java.io.*; +import java.nio.charset.Charset; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; @@ -61,6 +62,7 @@ public abstract class DataStore final static String configFilePath = dataLayerFolderPath + File.separator + "config.yml"; final static String messagesFilePath = dataLayerFolderPath + File.separator + "messages.yml"; final static String softMuteFilePath = dataLayerFolderPath + File.separator + "softMute.txt"; + final static String bannedWordsFilePath = dataLayerFolderPath + File.separator + "bannedWords.txt"; //the latest version of the data schema implemented here protected static final int latestSchemaVersion = 2; @@ -211,6 +213,31 @@ public abstract class DataStore } } + List loadBannedWords() + { + try + { + File bannedWordsFile = new File(bannedWordsFilePath); + if(!bannedWordsFile.exists()) + { + Files.touch(bannedWordsFile); + String defaultWords = + "nigger\nniggers\nniger\nnigga\nnigers\nniggas\n" + + "fag\nfags\nfaggot\nfaggots\nfeggit\nfeggits\nfaggit\nfaggits\n" + + "cunt\ncunts\nwhore\nwhores\nslut\nsluts\n"; + Files.append(defaultWords, bannedWordsFile, Charset.forName("UTF-8")); + } + + return Files.readLines(bannedWordsFile, Charset.forName("UTF-8")); + } + catch(Exception e) + { + GriefPrevention.AddLogEntry("Failed to read from the banned words data file: " + e.toString()); + e.printStackTrace(); + return new ArrayList(); + } + } + //updates soft mute map and data file boolean toggleSoftMute(UUID playerID) { @@ -1338,6 +1365,7 @@ public abstract class DataStore this.addDefault(defaults, Messages.BookTools, "Our claim tools are {0} and {1}.", "0: claim modification tool name; 1:claim information tool name"); this.addDefault(defaults, Messages.BookDisabledChestClaims, " On this server, placing a chest will NOT claim land for you.", null); this.addDefault(defaults, Messages.BookUsefulCommands, "Useful Commands:", null); + this.addDefault(defaults, Messages.NoProfanity, "Please moderate your language.", null); //load the config file FileConfiguration config = YamlConfiguration.loadConfiguration(new File(messagesFilePath)); diff --git a/src/me/ryanhamshire/GriefPrevention/Messages.java b/src/me/ryanhamshire/GriefPrevention/Messages.java index 050e575..4424deb 100644 --- a/src/me/ryanhamshire/GriefPrevention/Messages.java +++ b/src/me/ryanhamshire/GriefPrevention/Messages.java @@ -20,5 +20,5 @@ package me.ryanhamshire.GriefPrevention; public enum Messages { - RespectingClaims, IgnoringClaims, SuccessfulAbandon, RestoreNatureActivate, RestoreNatureAggressiveActivate, FillModeActive, TransferClaimPermission, TransferClaimMissing, TransferClaimAdminOnly, PlayerNotFound2, TransferTopLevel, TransferSuccess, TrustListNoClaim, ClearPermsOwnerOnly, UntrustIndividualAllClaims, UntrustEveryoneAllClaims, NoPermissionTrust, ClearPermissionsOneClaim, UntrustIndividualSingleClaim, OnlySellBlocks, BlockPurchaseCost, ClaimBlockLimit, InsufficientFunds, PurchaseConfirmation, OnlyPurchaseBlocks, BlockSaleValue, NotEnoughBlocksForSale, BlockSaleConfirmation, AdminClaimsMode, BasicClaimsMode, SubdivisionMode, SubdivisionVideo2, DeleteClaimMissing, DeletionSubdivisionWarning, DeleteSuccess, CantDeleteAdminClaim, DeleteAllSuccess, NoDeletePermission, AllAdminDeleted, AdjustBlocksSuccess, NotTrappedHere, RescuePending, NonSiegeWorld, AlreadySieging, NotSiegableThere, SiegeTooFarAway, NoSiegeDefenseless, AlreadyUnderSiegePlayer, AlreadyUnderSiegeArea, NoSiegeAdminClaim, SiegeOnCooldown, SiegeAlert, SiegeConfirmed, AbandonClaimMissing, NotYourClaim, DeleteTopLevelClaim, AbandonSuccess, CantGrantThatPermission, GrantPermissionNoClaim, GrantPermissionConfirmation, ManageUniversalPermissionsInstruction, ManageOneClaimPermissionsInstruction, CollectivePublic, BuildPermission, ContainersPermission, AccessPermission, PermissionsPermission, LocationCurrentClaim, LocationAllClaims, PvPImmunityStart, SiegeNoDrop, DonateItemsInstruction, ChestFull, DonationSuccess, PlayerTooCloseForFire, TooDeepToClaim, ChestClaimConfirmation, AutomaticClaimNotification, UnprotectedChestWarning, ThatPlayerPvPImmune, CantFightWhileImmune, NoDamageClaimedEntity, ShovelBasicClaimMode, RemainingBlocks, CreativeBasicsVideo2, SurvivalBasicsVideo2, TrappedChatKeyword, TrappedInstructions, PvPNoDrop, SiegeNoTeleport, BesiegedNoTeleport, SiegeNoContainers, PvPNoContainers, PvPImmunityEnd, NoBedPermission, NoWildernessBuckets, NoLavaNearOtherPlayer, TooFarAway, BlockNotClaimed, BlockClaimed, SiegeNoShovel, RestoreNaturePlayerInChunk, NoCreateClaimPermission, ResizeNeedMoreBlocks, NoCreativeUnClaim, ClaimResizeSuccess, ResizeFailOverlap, ResizeStart, ResizeFailOverlapSubdivision, SubdivisionStart, CreateSubdivisionOverlap, SubdivisionSuccess, CreateClaimFailOverlap, CreateClaimFailOverlapOtherPlayer, ClaimsDisabledWorld, ClaimStart, NewClaimTooNarrow, CreateClaimInsufficientBlocks, AbandonClaimAdvertisement, CreateClaimFailOverlapShort, CreateClaimSuccess, SiegeWinDoorsOpen, RescueAbortedMoved, SiegeDoorsLockedEjection, NoModifyDuringSiege, OnlyOwnersModifyClaims, NoBuildUnderSiege, NoBuildPvP, NoBuildPermission, NonSiegeMaterial, NoOwnerBuildUnderSiege, NoAccessPermission, NoContainersSiege, NoContainersPermission, OwnerNameForAdminClaims, ClaimTooSmallForEntities, TooManyEntitiesInClaim, YouHaveNoClaims, ConfirmFluidRemoval, AutoBanNotify, AdjustGroupBlocksSuccess, InvalidPermissionID, HowToClaimRegex, NoBuildOutsideClaims, PlayerOfflineTime, BuildingOutsideClaims, TrappedWontWorkHere, CommandBannedInPvP, UnclaimCleanupWarning, BuySellNotConfigured, NoTeleportPvPCombat, NoTNTDamageAboveSeaLevel, NoTNTDamageClaims, IgnoreClaimsAdvertisement, NoPermissionForCommand, ClaimsListNoPermission, ExplosivesDisabled, ExplosivesEnabled, ClaimExplosivesAdvertisement, PlayerInPvPSafeZone, NoPistonsOutsideClaims, SoftMuted, UnSoftMuted, DropUnlockAdvertisement, PickupBlockedExplanation, DropUnlockConfirmation, AdvertiseACandACB, AdvertiseAdminClaims, AdvertiseACB, NotYourPet, PetGiveawayConfirmation, PetTransferCancellation, ReadyToTransferPet, AvoidGriefClaimLand, BecomeMayor, ClaimCreationFailedOverClaimCountLimit, CreateClaimFailOverlapRegion, ResizeFailOverlapRegion, NoBuildPortalPermission, ShowNearbyClaims, NoChatUntilMove, SiegeImmune, SetClaimBlocksSuccess, IgnoreConfirmation, NotIgnoringPlayer, UnIgnoreConfirmation, SeparateConfirmation, UnSeparateConfirmation, NotIgnoringAnyone, TrustListHeader, Manage, Build, Containers, Access, StartBlockMath, ClaimsListHeader, ContinueBlockMath, EndBlockMath, NoClaimDuringPvP, UntrustAllOwnerOnly, ManagersDontUntrustManagers, BookAuthor, BookTitle, BookIntro, BookDisabledChestClaims, BookUsefulCommands, BookLink, BookTools, ResizeClaimTooNarrow, ResizeClaimInsufficientArea + RespectingClaims, IgnoringClaims, SuccessfulAbandon, RestoreNatureActivate, RestoreNatureAggressiveActivate, FillModeActive, TransferClaimPermission, TransferClaimMissing, TransferClaimAdminOnly, PlayerNotFound2, TransferTopLevel, TransferSuccess, TrustListNoClaim, ClearPermsOwnerOnly, UntrustIndividualAllClaims, UntrustEveryoneAllClaims, NoPermissionTrust, ClearPermissionsOneClaim, UntrustIndividualSingleClaim, OnlySellBlocks, BlockPurchaseCost, ClaimBlockLimit, InsufficientFunds, PurchaseConfirmation, OnlyPurchaseBlocks, BlockSaleValue, NotEnoughBlocksForSale, BlockSaleConfirmation, AdminClaimsMode, BasicClaimsMode, SubdivisionMode, SubdivisionVideo2, DeleteClaimMissing, DeletionSubdivisionWarning, DeleteSuccess, CantDeleteAdminClaim, DeleteAllSuccess, NoDeletePermission, AllAdminDeleted, AdjustBlocksSuccess, NotTrappedHere, RescuePending, NonSiegeWorld, AlreadySieging, NotSiegableThere, SiegeTooFarAway, NoSiegeDefenseless, AlreadyUnderSiegePlayer, AlreadyUnderSiegeArea, NoSiegeAdminClaim, SiegeOnCooldown, SiegeAlert, SiegeConfirmed, AbandonClaimMissing, NotYourClaim, DeleteTopLevelClaim, AbandonSuccess, CantGrantThatPermission, GrantPermissionNoClaim, GrantPermissionConfirmation, ManageUniversalPermissionsInstruction, ManageOneClaimPermissionsInstruction, CollectivePublic, BuildPermission, ContainersPermission, AccessPermission, PermissionsPermission, LocationCurrentClaim, LocationAllClaims, PvPImmunityStart, SiegeNoDrop, DonateItemsInstruction, ChestFull, DonationSuccess, PlayerTooCloseForFire, TooDeepToClaim, ChestClaimConfirmation, AutomaticClaimNotification, UnprotectedChestWarning, ThatPlayerPvPImmune, CantFightWhileImmune, NoDamageClaimedEntity, ShovelBasicClaimMode, RemainingBlocks, CreativeBasicsVideo2, SurvivalBasicsVideo2, TrappedChatKeyword, TrappedInstructions, PvPNoDrop, SiegeNoTeleport, BesiegedNoTeleport, SiegeNoContainers, PvPNoContainers, PvPImmunityEnd, NoBedPermission, NoWildernessBuckets, NoLavaNearOtherPlayer, TooFarAway, BlockNotClaimed, BlockClaimed, SiegeNoShovel, RestoreNaturePlayerInChunk, NoCreateClaimPermission, ResizeNeedMoreBlocks, NoCreativeUnClaim, ClaimResizeSuccess, ResizeFailOverlap, ResizeStart, ResizeFailOverlapSubdivision, SubdivisionStart, CreateSubdivisionOverlap, SubdivisionSuccess, CreateClaimFailOverlap, CreateClaimFailOverlapOtherPlayer, ClaimsDisabledWorld, ClaimStart, NewClaimTooNarrow, CreateClaimInsufficientBlocks, AbandonClaimAdvertisement, CreateClaimFailOverlapShort, CreateClaimSuccess, SiegeWinDoorsOpen, RescueAbortedMoved, SiegeDoorsLockedEjection, NoModifyDuringSiege, OnlyOwnersModifyClaims, NoBuildUnderSiege, NoBuildPvP, NoBuildPermission, NonSiegeMaterial, NoOwnerBuildUnderSiege, NoAccessPermission, NoContainersSiege, NoContainersPermission, OwnerNameForAdminClaims, ClaimTooSmallForEntities, TooManyEntitiesInClaim, YouHaveNoClaims, ConfirmFluidRemoval, AutoBanNotify, AdjustGroupBlocksSuccess, InvalidPermissionID, HowToClaimRegex, NoBuildOutsideClaims, PlayerOfflineTime, BuildingOutsideClaims, TrappedWontWorkHere, CommandBannedInPvP, UnclaimCleanupWarning, BuySellNotConfigured, NoTeleportPvPCombat, NoTNTDamageAboveSeaLevel, NoTNTDamageClaims, IgnoreClaimsAdvertisement, NoPermissionForCommand, ClaimsListNoPermission, ExplosivesDisabled, ExplosivesEnabled, ClaimExplosivesAdvertisement, PlayerInPvPSafeZone, NoPistonsOutsideClaims, SoftMuted, UnSoftMuted, DropUnlockAdvertisement, PickupBlockedExplanation, DropUnlockConfirmation, AdvertiseACandACB, AdvertiseAdminClaims, AdvertiseACB, NotYourPet, PetGiveawayConfirmation, PetTransferCancellation, ReadyToTransferPet, AvoidGriefClaimLand, BecomeMayor, ClaimCreationFailedOverClaimCountLimit, CreateClaimFailOverlapRegion, ResizeFailOverlapRegion, NoBuildPortalPermission, ShowNearbyClaims, NoChatUntilMove, SiegeImmune, SetClaimBlocksSuccess, IgnoreConfirmation, NotIgnoringPlayer, UnIgnoreConfirmation, SeparateConfirmation, UnSeparateConfirmation, NotIgnoringAnyone, TrustListHeader, Manage, Build, Containers, Access, StartBlockMath, ClaimsListHeader, ContinueBlockMath, EndBlockMath, NoClaimDuringPvP, UntrustAllOwnerOnly, ManagersDontUntrustManagers, BookAuthor, BookTitle, BookIntro, BookDisabledChestClaims, BookUsefulCommands, BookLink, BookTools, ResizeClaimTooNarrow, ResizeClaimInsufficientArea, NoProfanity } diff --git a/src/me/ryanhamshire/GriefPrevention/PlayerData.java b/src/me/ryanhamshire/GriefPrevention/PlayerData.java index d2cfa80..256f02f 100644 --- a/src/me/ryanhamshire/GriefPrevention/PlayerData.java +++ b/src/me/ryanhamshire/GriefPrevention/PlayerData.java @@ -143,6 +143,9 @@ public class PlayerData //true means invisible (admin-forced ignore), false means player-created ignore public ConcurrentHashMap ignoredPlayers = new ConcurrentHashMap(); public boolean ignoreListChanged = false; + + //profanity warning, once per play session + boolean profanityWarned = false; //whether or not this player is "in" pvp combat public boolean inPvpCombat() diff --git a/src/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java b/src/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java index 1ae18f8..19f3d56 100644 --- a/src/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java +++ b/src/me/ryanhamshire/GriefPrevention/PlayerEventHandler.java @@ -87,6 +87,9 @@ class PlayerEventHandler implements Listener //regex pattern for the "how do i claim land?" scanner private Pattern howToClaimPattern = null; + //matcher for banned words + private WordFinder bannedWordFinder = new WordFinder(GriefPrevention.instance.dataStore.loadBannedWords()); + //typical constructor, yawn PlayerEventHandler(DataStore dataStore, GriefPrevention plugin) { @@ -138,6 +141,34 @@ class PlayerEventHandler implements Listener GriefPrevention.AddLogEntry(notificationMessage, CustomLogEntryTypes.Debug, true); } + //troll and excessive profanity filter + else if(!player.hasPermission("griefprevention.spam") && this.bannedWordFinder.hasMatch(message)) + { + //limit recipients to sender + recipients.clear(); + recipients.add(player); + + //if player not new warn for the first infraction per play session. + if(player.hasAchievement(Achievement.MINE_WOOD)) + { + PlayerData playerData = GriefPrevention.instance.dataStore.getPlayerData(player.getUniqueId()); + if(!playerData.profanityWarned) + { + playerData.profanityWarned = true; + GriefPrevention.sendMessage(player, TextMode.Err, Messages.NoProfanity); + event.setCancelled(true); + return; + } + } + + //otherwise assume chat troll and mute all chat from this sender until an admin says otherwise + else + { + GriefPrevention.AddLogEntry("Auto-muted new player " + player.getName() + " for profanity shortly after join. Use /SoftMute to undo."); + GriefPrevention.instance.dataStore.toggleSoftMute(player.getUniqueId()); + } + } + //remaining messages else { diff --git a/src/me/ryanhamshire/GriefPrevention/Tests.java b/src/me/ryanhamshire/GriefPrevention/Tests.java new file mode 100644 index 0000000..dc52ff9 --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/Tests.java @@ -0,0 +1,60 @@ +package me.ryanhamshire.GriefPrevention; + +import static org.junit.Assert.*; + +import java.util.Arrays; + +import org.junit.Test; + +public class Tests +{ + @Test + public void TrivialTest() + { + assertTrue(true); + } + + @Test + public void WordFinder_BeginningMiddleEnd() + { + WordFinder finder = new WordFinder(Arrays.asList("alpha", "beta", "gamma")); + assertTrue(finder.hasMatch("alpha")); + assertTrue(finder.hasMatch("alpha etc")); + assertTrue(finder.hasMatch("etc alpha etc")); + assertTrue(finder.hasMatch("etc alpha")); + + assertTrue(finder.hasMatch("beta")); + assertTrue(finder.hasMatch("beta etc")); + assertTrue(finder.hasMatch("etc beta etc")); + assertTrue(finder.hasMatch("etc beta")); + + assertTrue(finder.hasMatch("gamma")); + assertTrue(finder.hasMatch("gamma etc")); + assertTrue(finder.hasMatch("etc gamma etc")); + assertTrue(finder.hasMatch("etc gamma")); + } + + @Test + public void WordFinder_Casing() + { + WordFinder finder = new WordFinder(Arrays.asList("aLPhA")); + assertTrue(finder.hasMatch("alpha")); + assertTrue(finder.hasMatch("aLPhA")); + assertTrue(finder.hasMatch("AlpHa")); + assertTrue(finder.hasMatch("ALPHA")); + } + + @Test + public void WordFinder_Punctuation() + { + WordFinder finder = new WordFinder(Arrays.asList("alpha")); + assertTrue(finder.hasMatch("What do you think,alpha?")); + } + + @Test + public void WordFinder_NoMatch() + { + WordFinder finder = new WordFinder(Arrays.asList("alpha")); + assertFalse(finder.hasMatch("Unit testing is smart.")); + } +} \ No newline at end of file diff --git a/src/me/ryanhamshire/GriefPrevention/WordFinder.java b/src/me/ryanhamshire/GriefPrevention/WordFinder.java new file mode 100644 index 0000000..7c3191e --- /dev/null +++ b/src/me/ryanhamshire/GriefPrevention/WordFinder.java @@ -0,0 +1,34 @@ +package me.ryanhamshire.GriefPrevention; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class WordFinder +{ + private Pattern pattern; + + WordFinder(List wordsToFind) + { + StringBuilder patternBuilder = new StringBuilder(); + for(String word : wordsToFind) + { + patternBuilder.append("|(([^\\w]|^)" + Pattern.quote(word) + "([^\\w]|$))"); + } + + String patternString = patternBuilder.toString(); + if(patternString.length() > 1) + { + //trim extraneous leading pipe (|) + patternString = patternString.substring(1); + } + + this.pattern = Pattern.compile(patternString, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); + } + + boolean hasMatch(String input) + { + Matcher matcher = this.pattern.matcher(input); + return matcher.find(); + } +}