Added profanity/troll filter.
This commit is contained in:
parent
2a66ad3d01
commit
f5318e3b19
|
|
@ -19,6 +19,7 @@
|
||||||
package me.ryanhamshire.GriefPrevention;
|
package me.ryanhamshire.GriefPrevention;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.regex.Pattern;
|
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 configFilePath = dataLayerFolderPath + File.separator + "config.yml";
|
||||||
final static String messagesFilePath = dataLayerFolderPath + File.separator + "messages.yml";
|
final static String messagesFilePath = dataLayerFolderPath + File.separator + "messages.yml";
|
||||||
final static String softMuteFilePath = dataLayerFolderPath + File.separator + "softMute.txt";
|
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
|
//the latest version of the data schema implemented here
|
||||||
protected static final int latestSchemaVersion = 2;
|
protected static final int latestSchemaVersion = 2;
|
||||||
|
|
@ -211,6 +213,31 @@ public abstract class DataStore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> 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<String>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//updates soft mute map and data file
|
//updates soft mute map and data file
|
||||||
boolean toggleSoftMute(UUID playerID)
|
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.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.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.BookUsefulCommands, "Useful Commands:", null);
|
||||||
|
this.addDefault(defaults, Messages.NoProfanity, "Please moderate your language.", null);
|
||||||
|
|
||||||
//load the config file
|
//load the config file
|
||||||
FileConfiguration config = YamlConfiguration.loadConfiguration(new File(messagesFilePath));
|
FileConfiguration config = YamlConfiguration.loadConfiguration(new File(messagesFilePath));
|
||||||
|
|
|
||||||
|
|
@ -20,5 +20,5 @@ package me.ryanhamshire.GriefPrevention;
|
||||||
|
|
||||||
public enum Messages
|
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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,9 @@ public class PlayerData
|
||||||
public ConcurrentHashMap<UUID, Boolean> ignoredPlayers = new ConcurrentHashMap<UUID, Boolean>();
|
public ConcurrentHashMap<UUID, Boolean> ignoredPlayers = new ConcurrentHashMap<UUID, Boolean>();
|
||||||
public boolean ignoreListChanged = false;
|
public boolean ignoreListChanged = false;
|
||||||
|
|
||||||
|
//profanity warning, once per play session
|
||||||
|
boolean profanityWarned = false;
|
||||||
|
|
||||||
//whether or not this player is "in" pvp combat
|
//whether or not this player is "in" pvp combat
|
||||||
public boolean inPvpCombat()
|
public boolean inPvpCombat()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,9 @@ class PlayerEventHandler implements Listener
|
||||||
//regex pattern for the "how do i claim land?" scanner
|
//regex pattern for the "how do i claim land?" scanner
|
||||||
private Pattern howToClaimPattern = null;
|
private Pattern howToClaimPattern = null;
|
||||||
|
|
||||||
|
//matcher for banned words
|
||||||
|
private WordFinder bannedWordFinder = new WordFinder(GriefPrevention.instance.dataStore.loadBannedWords());
|
||||||
|
|
||||||
//typical constructor, yawn
|
//typical constructor, yawn
|
||||||
PlayerEventHandler(DataStore dataStore, GriefPrevention plugin)
|
PlayerEventHandler(DataStore dataStore, GriefPrevention plugin)
|
||||||
{
|
{
|
||||||
|
|
@ -138,6 +141,34 @@ class PlayerEventHandler implements Listener
|
||||||
GriefPrevention.AddLogEntry(notificationMessage, CustomLogEntryTypes.Debug, true);
|
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
|
//remaining messages
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
60
src/me/ryanhamshire/GriefPrevention/Tests.java
Normal file
60
src/me/ryanhamshire/GriefPrevention/Tests.java
Normal file
|
|
@ -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."));
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/me/ryanhamshire/GriefPrevention/WordFinder.java
Normal file
34
src/me/ryanhamshire/GriefPrevention/WordFinder.java
Normal file
|
|
@ -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<String> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user