mirror of
https://github.com/CitizensDev/Citizens2.git
synced 2024-11-27 21:29:14 +01:00
Switch Requirements to CommandAnnotationProcessor
This commit is contained in:
parent
b7cf0109da
commit
9b15542548
@ -22,6 +22,7 @@ import net.citizensnpcs.command.CommandContext;
|
||||
import net.citizensnpcs.command.CommandManager;
|
||||
import net.citizensnpcs.command.CommandManager.CommandInfo;
|
||||
import net.citizensnpcs.command.Injector;
|
||||
import net.citizensnpcs.command.RequirementsProcessor;
|
||||
import net.citizensnpcs.command.command.AdminCommands;
|
||||
import net.citizensnpcs.command.command.EditorCommands;
|
||||
import net.citizensnpcs.command.command.HelpCommands;
|
||||
@ -267,6 +268,7 @@ public class Citizens extends JavaPlugin implements CitizensPlugin {
|
||||
commands.register(TemplateCommands.class);
|
||||
commands.register(TraitCommands.class);
|
||||
commands.register(WaypointCommands.class);
|
||||
commands.registerAnnotationProcessor(new RequirementsProcessor());
|
||||
}
|
||||
|
||||
private void registerScriptHelpers() {
|
||||
|
@ -2,8 +2,13 @@ package net.citizensnpcs.command;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
|
||||
import net.citizensnpcs.command.exception.CommandException;
|
||||
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
public interface CommandAnnotationProcessor<T extends Annotation> {
|
||||
void process(CommandSender sender, CommandContext context, T instance, Object[] args);
|
||||
public interface CommandAnnotationProcessor {
|
||||
Class<? extends Annotation> getAnnotationClass();
|
||||
|
||||
void process(CommandSender sender, CommandContext context, Annotation instance, Object[] args)
|
||||
throws CommandException;
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
package net.citizensnpcs.command;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
@ -14,33 +14,30 @@ import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.trait.MobType;
|
||||
import net.citizensnpcs.api.trait.trait.Owner;
|
||||
import net.citizensnpcs.command.exception.CommandException;
|
||||
import net.citizensnpcs.command.exception.CommandUsageException;
|
||||
import net.citizensnpcs.command.exception.NoPermissionsException;
|
||||
import net.citizensnpcs.command.exception.RequirementMissingException;
|
||||
import net.citizensnpcs.command.exception.ServerCommandException;
|
||||
import net.citizensnpcs.command.exception.UnhandledCommandException;
|
||||
import net.citizensnpcs.command.exception.WrappedCommandException;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
import net.citizensnpcs.util.Messaging;
|
||||
import net.citizensnpcs.util.StringHelper;
|
||||
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.ConsoleCommandSender;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
public class CommandManager {
|
||||
|
||||
private final Map<Class<? extends Annotation>, CommandAnnotationProcessor> annotationProcessors = Maps
|
||||
.newHashMap();
|
||||
|
||||
/*
|
||||
* Mapping of commands (including aliases) with a description. Root commands
|
||||
* are stored under a key of null, whereas child commands are cached under
|
||||
@ -51,11 +48,9 @@ public class CommandManager {
|
||||
|
||||
// Stores the injector used to getInstance.
|
||||
private Injector injector;
|
||||
|
||||
// Used to store the instances associated with a method.
|
||||
private final Map<Method, Object> instances = new HashMap<Method, Object>();
|
||||
|
||||
private final Map<Method, Requirements> requirements = new HashMap<Method, Requirements>();
|
||||
private final ListMultimap<Method, Annotation> registeredAnnotations = ArrayListMultimap.create();
|
||||
|
||||
private final Set<Method> serverCommands = new HashSet<Method>();
|
||||
|
||||
@ -130,9 +125,9 @@ public class CommandManager {
|
||||
|
||||
methodArgs[0] = context;
|
||||
|
||||
Requirements cmdRequirements = requirements.get(method);
|
||||
if (cmdRequirements != null) {
|
||||
processRequirements(sender, methodArgs, context, cmdRequirements);
|
||||
for (Annotation annotation : registeredAnnotations.get(method)) {
|
||||
CommandAnnotationProcessor processor = annotationProcessors.get(annotation.getClass());
|
||||
processor.process(sender, context, annotation, methodArgs);
|
||||
}
|
||||
|
||||
Object instance = instances.get(method);
|
||||
@ -196,7 +191,7 @@ public class CommandManager {
|
||||
Command commandAnnotation = entry.getValue().getAnnotation(Command.class);
|
||||
if (commandAnnotation == null)
|
||||
continue;
|
||||
return new CommandInfo(commandAnnotation, requirements.get(entry.getValue()));
|
||||
return new CommandInfo(commandAnnotation);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -220,7 +215,7 @@ public class CommandManager {
|
||||
Command commandAnnotation = entry.getValue().getAnnotation(Command.class);
|
||||
if (commandAnnotation == null)
|
||||
continue;
|
||||
cmds.add(new CommandInfo(commandAnnotation, requirements.get(entry.getValue())));
|
||||
cmds.add(new CommandInfo(commandAnnotation));
|
||||
}
|
||||
return cmds;
|
||||
}
|
||||
@ -264,49 +259,6 @@ public class CommandManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void processRequirements(CommandSender sender, Object[] methodArgs, CommandContext context,
|
||||
Requirements cmdRequirements) throws RequirementMissingException {
|
||||
NPC npc = (methodArgs.length >= 3 && methodArgs[2] instanceof NPC) ? (NPC) methodArgs[2] : null;
|
||||
|
||||
// Requirements
|
||||
if (cmdRequirements.selected()) {
|
||||
boolean canRedefineSelected = context.hasValueFlag("id") && sender.hasPermission("npc.select");
|
||||
String error = Messaging.tr(Messages.COMMAND_MUST_HAVE_SELECTED);
|
||||
if (canRedefineSelected) {
|
||||
npc = CitizensAPI.getNPCRegistry().getById(context.getFlagInteger("id"));
|
||||
if (npc == null)
|
||||
error += ' ' + Messaging.tr(Messages.COMMAND_ID_NOT_FOUND, context.getFlagInteger("id"));
|
||||
}
|
||||
if (npc == null)
|
||||
throw new RequirementMissingException(error);
|
||||
}
|
||||
|
||||
if (cmdRequirements.ownership() && npc != null && !sender.hasPermission("citizens.admin")
|
||||
&& !npc.getTrait(Owner.class).isOwnedBy(sender))
|
||||
throw new RequirementMissingException(Messaging.tr(Messages.COMMAND_MUST_BE_OWNER));
|
||||
|
||||
if (npc != null) {
|
||||
for (Class<? extends Trait> clazz : cmdRequirements.traits()) {
|
||||
if (!npc.hasTrait(clazz))
|
||||
throw new RequirementMissingException(Messaging.tr(Messages.COMMAND_MISSING_TRAIT,
|
||||
clazz.getSimpleName()));
|
||||
}
|
||||
}
|
||||
|
||||
if (npc != null) {
|
||||
Set<EntityType> types = Sets.newEnumSet(Arrays.asList(cmdRequirements.types()), EntityType.class);
|
||||
if (types.contains(EntityType.UNKNOWN))
|
||||
types = EnumSet.allOf(EntityType.class);
|
||||
types.removeAll(Sets.newHashSet(cmdRequirements.excludedTypes()));
|
||||
|
||||
EntityType type = npc.getTrait(MobType.class).getType();
|
||||
if (!types.contains(type)) {
|
||||
throw new RequirementMissingException(Messaging.tr(
|
||||
Messages.COMMAND_REQUIREMENTS_INVALID_MOB_TYPE, type.getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a class that contains commands (methods annotated with
|
||||
* {@link Command}). If no dependency {@link Injector} is specified, then
|
||||
@ -322,6 +274,10 @@ public class CommandManager {
|
||||
registerMethods(clazz, null);
|
||||
}
|
||||
|
||||
public void registerAnnotationProcessor(CommandAnnotationProcessor processor) {
|
||||
annotationProcessors.put(processor.getAnnotationClass(), processor);
|
||||
}
|
||||
|
||||
/*
|
||||
* Register the methods of a class. This will automatically construct
|
||||
* instances as necessary.
|
||||
@ -347,18 +303,31 @@ public class CommandManager {
|
||||
}
|
||||
}
|
||||
|
||||
Requirements cmdRequirements = null;
|
||||
if (method.getDeclaringClass().isAnnotationPresent(Requirements.class))
|
||||
cmdRequirements = method.getDeclaringClass().getAnnotation(Requirements.class);
|
||||
List<Annotation> annotations = Lists.newArrayList();
|
||||
for (Annotation annotation : method.getDeclaringClass().getAnnotations()) {
|
||||
Class<? extends Annotation> annotationClass = annotation.getClass();
|
||||
if (annotationProcessors.containsKey(annotationClass))
|
||||
annotations.add(annotation);
|
||||
}
|
||||
for (Annotation annotation : method.getAnnotations()) {
|
||||
Class<? extends Annotation> annotationClass = annotation.getClass();
|
||||
if (!annotationProcessors.containsKey(annotationClass))
|
||||
continue;
|
||||
Iterator<Annotation> itr = annotations.iterator();
|
||||
while (itr.hasNext()) {
|
||||
Annotation previous = itr.next();
|
||||
if (previous.getClass() == annotationClass) {
|
||||
itr.remove();
|
||||
}
|
||||
}
|
||||
annotations.add(annotation);
|
||||
}
|
||||
|
||||
if (method.isAnnotationPresent(Requirements.class))
|
||||
cmdRequirements = method.getAnnotation(Requirements.class);
|
||||
if (annotations.size() > 0)
|
||||
registeredAnnotations.putAll(method, annotations);
|
||||
|
||||
if (requirements != null)
|
||||
requirements.put(method, cmdRequirements);
|
||||
|
||||
Class<?> senderClass = method.getParameterTypes()[1];
|
||||
if (senderClass == CommandSender.class)
|
||||
Class<?>[] parameterTypes = method.getParameterTypes();
|
||||
if (parameterTypes.length <= 1 || parameterTypes[1] == CommandSender.class)
|
||||
serverCommands.add(method);
|
||||
|
||||
// We want to be able invoke with an instance
|
||||
@ -378,11 +347,9 @@ public class CommandManager {
|
||||
|
||||
public static class CommandInfo {
|
||||
private final Command commandAnnotation;
|
||||
private final Requirements requirements;
|
||||
|
||||
public CommandInfo(Command commandAnnotation, Requirements requirements) {
|
||||
public CommandInfo(Command commandAnnotation) {
|
||||
this.commandAnnotation = commandAnnotation;
|
||||
this.requirements = requirements;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -408,10 +375,6 @@ public class CommandManager {
|
||||
return commandAnnotation;
|
||||
}
|
||||
|
||||
public Requirements getRequirements() {
|
||||
return requirements;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 31 + ((commandAnnotation == null) ? 0 : commandAnnotation.hashCode());
|
||||
|
@ -0,0 +1,71 @@
|
||||
package net.citizensnpcs.command;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.trait.MobType;
|
||||
import net.citizensnpcs.api.trait.trait.Owner;
|
||||
import net.citizensnpcs.command.exception.CommandException;
|
||||
import net.citizensnpcs.command.exception.RequirementMissingException;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
import net.citizensnpcs.util.Messaging;
|
||||
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.EntityType;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
public class RequirementsProcessor implements CommandAnnotationProcessor {
|
||||
@Override
|
||||
public Class<? extends Annotation> getAnnotationClass() {
|
||||
return Requirements.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(CommandSender sender, CommandContext context, Annotation instance, Object[] methodArgs)
|
||||
throws CommandException {
|
||||
Requirements requirements = (Requirements) instance;
|
||||
NPC npc = (methodArgs.length >= 3 && methodArgs[2] instanceof NPC) ? (NPC) methodArgs[2] : null;
|
||||
|
||||
// Requirements
|
||||
if (requirements.selected()) {
|
||||
boolean canRedefineSelected = context.hasValueFlag("id") && sender.hasPermission("npc.select");
|
||||
String error = Messaging.tr(Messages.COMMAND_MUST_HAVE_SELECTED);
|
||||
if (canRedefineSelected) {
|
||||
npc = CitizensAPI.getNPCRegistry().getById(context.getFlagInteger("id"));
|
||||
if (npc == null)
|
||||
error += ' ' + Messaging.tr(Messages.COMMAND_ID_NOT_FOUND, context.getFlagInteger("id"));
|
||||
}
|
||||
if (npc == null)
|
||||
throw new RequirementMissingException(error);
|
||||
}
|
||||
|
||||
if (requirements.ownership() && npc != null && !sender.hasPermission("citizens.admin")
|
||||
&& !npc.getTrait(Owner.class).isOwnedBy(sender))
|
||||
throw new RequirementMissingException(Messaging.tr(Messages.COMMAND_MUST_BE_OWNER));
|
||||
|
||||
if (npc == null)
|
||||
return;
|
||||
for (Class<? extends Trait> clazz : requirements.traits()) {
|
||||
if (!npc.hasTrait(clazz))
|
||||
throw new RequirementMissingException(Messaging.tr(Messages.COMMAND_MISSING_TRAIT,
|
||||
clazz.getSimpleName()));
|
||||
}
|
||||
|
||||
Set<EntityType> types = Sets.newEnumSet(Arrays.asList(requirements.types()), EntityType.class);
|
||||
if (types.contains(EntityType.UNKNOWN))
|
||||
types = EnumSet.allOf(EntityType.class);
|
||||
types.removeAll(Sets.newHashSet(requirements.excludedTypes()));
|
||||
|
||||
EntityType type = npc.getTrait(MobType.class).getType();
|
||||
if (!types.contains(type)) {
|
||||
throw new RequirementMissingException(Messaging.tr(
|
||||
Messages.COMMAND_REQUIREMENTS_INVALID_MOB_TYPE, type.getName()));
|
||||
}
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@ import net.minecraft.server.v1_4_5.EntityLiving;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
public class CitizensNavigator implements Navigator, Runnable {
|
||||
private final NavigatorParameters defaultParams = new NavigatorParameters()
|
||||
@ -97,7 +98,7 @@ public class CitizensNavigator implements Navigator, Runnable {
|
||||
|
||||
public void onSpawn() {
|
||||
if (defaultParams.baseSpeed() == UNINITIALISED_SPEED)
|
||||
defaultParams.baseSpeed(NMS.getSpeedFor(npc.getHandle()));
|
||||
defaultParams.baseSpeed(NMS.getSpeedFor(npc));
|
||||
updatePathfindingRange();
|
||||
if (!updatedAvoidWater) {
|
||||
boolean defaultAvoidWater = npc.getHandle().getNavigation().a();
|
||||
@ -177,8 +178,9 @@ public class CitizensNavigator implements Navigator, Runnable {
|
||||
localParams = defaultParams;
|
||||
stationaryTicks = 0;
|
||||
if (npc.isSpawned()) {
|
||||
EntityLiving entity = npc.getHandle();
|
||||
entity.motX = entity.motY = entity.motZ = 0F;
|
||||
Vector velocity = npc.getBukkitEntity().getVelocity();
|
||||
velocity.setX(0).setY(0).setZ(0);
|
||||
npc.getBukkitEntity().setVelocity(velocity);
|
||||
}
|
||||
}
|
||||
|
||||
@ -216,17 +218,17 @@ public class CitizensNavigator implements Navigator, Runnable {
|
||||
private boolean updateStationaryStatus() {
|
||||
if (localParams.stationaryTicks() < 0)
|
||||
return false;
|
||||
EntityLiving handle = npc.getHandle();
|
||||
if (lastX == (int) handle.locX && lastY == (int) handle.locY && lastZ == (int) handle.locZ) {
|
||||
Location handle = npc.getBukkitEntity().getLocation();
|
||||
if (lastX == handle.getBlockX() && lastY == handle.getBlockY() && lastZ == handle.getBlockZ()) {
|
||||
if (++stationaryTicks >= localParams.stationaryTicks()) {
|
||||
stopNavigating(CancelReason.STUCK);
|
||||
return true;
|
||||
}
|
||||
} else
|
||||
stationaryTicks = 0;
|
||||
lastX = (int) handle.locX;
|
||||
lastY = (int) handle.locY;
|
||||
lastZ = (int) handle.locZ;
|
||||
lastX = handle.getBlockX();
|
||||
lastY = handle.getBlockY();
|
||||
lastZ = handle.getBlockZ();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ package net.citizensnpcs.trait;
|
||||
import net.citizensnpcs.api.persistence.Persist;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
|
||||
import org.bukkit.craftbukkit.v1_4_5.entity.CraftEntity;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
public class Gravity extends Trait implements Toggleable {
|
||||
@Persist
|
||||
@ -21,8 +21,9 @@ public class Gravity extends Trait implements Toggleable {
|
||||
public void run() {
|
||||
if (!npc.isSpawned() || !enabled)
|
||||
return;
|
||||
net.minecraft.server.v1_4_5.Entity entity = ((CraftEntity) npc.getBukkitEntity()).getHandle();
|
||||
entity.motY = Math.max(0, entity.motY);
|
||||
Vector vector = npc.getBukkitEntity().getVelocity();
|
||||
vector.setY(0);
|
||||
npc.getBukkitEntity().setVelocity(vector);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -8,6 +8,7 @@ import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.npc.CitizensNPC;
|
||||
import net.minecraft.server.v1_4_5.ControllerLook;
|
||||
import net.minecraft.server.v1_4_5.DamageSource;
|
||||
@ -155,8 +156,8 @@ public class NMS {
|
||||
return f;
|
||||
}
|
||||
|
||||
public static float getSpeedFor(EntityLiving from) {
|
||||
EntityType entityType = from.getBukkitEntity().getType();
|
||||
public static float getSpeedFor(NPC npc) {
|
||||
EntityType entityType = npc.getBukkitEntity().getType();
|
||||
Float cached = MOVEMENT_SPEEDS.get(entityType);
|
||||
if (cached != null)
|
||||
return cached;
|
||||
@ -165,7 +166,7 @@ public class NMS {
|
||||
return DEFAULT_SPEED;
|
||||
}
|
||||
try {
|
||||
float speed = SPEED_FIELD.getFloat(from);
|
||||
float speed = SPEED_FIELD.getFloat(((CraftEntity)npc.getBukkitEntity()).getHandle());
|
||||
MOVEMENT_SPEEDS.put(entityType, speed);
|
||||
return speed;
|
||||
} catch (IllegalAccessException ex) {
|
||||
@ -221,7 +222,7 @@ public class NMS {
|
||||
}
|
||||
|
||||
public static org.bukkit.entity.Entity spawnCustomEntity(org.bukkit.World world, Location at,
|
||||
Class<? extends Entity> clazz, EntityType type) {
|
||||
Class<?> clazz, EntityType type) {
|
||||
World handle = ((CraftWorld) world).getHandle();
|
||||
Entity entity = null;
|
||||
try {
|
||||
|
@ -10,6 +10,8 @@ import org.bukkit.ChatColor;
|
||||
public class StringHelper {
|
||||
private static Pattern COLOR_MATCHER;
|
||||
|
||||
private static String GROUP = ChatColor.COLOR_CHAR + "$1";
|
||||
|
||||
public static String capitalize(Object string) {
|
||||
String capitalize = string.toString();
|
||||
return capitalize.replaceFirst(String.valueOf(capitalize.charAt(0)),
|
||||
@ -70,8 +72,6 @@ public class StringHelper {
|
||||
return matcher.replaceAll(GROUP);
|
||||
}
|
||||
|
||||
private static String GROUP = ChatColor.COLOR_CHAR + "$1";
|
||||
|
||||
public static String wrap(Object string) {
|
||||
return wrap(string, parseColors(Setting.MESSAGE_COLOUR.asString()));
|
||||
}
|
||||
|
@ -27,6 +27,8 @@ public class Util {
|
||||
private Util() {
|
||||
}
|
||||
|
||||
private static Class<?> RNG_CLASS = null;
|
||||
|
||||
public static void assumePose(org.bukkit.entity.Entity entity, float yaw, float pitch) {
|
||||
EntityLiving handle = ((CraftLivingEntity) entity).getHandle();
|
||||
NMS.look(handle, yaw, pitch);
|
||||
@ -65,6 +67,14 @@ public class Util {
|
||||
NMS.look(handle, (float) yaw - 90, (float) pitch);
|
||||
}
|
||||
|
||||
public static Random getFastRandom() {
|
||||
try {
|
||||
return (Random) RNG_CLASS.newInstance();
|
||||
} catch (Exception e) {
|
||||
return new Random();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isLoaded(Location location) {
|
||||
if (location.getWorld() == null)
|
||||
return false;
|
||||
@ -135,16 +145,6 @@ public class Util {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Random getFastRandom() {
|
||||
try {
|
||||
return (Random) RNG_CLASS.newInstance();
|
||||
} catch (Exception e) {
|
||||
return new Random();
|
||||
}
|
||||
}
|
||||
|
||||
private static Class<?> RNG_CLASS = null;
|
||||
static {
|
||||
try {
|
||||
RNG_CLASS = Class.forName("org.uncommons.maths.random.XORShiftRNG");
|
||||
|
Loading…
Reference in New Issue
Block a user