Merge remote-tracking branch 'refs/remotes/origin/develop' into update/ssm

This commit is contained in:
Sam 2016-10-01 15:45:05 +01:00
commit 4bfc632fd5
144 changed files with 7695 additions and 4527 deletions

View File

@ -4,10 +4,12 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.google.common.collect.Lists;
@ -275,4 +277,8 @@ public class UtilCollections
return total.toString();
}
public static <T> List<T> unboxPresent(Collection<Optional<T>> optionalList)
{
return optionalList.stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,133 @@
package mineplex.core.common.util;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
public class UtilFuture
{
/**
* Returns a {@link CompletableFuture} which will complete when supplied futures have completed.
* This is a workaround for {@link CompletableFuture#anyOf(CompletableFuture[])} returning void.
*
* @param futures the futures to wait to complete
* @param <T> the type of item(s)
* @return a future which will complete when all supplied futures have completed
*/
public static <T> CompletableFuture<List<T>> sequence(Collection<CompletableFuture<T>> futures)
{
return sequence(futures, Collectors.toList());
}
/**
* Returns a {@link CompletableFuture} which will complete when supplied futures have completed.
* This is a workaround for {@link CompletableFuture#anyOf(CompletableFuture[])} returning void.
*
* @param futures the futures to wait to complete
* @param <R> the collection type
* @param <T> the type of the collection's items
* @return a future which will complete when all supplied futures have completed
*/
public static <R, T> CompletableFuture<R> sequence(Collection<CompletableFuture<T>> futures, Collector<T, ?, R> collector)
{
CompletableFuture<Void> futuresCompletedFuture =
CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()]));
return futuresCompletedFuture.thenApply(v ->
futures.stream().map(CompletableFuture::join).collect(collector));
}
/**
* Returns a {@link CompletableFuture} containing a {@link Multimap} with future values unwrapped.
*
* @param multimap the multimap to transform the values of
* @param <M> the multimap type, values must be a {@link Collection} of {@link CompletableFuture} object.
* @param <K> the multimap key type
* @param <V> the future return type
* @return a future which returns a multimap with unwrapped values
*/
public static <M extends Multimap<K, CompletableFuture<V>>, K, V> CompletableFuture<Multimap<K, V>> sequenceValues(M multimap)
{
CompletableFuture<Map<K, Collection<V>>> sequenced = sequenceValues(multimap.asMap());
return sequenced.thenApply(map ->
{
Multimap<K, V> sequencedMultiMap = HashMultimap.create();
map.forEach(sequencedMultiMap::putAll);
return sequencedMultiMap;
});
}
/**
* Returns a {@link CompletableFuture} containing a {@link Map} with future values unwrapped.
*
* @param map the map to transform the values of
* @param <M> the map type, values must be a {@link Collection} of {@link CompletableFuture} object.
* @param <K> the map key type
* @param <V> the future return type
* @return a future which returns a map with unwrapped values
*/
public static <M extends Map<K, Collection<CompletableFuture<V>>>, K, V> CompletableFuture<Map<K, Collection<V>>> sequenceValues(M map)
{
Map<K, CompletableFuture<List<V>>> seqValues =
map.keySet().stream().collect(Collectors.toMap(Function.identity(), key -> sequence(map.get(key))));
Collection<CompletableFuture<List<V>>> values = seqValues.values();
CompletableFuture<Void> futuresCompleted =
CompletableFuture.allOf(values.toArray(new CompletableFuture[values.size()]));
return futuresCompleted.thenApply(v ->
seqValues.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().join())));
}
/**
* Filters a list using functions which return a {@link CompletableFuture}.
* The returned future is marked as complete once all items have been filtered.
*
* @param list the list of elements to filter
* @param futureFunction a function which returns a future containing whether an element should be filtered
* @param <T> the type of element
* @return a collection containing the filtered elements
*/
public static <T> CompletableFuture<List<T>> filter(List<T> list, Function<T, CompletableFuture<Boolean>> futureFunction)
{
return filter(list, futureFunction, Collectors.toList());
}
/**
* Filters a list using functions which return a {@link CompletableFuture}.
* The returned future is marked as complete once all items have been filtered.
*
* @param list the list of elements to filter
* @param futureFunction a function which returns a future containing whether an element should be filtered
* @param collector the collector used to collect the results
* @param <T> the type of element
* @param <R> the returned collection type
* @return a collection containing the filtered elements
*/
public static <T, R> CompletableFuture<R> filter(List<T> list, Function<T, CompletableFuture<Boolean>> futureFunction, Collector<T, ?, R> collector)
{
Map<T, CompletableFuture<Boolean>> elementFutureMap = list.stream()
.collect(Collectors.toMap(
Function.identity(),
futureFunction,
(u, v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); },
LinkedHashMap::new));
Collection<CompletableFuture<Boolean>> futures = elementFutureMap.values();
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()])).thenApply(aVoid ->
elementFutureMap.entrySet().stream()
.filter(entry -> entry.getValue().join()) // this doesn't block as all futures have completed
.map(Map.Entry::getKey)
.collect(collector));
}
}

View File

@ -1,12 +1,16 @@
package mineplex.core.common.util;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Calendar;
import net.minecraft.server.v1_8_R3.MinecraftServer;
public class UtilTime
{
public static final ZoneId CENTRAL_ZONE = ZoneId.of("America/Chicago"); // This means "CST"
public static final String DATE_FORMAT_NOW = "MM-dd-yyyy HH:mm:ss";
public static final String DATE_FORMAT_DAY = "MM-dd-yyyy";
@ -59,6 +63,56 @@ public class UtilTime
return MinecraftServer.currentTick;
}
/**
* Converts a {@link Timestamp} to a {@link LocalDateTime}.
* This method will only work for timestamp's stored using {@link #CENTRAL_ZONE}, if stored using
* another zone please see: {@link #fromTimestamp(Timestamp, ZoneId)}.
*
* @param timestamp the timestamp to convert
* @return the time
*/
public static LocalDateTime fromTimestamp(Timestamp timestamp)
{
return fromTimestamp(timestamp, CENTRAL_ZONE);
}
/**
* Converts a {@link Timestamp} to a {@link LocalDateTime}.
* The zone supplied should be that of which the timezone was stored using.
*
* @param timestamp the timestamp to convert
* @param zoneId the zone of the timestamp
* @return the time
*/
public static LocalDateTime fromTimestamp(Timestamp timestamp, ZoneId zoneId)
{
return LocalDateTime.ofInstant(timestamp.toInstant(), zoneId);
}
/**
* Converts a {@link LocalDateTime} to a {@link Timestamp}.
* Please not that this will convert using the {@link #CENTRAL_ZONE} timezone.
*
* @param localDateTime the time to convert
* @return the timestamp
*/
public static Timestamp toTimestamp(LocalDateTime localDateTime)
{
return toTimestamp(localDateTime, CENTRAL_ZONE);
}
/**
* Converts a {@link LocalDateTime} to a {@link Timestamp}.
*
* @param localDateTime the time to convert
* @param zoneId the zone to use when converting to a timestamp
* @return the timestamp
*/
public static Timestamp toTimestamp(LocalDateTime localDateTime, ZoneId zoneId)
{
return new Timestamp(localDateTime.atZone(zoneId).toInstant().toEpochMilli());
}
public enum TimeUnit
{
FIT(1),

View File

@ -55,6 +55,7 @@ import net.minecraft.server.v1_8_R3.DataWatcher;
import net.minecraft.server.v1_8_R3.EntityCreeper;
import net.minecraft.server.v1_8_R3.PacketPlayOutEntityMetadata;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.craftbukkit.v1_8_R3.entity.CraftEntity;
import org.bukkit.entity.Entity;
@ -135,6 +136,7 @@ public class BonusManager extends MiniClientPlugin<BonusClientData> implements I
private ThankManager _thankManager;
public boolean _enabled;
private Npc _carlNpc;
private Location _carlLocation;
private AnimationCarl _animation;
private int _visualTick;
@ -175,7 +177,7 @@ public class BonusManager extends MiniClientPlugin<BonusClientData> implements I
updateOffSet();
}
public BonusManager(JavaPlugin plugin, CoreClientManager clientManager, PlayWireManager playWireManager, DonationManager donationManager, PollManager pollManager, NpcManager npcManager, HologramManager hologramManager, StatsManager statsManager, InventoryManager inventoryManager, PetManager petManager, FacebookManager facebookManager, YoutubeManager youtubeManager, GadgetManager gadgetManager, ThankManager thankManager)
public BonusManager(JavaPlugin plugin, Location carlLocation, PlayWireManager playWireManager, CoreClientManager clientManager, DonationManager donationManager, PollManager pollManager, NpcManager npcManager, HologramManager hologramManager, StatsManager statsManager, InventoryManager inventoryManager, PetManager petManager, FacebookManager facebookManager, YoutubeManager youtubeManager, GadgetManager gadgetManager, ThankManager thankManager)
{
super("Bonus", plugin);
_repository = new BonusRepository(plugin, this, donationManager);
@ -197,6 +199,7 @@ public class BonusManager extends MiniClientPlugin<BonusClientData> implements I
_statsManager = statsManager;
_facebookManager = facebookManager;
_youtubeManager = youtubeManager;
_playWireManager = playWireManager;
_powerPlayClubRepository = new PowerPlayClubRepository(plugin, _clientManager);
@ -220,6 +223,10 @@ public class BonusManager extends MiniClientPlugin<BonusClientData> implements I
}
else
{
if(carlLocation != null)
{
_carlNpc.setLocation(carlLocation);
}
_enabled = true;
_animation = new AnimationCarl(_carlNpc.getEntity());
_animation.setRunning(false);
@ -235,8 +242,10 @@ public class BonusManager extends MiniClientPlugin<BonusClientData> implements I
ServerCommandManager.getInstance().registerCommandType("VotifierCommand", VotifierCommand.class, new VoteHandler(this));
updateOffSet();
// updateStreakRecord();
}
@Override
public void addCommands()
{
@ -1101,4 +1110,14 @@ public class BonusManager extends MiniClientPlugin<BonusClientData> implements I
{
return _playWireManager;
}
public Location getCarlLocation()
{
return _carlLocation;
}
public void setCarlLocation(Location carlLocation)
{
_carlLocation = carlLocation;
}
}

View File

@ -5,6 +5,19 @@ package mineplex.core.chatsnap;
*/
public enum MessageType
{
CHAT,
PM
CHAT(0),
PM(1),
PARTY(2);
private final int _id;
MessageType(int id)
{
_id = id;
}
public int getId()
{
return _id;
}
}

View File

@ -1,119 +0,0 @@
package mineplex.core.chatsnap;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import org.bukkit.ChatColor;
import com.google.gson.annotations.SerializedName;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Represents a message sent by a player.
*/
public class Snapshot implements Comparable<Snapshot>
{
@SerializedName("type")
private MessageType _messageType;
@SerializedName("sender")
private UUID _sender;
@SerializedName("recipients")
private Collection<UUID> _recipients;
@SerializedName("message")
private String _message;
@SerializedName("time")
private LocalDateTime _time;
public Snapshot(UUID sender, UUID recipient, String message)
{
this(MessageType.PM, sender, Collections.singletonList(recipient), message, LocalDateTime.now());
}
public Snapshot(UUID sender, Collection<UUID> recipients, String message)
{
this(MessageType.CHAT, sender, recipients, message, LocalDateTime.now());
}
public Snapshot(MessageType messageType, UUID sender, Collection<UUID> recipients, String message, LocalDateTime time)
{
_messageType = messageType;
_sender = checkNotNull(sender);
_recipients = checkNotNull(recipients);
_message = checkNotNull(message);
_time = checkNotNull(time);
if (messageType == MessageType.PM && recipients.size() > 1)
{
throw new IllegalArgumentException("Snapshot type PM may not have more than 1 recipient.");
}
}
public MessageType getMessageType()
{
return _messageType;
}
public UUID getSender()
{
return _sender;
}
public String getMessage()
{
return _message;
}
public Set<UUID> getRecipients()
{
return new HashSet<>(_recipients);
}
public LocalDateTime getSentTime()
{
return _time;
}
@Override
public int compareTo(Snapshot o)
{
return getSentTime().compareTo(o.getSentTime());
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Snapshot that = (Snapshot) o;
return _time == that._time &&
Objects.equals(_sender, that._sender) &&
Objects.equals(_recipients, that._recipients) &&
Objects.equals(_message, that._message);
}
@Override
public int hashCode()
{
return Objects.hash(_sender, _recipients, _message, _time);
}
@Override
public String toString()
{
return "Snapshot{" +
"sender=" + _sender +
", recipients=" + _recipients +
", message='" + ChatColor.stripColor(_message) + '\'' +
", created=" + _time +
'}';
}
}

View File

@ -1,17 +1,22 @@
package mineplex.core.chatsnap;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.stream.Collectors;
import org.bukkit.plugin.java.JavaPlugin;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import mineplex.core.chatsnap.publishing.SnapshotPublisher;
import mineplex.core.report.data.Report;
/**
* Handles temporary storage of {@link Snapshot} instances.
* Handles temporary storage of {@link SnapshotMessage} instances.
*/
public class SnapshotManager
{
@ -19,32 +24,34 @@ public class SnapshotManager
// For an easy work around, we store values as the Key
// For the value we just use some dummy object
// I went with Boolean as it's the smallest data type
private final Cache<Snapshot, Boolean> _snapshots = CacheBuilder.newBuilder()
private final Cache<SnapshotMessage, Boolean> _messages = CacheBuilder.newBuilder()
.concurrencyLevel(4)
.expireAfterWrite(30, TimeUnit.MINUTES)
.expireAfterWrite(3, TimeUnit.MINUTES)
.build();
private final SnapshotPublisher _snapshotPublisher;
private final JavaPlugin _javaPlugin;
private final SnapshotRepository _snapshotRepository;
public SnapshotManager(SnapshotPublisher snapshotPublisher)
public SnapshotManager(JavaPlugin javaPlugin, SnapshotRepository snapshotRepository)
{
_snapshotPublisher = snapshotPublisher;
_javaPlugin = javaPlugin;
_snapshotRepository = snapshotRepository;
}
public SnapshotPublisher getSnapshotPublisher()
public SnapshotRepository getSnapshotRepository()
{
return _snapshotPublisher;
return _snapshotRepository;
}
/**
* Keeps a snapshot in memory temporarily (30 minutes) and then discards it.
* Keeps a message in memory temporarily (30 minutes) and then discards it.
* During this time, other modules (such as the Report module) can access it for their own use.
*
* @param snapshot the snapshot to temporarily store
* @param message the message to temporarily store
*/
public void cacheSnapshot(Snapshot snapshot)
public void cacheMessage(SnapshotMessage message)
{
_snapshots.put(snapshot, true);
_messages.put(message, true);
}
/**
@ -53,25 +60,92 @@ public class SnapshotManager
*
* @return a set containing all snapshots
*/
public Set<Snapshot> getSnapshots()
public Set<SnapshotMessage> getMessages()
{
// The compareTo method in Snapshot will ensure this in chronological order
Set<Snapshot> snapshots = new TreeSet<>();
snapshots.addAll(_snapshots.asMap().keySet());
return snapshots;
// The compareTo method in SnapshotMessage will ensure this in chronological order
Set<SnapshotMessage> messages = new TreeSet<>();
messages.addAll(_messages.asMap().keySet());
return messages;
}
/**
* Gets all instances of {@link Snapshot} which involve a particular user.
* Gets all messages an account is involved in.
* The user may be the sender or recipient of a message.
* Does not include PMs unless sender or receiver is in the exclusions collection.
*
* @param search the user to search for snaps involved in
* @return the snaps that the user is involved in
* @param accountId the account to search for messages involved in
* @param pmIdWhitelist a list of account ids of which to include PMs of
* @return the messages that the account is involved in
*/
public Set<Snapshot> getSnapshots(UUID search)
public Set<SnapshotMessage> getMessagesInvolving(int accountId, Collection<Integer> pmIdWhitelist)
{
return _snapshots.asMap().keySet().stream()
.filter(snapshot -> snapshot.getSender().equals(search) || snapshot.getRecipients().contains(search))
return getMessagesInvolving(accountId).stream()
.filter(message -> includeMessage(message, pmIdWhitelist))
.collect(Collectors.toCollection(TreeSet::new));
}
private boolean includeMessage(SnapshotMessage message, Collection<Integer> pmExclusions)
{
return message.getType() != MessageType.PM ||
pmExclusions.contains(message.getSenderId()) ||
!Collections.disjoint(message.getRecipientIds(), pmExclusions);
}
/**
* Gets all messages an account is involved in.
* The user may be the sender or recipient of a message.
*
* @param accountId the account to search for messages involved in
* @return the messages that the account is involved in
*/
public Set<SnapshotMessage> getMessagesInvolving(int accountId)
{
Set<SnapshotMessage> messagesInvolved = new TreeSet<>();
messagesInvolved.addAll(getMessagesFrom(accountId));
messagesInvolved.addAll(getMessagesReceived(accountId));
return messagesInvolved;
}
/**
* Gets all messages sent by an account.
*
* @param senderId the account to search for messages involved in
* @return the messages that the account is involved in
*/
public Set<SnapshotMessage> getMessagesFrom(int senderId)
{
return _messages.asMap().keySet().stream()
.filter(message -> message.getSenderId() == senderId)
.collect(Collectors.toCollection(TreeSet::new));
}
/**
* Gets all messages received by an account.
*
* @param recipientId the account to search for messages received
* @return the messages that the account is involved in
*/
public Set<SnapshotMessage> getMessagesReceived(int recipientId)
{
return _messages.asMap().keySet().stream()
.filter(message -> message.getRecipientIds().contains(recipientId))
.collect(Collectors.toCollection(TreeSet::new));
}
public CompletableFuture<Integer> saveReportSnapshot(Report report, Collection<SnapshotMessage> messages)
{
return _snapshotRepository
.saveReportSnapshot(report, messages)
.whenComplete((snapshotId, throwable) ->
{
if (throwable == null)
{
report.setSnapshotId(snapshotId);
}
else
{
_javaPlugin.getLogger().log(Level.SEVERE, "Error whilst saving snapshot.", throwable);
}
});
}
}

View File

@ -0,0 +1,126 @@
package mineplex.core.chatsnap;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import org.bukkit.ChatColor;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Represents a message sent by a player.
*/
public class SnapshotMessage implements Comparable<SnapshotMessage>
{
protected Long _id = null;
private final MessageType _messageType;
private final int _senderId;
private final Collection<Integer> _recipientIds;
private final String _message;
private final LocalDateTime _time;
private final Set<Integer> linkedSnapshots = new HashSet<>();
public SnapshotMessage(int senderId, int recipientId, String message)
{
this(MessageType.PM, senderId, Collections.singletonList(recipientId), message, LocalDateTime.now());
}
public SnapshotMessage(MessageType messageType, int senderId, Collection<Integer> recipientIds, String message)
{
this(messageType, senderId, recipientIds, message, LocalDateTime.now());
}
public SnapshotMessage(MessageType messageType, int senderId, Collection<Integer> recipientIds, String message, LocalDateTime time)
{
_messageType = messageType;
_senderId = checkNotNull(senderId);
_recipientIds = checkNotNull(recipientIds);
_message = checkNotNull(message);
_time = checkNotNull(time);
if (messageType == MessageType.PM && recipientIds.size() > 1)
{
throw new IllegalArgumentException("SnapshotMessage type PM may not have more than 1 recipient.");
}
}
public Optional<Long> getId()
{
return Optional.ofNullable(_id);
}
public MessageType getType()
{
return _messageType;
}
public int getSenderId()
{
return _senderId;
}
public String getMessage()
{
return _message;
}
public Set<Integer> getRecipientIds()
{
return new HashSet<>(_recipientIds);
}
public LocalDateTime getSentTime()
{
return _time;
}
public Set<Integer> getLinkedSnapshots()
{
return linkedSnapshots;
}
public void addLinkedSnapshot(int snapshotId)
{
linkedSnapshots.add(snapshotId);
}
@Override
public int compareTo(SnapshotMessage o)
{
return getSentTime().compareTo(o.getSentTime());
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SnapshotMessage that = (SnapshotMessage) o;
return _time == that._time &&
Objects.equals(_senderId, that._senderId) &&
Objects.equals(_recipientIds, that._recipientIds) &&
Objects.equals(_message, that._message);
}
@Override
public int hashCode()
{
return Objects.hash(_senderId, _recipientIds, _message, _time);
}
@Override
public String toString()
{
return "SnapshotMessage{" +
"sender=" + _senderId +
", recipients=" + _recipientIds +
", message='" + ChatColor.stripColor(_message) + '\'' +
", created=" + _time +
'}';
}
}

View File

@ -1,7 +1,6 @@
package mineplex.core.chatsnap;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.bukkit.entity.Player;
@ -11,7 +10,7 @@ import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.plugin.java.JavaPlugin;
import mineplex.core.MiniPlugin;
import mineplex.core.chatsnap.commands.ChatCacheCommand;
import mineplex.core.account.CoreClientManager;
import mineplex.core.message.PrivateMessageEvent;
/**
@ -20,11 +19,13 @@ import mineplex.core.message.PrivateMessageEvent;
public class SnapshotPlugin extends MiniPlugin
{
private final SnapshotManager _snapshotManager;
private final CoreClientManager _clientManager;
public SnapshotPlugin(JavaPlugin plugin, SnapshotManager snapshotManager)
public SnapshotPlugin(JavaPlugin plugin, SnapshotManager snapshotManager, CoreClientManager clientManager)
{
super("ChatSnap", plugin);
_snapshotManager = snapshotManager;
_clientManager = clientManager;
}
public SnapshotManager getSnapshotManager()
@ -35,39 +36,46 @@ public class SnapshotPlugin extends MiniPlugin
@Override
public void addCommands()
{
addCommand(new ChatCacheCommand(this));
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerChat(AsyncPlayerChatEvent e)
{
_snapshotManager.cacheSnapshot(createSnapshot(e));
_snapshotManager.cacheMessage(createSnapshot(e));
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPrivateMessage(PrivateMessageEvent e)
{
_snapshotManager.cacheSnapshot(createSnapshot(e));
_snapshotManager.cacheMessage(createSnapshot(e));
}
public Set<UUID> getUUIDSet(Set<Player> playerSet)
public Set<Integer> getAccountIds(Set<Player> players)
{
return playerSet.stream().map(Player::getUniqueId).collect(Collectors.toSet());
return players.stream().map(_clientManager::getAccountId).collect(Collectors.toSet());
}
public Snapshot createSnapshot(AsyncPlayerChatEvent e)
public SnapshotMessage createSnapshot(AsyncPlayerChatEvent e)
{
UUID senderUUID = e.getPlayer().getUniqueId();
Set<UUID> uuidSet = getUUIDSet(e.getRecipients());
uuidSet.remove(senderUUID);
return new Snapshot(senderUUID, uuidSet, e.getMessage());
MessageType messageType = MessageType.CHAT;
int senderId = _clientManager.getAccountId(e.getPlayer());
Set<Integer> recipientIds = getAccountIds(e.getRecipients());
recipientIds.remove(senderId);
if (e.getFormat().contains("Party"))
{
messageType = MessageType.PARTY;
}
return new SnapshotMessage(messageType, senderId, recipientIds, e.getMessage());
}
public Snapshot createSnapshot(PrivateMessageEvent e)
public SnapshotMessage createSnapshot(PrivateMessageEvent e)
{
Player sender = e.getSender();
Player recipient = e.getRecipient();
int senderId = _clientManager.getAccountId(e.getSender());
int recipientId = _clientManager.getAccountId(e.getRecipient());
String message = e.getMessage();
return new Snapshot(sender.getUniqueId(), recipient.getUniqueId(), message);
return new SnapshotMessage(senderId, recipientId, message);
}
}

View File

@ -0,0 +1,243 @@
package mineplex.core.chatsnap;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Collection;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.logging.Logger;
import mineplex.core.common.util.UtilTime;
import mineplex.core.report.data.Report;
import mineplex.serverdata.database.DBPool;
/**
* Class responsible for publishing snapshots on the website via Redis and a separate Report server.
*/
public class SnapshotRepository
{
public static String getURL(long reportId)
{
return URL_PREFIX + reportId;
}
private static final String URL_PREFIX = "http://report.mineplex.com/chatsnap/view.php?id=";
private static final String INSERT_SNAPSHOT = "INSERT INTO snapshots (creator) VALUES (?);";
private static final String INSERT_MESSAGE = "INSERT INTO snapshotMessages (senderId, `server`, `time`, message, snapshotType) VALUES (?, ?, ?, ?, ?);";
private static final String INSERT_MESSAGE_RECIPIENT = "INSERT INTO snapshotRecipients (messageId, recipientId) VALUES (?, ?);";
private static final String INSERT_MESSAGE_MAPPING = "INSERT INTO snapshotMessageMap (snapshotId, messageId) VALUES (?, ?);";
private final String _serverName;
private final Logger _logger;
public SnapshotRepository(String serverName, Logger logger)
{
_serverName = serverName;
_logger = logger;
}
public CompletableFuture<Integer> saveReportSnapshot(Report report, Collection<SnapshotMessage> messages)
{
return CompletableFuture.supplyAsync(() ->
{
try (Connection connection = DBPool.getAccount().getConnection())
{
int snapshotId = report.getSnapshotId().orElseThrow(() ->
new IllegalStateException("Report does not have associated snapshot id."));
insertMessages(snapshotId, messages, connection);
return snapshotId;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
});
}
public CompletableFuture<Integer> saveSnapshot(Collection<SnapshotMessage> messages)
{
return CompletableFuture.supplyAsync(() ->
{
try (Connection connection = DBPool.getAccount().getConnection())
{
int snapshotId = createSnapshot(connection, null);
insertMessages(snapshotId, messages, connection);
return snapshotId;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
});
}
public CompletableFuture<Integer> createSnapshot(Integer creatorAccountId)
{
return CompletableFuture.supplyAsync(() ->
{
try (Connection connection = DBPool.getAccount().getConnection())
{
return createSnapshot(connection, creatorAccountId);
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
});
}
private int createSnapshot(Connection connection, Integer creatorAccount) throws SQLException
{
PreparedStatement insertSnapshotStatement = connection.prepareStatement(INSERT_SNAPSHOT, new String[]{"id"});
if (creatorAccount != null)
{
insertSnapshotStatement.setInt(1, creatorAccount);
}
else
{
insertSnapshotStatement.setNull(1, Types.INTEGER);
}
insertSnapshotStatement.execute();
try (ResultSet resultSet = insertSnapshotStatement.getGeneratedKeys())
{
if (resultSet.next())
{
return resultSet.getInt(1);
}
else
{
throw new IllegalStateException("Query did not return a snapshot id.");
}
}
}
private void insertMessages(int snapshotId, Collection<SnapshotMessage> messages, Connection connection) throws SQLException
{
try (PreparedStatement insertSnapshotStatement = connection.prepareStatement(INSERT_MESSAGE, new String[]{"id"}))
{
try (PreparedStatement insertRecipientStatement = connection.prepareStatement(INSERT_MESSAGE_RECIPIENT))
{
try (PreparedStatement insertMappingStatement = connection.prepareStatement(INSERT_MESSAGE_MAPPING))
{
for (SnapshotMessage message : messages)
{
try
{
insertMessage(insertSnapshotStatement, insertRecipientStatement, insertMappingStatement, snapshotId, message);
}
catch (Exception e)
{
_logger.log(Level.SEVERE, "Error inserting snapshot message.", e);
}
}
insertRecipientStatement.executeBatch();
insertMappingStatement.executeBatch();
}
}
}
}
public CompletableFuture<Void> insertMessage(int snapshotId, SnapshotMessage message)
{
return CompletableFuture.supplyAsync(() ->
{
try (Connection connection = DBPool.getAccount().getConnection())
{
try (PreparedStatement insertSnapshotStatement = connection.prepareStatement(INSERT_MESSAGE, new String[]{"id"}))
{
try (PreparedStatement insertRecipientStatement = connection.prepareStatement(INSERT_MESSAGE_RECIPIENT))
{
try (PreparedStatement insertMappingStatement = connection.prepareStatement(INSERT_MESSAGE_MAPPING))
{
insertMessage(insertSnapshotStatement, insertRecipientStatement, insertMappingStatement, snapshotId, message);
}
}
}
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
return null;
});
}
private void insertMessage(PreparedStatement insertSnapshotStatement, PreparedStatement insertRecipientStatement, PreparedStatement insertMappingStatement, int snapshotId, SnapshotMessage message) throws SQLException
{
boolean freshInsert = insertMessage(insertSnapshotStatement, message);
long messageId = message.getId().orElseThrow(() ->
new IllegalStateException("Message id not present (perhaps insert failed?)."));
if (freshInsert)
{
insertRecipients(insertRecipientStatement, messageId, message.getRecipientIds());
}
if (!message.getLinkedSnapshots().contains(snapshotId))
{
insertMessageMapping(insertMappingStatement, snapshotId, messageId);
message.addLinkedSnapshot(snapshotId);
}
}
private boolean insertMessage(PreparedStatement insertSnapshotStatement, SnapshotMessage message) throws SQLException
{
Optional<Long> messageIdOptional = message.getId();
boolean freshInsert = !messageIdOptional.isPresent();
if (freshInsert)
{
insertSnapshotStatement.setInt(1, message.getSenderId());
insertSnapshotStatement.setString(2, _serverName);
insertSnapshotStatement.setTimestamp(3, UtilTime.toTimestamp(message.getSentTime()));
insertSnapshotStatement.setString(4, message.getMessage());
insertSnapshotStatement.setInt(5, message.getType().getId());
insertSnapshotStatement.execute();
try (ResultSet resultSet = insertSnapshotStatement.getGeneratedKeys())
{
if (resultSet.next())
{
message._id = resultSet.getLong(1);
}
else
{
throw new IllegalStateException("Query did not return a message id.");
}
}
}
return freshInsert;
}
private void insertRecipients(PreparedStatement insertRecipientStatement, long messageId, Collection<Integer> recipients) throws SQLException
{
for (int recipientId : recipients)
{
insertRecipientStatement.setLong(1, messageId);
insertRecipientStatement.setInt(2, recipientId);
insertRecipientStatement.addBatch();
}
insertRecipientStatement.executeBatch();
}
private void insertMessageMapping(PreparedStatement insertMappingStatement, int snapshotId, long messageId) throws SQLException
{
insertMappingStatement.setInt(1, snapshotId);
insertMappingStatement.setLong(2, messageId);
insertMappingStatement.execute();
}
}

View File

@ -0,0 +1,44 @@
package mineplex.core.chatsnap.command;
import java.util.Set;
import mineplex.serverdata.commands.ServerCommand;
/**
* This command when executed will get all snapshots involving a particular account and push them to the database.
*/
public class PushSnapshotsCommand extends ServerCommand
{
private final int _accountId;
private final long _reportId;
private final Set<Integer> _reporters;
public PushSnapshotsCommand(int accountId, long reportId, Set<Integer> reporters)
{
super();
_accountId = accountId;
_reportId = reportId;
_reporters = reporters;
}
public int getAccountId()
{
return _accountId;
}
public long getReportId()
{
return _reportId;
}
public Set<Integer> getReporters()
{
return _reporters;
}
@Override
public void run()
{
// Utilitizes a callback functionality to seperate dependencies
}
}

View File

@ -0,0 +1,49 @@
package mineplex.core.chatsnap.command;
import java.util.Set;
import mineplex.core.chatsnap.SnapshotMessage;
import mineplex.core.chatsnap.SnapshotManager;
import mineplex.core.report.ReportManager;
import mineplex.core.report.data.Report;
import mineplex.serverdata.commands.CommandCallback;
import mineplex.serverdata.commands.ServerCommand;
/**
* Handles receiving of {@link PushSnapshotsCommand} instances.
*/
public class PushSnapshotsHandler implements CommandCallback
{
private final ReportManager _reportManager;
private final SnapshotManager _snapshotManager;
public PushSnapshotsHandler(ReportManager reportManager, SnapshotManager snapshotManager)
{
_reportManager = reportManager;
_snapshotManager = snapshotManager;
}
@Override
public void run(ServerCommand command)
{
if (command instanceof PushSnapshotsCommand)
{
PushSnapshotsCommand pushCommand = (PushSnapshotsCommand) command;
int accountId = pushCommand.getAccountId();
long reportId = pushCommand.getReportId();
Set<Integer> reporters = pushCommand.getReporters();
Set<SnapshotMessage> messages = _snapshotManager.getMessagesInvolving(accountId, reporters);
if (messages.size() > 0)
{
_reportManager.getReportRepository().getReport(reportId).thenAccept(reportOptional ->
{
if (reportOptional.isPresent())
{
Report report = reportOptional.get();
_snapshotManager.saveReportSnapshot(report, messages);
}
});
}
}
}
}

View File

@ -1,54 +0,0 @@
package mineplex.core.chatsnap.commands;
import java.util.Set;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import mineplex.core.chatsnap.SnapshotPlugin;
import mineplex.core.chatsnap.Snapshot;
import mineplex.core.command.CommandBase;
import mineplex.core.common.Rank;
import mineplex.core.common.util.F;
import mineplex.core.common.util.UtilPlayer;
/**
* Displays what chat messages we have cached for a player.
*/
public class ChatCacheCommand extends CommandBase<SnapshotPlugin>
{
public ChatCacheCommand(SnapshotPlugin plugin)
{
super(plugin, Rank.MODERATOR, "chatcache");
}
@Override
public void Execute(final Player caller, String[] args)
{
if (args.length != 1)
{
UtilPlayer.message(caller, F.main(Plugin.getName(), String.format("Invalid arguments, usage: /%s <player>", _aliasUsed)));
return;
}
final String playerName = args[0];
// getOfflinePlayer sometimes blocks, see this needs to be async
Plugin.getScheduler().runTaskAsynchronously(Plugin.getPlugin(), new Runnable()
{
@Override
public void run()
{
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerName);
Set<Snapshot> snaps = Plugin.getSnapshotManager().getSnapshots(offlinePlayer.getUniqueId());
for (Snapshot snapshot : snaps)
{
// TODO: show sender name
caller.sendMessage(snapshot.getMessage());
}
}
});
}
}

View File

@ -1,33 +0,0 @@
package mineplex.core.chatsnap.publishing;
import java.lang.reflect.Type;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
/**
* Handles serialization of Java 8's {@link LocalDateTime}.
*/
public class LocalDateTimeSerializer implements JsonSerializer<LocalDateTime>
{
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
private ZoneId _zoneId;
public LocalDateTimeSerializer(ZoneId zoneId)
{
_zoneId = zoneId;
}
@Override
public JsonElement serialize(LocalDateTime localDateTime, Type type, JsonSerializationContext jsonSerializationContext)
{
return new JsonPrimitive(localDateTime.atZone(_zoneId).toLocalDateTime().truncatedTo(ChronoUnit.SECONDS).format(FORMATTER));
}
}

View File

@ -1,32 +0,0 @@
package mineplex.core.chatsnap.publishing;
import java.lang.reflect.Type;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import mineplex.core.report.Report;
/**
* Handles serialization of {@link Report} instances.
*/
public class ReportSerializer implements JsonSerializer<Report>
{
@Override
public JsonElement serialize(Report report, Type type, JsonSerializationContext jsonSerializationContext)
{
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("id", report.getReportId());
jsonObject.addProperty("serverName", report.getServerName());
if (report.getHandler() != null)
{
jsonObject.addProperty("handler", report.getHandler().toString());
}
jsonObject.addProperty("suspect", report.getSuspect().toString());
jsonObject.add("reporters", jsonSerializationContext.serialize(report.getReportReasons()));
return jsonObject;
}
}

View File

@ -1,119 +0,0 @@
package mineplex.core.chatsnap.publishing;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import mineplex.core.chatsnap.Snapshot;
import mineplex.core.report.Report;
import mineplex.serverdata.Utility;
import mineplex.serverdata.servers.ServerManager;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
/**
* Class responsible for publishing snapshots on the website via Redis and a separate Report server.
*/
public class SnapshotPublisher
{
private static final ZoneId ZONE_ID = ZoneId.of("America/Chicago"); // This means "CST"
private static final Gson GSON = new GsonBuilder()
.setPrettyPrinting()
.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeSerializer(ZONE_ID))
.registerTypeAdapter(Report.class, new ReportSerializer())
.create();
public static ZoneId getZoneId()
{
return ZONE_ID;
}
public static Gson getGson()
{
return GSON;
}
public static String getURL(String token)
{
return URL_PREFIX + token;
}
private static final String URL_PREFIX = "http://file.mineplex.com/chatsnap/view.php?identifier=";
public static final String CHANNEL_DEPLOY = "reportserver:deploy";
public static final String CHANNEL_DESTROY = "reportserver:destroy";
private JavaPlugin _plugin;
private JedisPool _jedisPool;
public SnapshotPublisher(JavaPlugin plugin)
{
_plugin = plugin;
_jedisPool = Utility.generatePool(ServerManager.getMasterConnection());
}
public void publishChatLog(String token, JsonObject jsonObject)
{
jsonObject.addProperty("token", token);
String json = GSON.toJson(jsonObject);
// getting a Jedis resource can block, so lets async it
Bukkit.getScheduler().runTaskAsynchronously(_plugin, () ->
{
try (Jedis jedis = _jedisPool.getResource())
{
jedis.publish(CHANNEL_DEPLOY, json);
}
});
}
public void unpublishChatLog(String token)
{
// getting a Jedis resource can block, so lets async it
Bukkit.getScheduler().runTaskAsynchronously(_plugin, () ->
{
try (Jedis jedis = _jedisPool.getResource())
{
jedis.publish(CHANNEL_DESTROY, token);
}
});
}
public Set<UUID> getUUIDs(Collection<Snapshot> snapshots)
{
// Being a Set ensures no duplicates
Set<UUID> uuids = new HashSet<>();
for (Snapshot snapshot : snapshots)
{
uuids.add(snapshot.getSender());
uuids.addAll(snapshot.getRecipients().stream().collect(Collectors.toList()));
}
return uuids;
}
public Map<UUID, String> getUsernameMap(Collection<UUID> collection)
{
Map<UUID, String> uuidUsernameMap = new HashMap<>();
for (UUID uuid : collection)
{
String username = Bukkit.getOfflinePlayer(uuid).getName();
uuidUsernameMap.put(uuid, username);
}
return uuidUsernameMap;
}
}

View File

@ -15,7 +15,7 @@ public class Npc
{
private final NpcManager _npcManager;
private final NpcsRecord _databaseRecord;
private final Location _location;
private Location _location;
private LivingEntity _entity;
private int _failedAttempts = 0;
private boolean _returning = false;
@ -183,4 +183,9 @@ public class Npc
{
return _infoRadiusSquared;
}
public void setLocation(Location location)
{
_location = location;
}
}

View File

@ -166,7 +166,7 @@ public class Party
*/
public void sendMessage(String message)
{
getMembersByUUID().stream().map(Bukkit::getPlayer).forEach(player -> player.sendMessage(message));
_members.stream().map(Bukkit::getPlayer).forEach(player -> player.sendMessage(message));
}
public int getSize()

View File

@ -149,6 +149,7 @@ public class PartyMethodManager
{
caller.sendMessage(F.main("Party", "You have denied the invite to " + F.name(target)) + "'s party.");
}
inviteManager.respondToInvite(caller, target, accept);
}

View File

@ -159,7 +159,7 @@ public class PersonalServerManager extends MiniPlugin
}
}
final ServerGroup serverGroup = new ServerGroup(serverName, serverName, host.getName(), ram, cpu, 1, 0, UtilMath.random.nextInt(250) + 19999, "", true, "arcade.zip", "Arcade.jar", "plugins/Arcade/", minPlayers, maxPlayers,
final ServerGroup serverGroup = new ServerGroup(serverName, serverName, host.getName(), ram, cpu, 1, 0, UtilMath.random.nextInt(250) + 19999, "", true, "Lobby_MPS.zip", "Arcade.jar", "plugins/Arcade/", minPlayers, maxPlayers,
true, false, false, games, "", "", "Player", true, event, false, true, false, true, true, false, false, false, false, true, true, true, false, false, "", _us ? Region.US : Region.EU, "", "", "", "");
getPlugin().getServer().getScheduler().runTaskAsynchronously(getPlugin(), new Runnable()

View File

@ -8,6 +8,7 @@ public enum Category
Hacking, // Illegal Mods
Warning,
PermMute,
ReportAbuse, // Abusing /report command
Other; // Represents perm ban - (or old perm mutes)
public static boolean contains(String s)

View File

@ -1,6 +1,7 @@
package mineplex.core.punish;
import java.util.HashMap;
import java.util.logging.Level;
import mineplex.core.account.CoreClient;
import org.bukkit.Bukkit;
@ -188,8 +189,24 @@ public class Punish extends MiniPlugin
_punishClients.put(playerName.toLowerCase(), new PunishClient());
}
final PunishmentSentence sentence = !ban ? PunishmentSentence.Mute : PunishmentSentence.Ban;
final PunishmentSentence sentence;
if (ban)
{
sentence = PunishmentSentence.Ban;
}
else
{
if (category == Category.ReportAbuse)
{
sentence = PunishmentSentence.ReportBan;
}
else
{
sentence = PunishmentSentence.Mute;
}
}
final long finalDuration = duration;
_repository.Punish(new Callback<String>()
@ -241,6 +258,34 @@ public class Punish extends MiniPlugin
informOfPunish(finalPlayerName, F.main(getName(), caller == null ? "Mineplex Anti-Cheat" : caller.getName() + " banned " + finalPlayerName + " for " + durationString + "."));
}
}
else if (sentence == PunishmentSentence.ReportBan)
{
if (caller == null)
System.out.println(F.main(getName(), F.elem(caller == null ? "Mineplex Anti-Cheat" : caller.getName()) + " report banned " + F.elem(finalPlayerName) + " because of " + F.elem(reason) + " for " +
durationString + "."));
if (!silent)
{
informOfPunish(finalPlayerName, F.main(getName(), caller == null ? "Mineplex Anti-Cheat" : caller.getName() + " report banned " + finalPlayerName + " for " + durationString + "."));
}
//Inform
if (player != null)
{
UtilPlayer.message(player, F.main("Punish", F.elem(C.cGray + C.Bold + "Reason: ") + reason));
player.playSound(player.getLocation(), Sound.CAT_MEOW, 1f, 1f);
}
else
new mineplex.serverdata.commands.PunishCommand(finalPlayerName, false, finalDuration != 0, F.main("Punish", F.elem(C.cGray + C.Bold + "Report Ban Reason: ") + reason)).publish();
_repository.LoadPunishClient(finalPlayerName, new Callback<PunishClientToken>()
{
public void run(PunishClientToken token)
{
LoadClient(token);
}
});
}
else
{
if (caller == null)
@ -306,7 +351,22 @@ public class Punish extends MiniPlugin
for (PunishmentToken punishment : token.Punishments)
{
client.AddPunishment(Category.valueOf(punishment.Category), new Punishment(punishment.PunishmentId, PunishmentSentence.valueOf(punishment.Sentence), Category.valueOf(punishment.Category), punishment.Reason, punishment.Admin, punishment.Duration, punishment.Severity, punishment.Time + timeDifference, punishment.Active, punishment.Removed, punishment.RemoveAdmin, punishment.RemoveReason));
Category category;
PunishmentSentence punishmentType;
// catch if category or punishment type no longer exists
try
{
category = Category.valueOf(punishment.Category);
punishmentType = PunishmentSentence.valueOf(punishment.Sentence);
}
catch(IllegalArgumentException e)
{
getPlugin().getLogger().log(Level.WARNING, "Skipping loading of punishment id " + punishment.PunishmentId + ", invalid category or punishment type.");
continue;
}
client.AddPunishment(category, new Punishment(punishment.PunishmentId, punishmentType, category, punishment.Reason, punishment.Admin, punishment.Duration, punishment.Severity, punishment.Time + timeDifference, punishment.Active, punishment.Removed, punishment.RemoveAdmin, punishment.RemoveReason));
}
_punishClients.put(token.Name.toLowerCase(), client);

View File

@ -54,6 +54,22 @@ public class PunishClient
return false;
}
public boolean IsReportBanned()
{
for (List<Punishment> punishments : _punishments.values())
{
for (Punishment punishment : punishments)
{
if (punishment.IsReportBanned())
{
return true;
}
}
}
return false;
}
public Punishment GetPunishment(PunishmentSentence sentence)
{
for (List<Punishment> punishments : _punishments.values())
@ -68,6 +84,10 @@ public class PunishClient
{
return punishment;
}
else if (sentence == PunishmentSentence.ReportBan && punishment.IsReportBanned())
{
return punishment;
}
}
}

View File

@ -105,6 +105,11 @@ public class Punishment
return _punishmentType == PunishmentSentence.Mute && (GetRemaining() > 0 || _hours < 0) && _active;
}
public boolean IsReportBanned()
{
return _punishmentType == PunishmentSentence.ReportBan && (GetRemaining() > 0 || _hours < 0) && _active;
}
public long GetRemaining()
{
return _hours < 0 ? -1 : (long) ((_time + (TimeSpan.HOUR * _hours)) - System.currentTimeMillis());

View File

@ -3,5 +3,6 @@ package mineplex.core.punish;
public enum PunishmentSentence
{
Ban,
Mute
Mute,
ReportBan
}

View File

@ -55,6 +55,7 @@ public class PunishPage extends CraftInventoryCustom implements Listener
private ShopItem _warningButton;
private ShopItem _permMuteButton;
private ShopItem _permBanButton;
private ShopItem _permReportBanButton;
public PunishPage(Punish plugin, Player player, String target, String reason, boolean wasDisguised, String originalName, String disguisedName)
{
@ -118,6 +119,7 @@ public class PunishPage extends CraftInventoryCustom implements Listener
_warningButton = new ShopItem(Material.PAPER, (byte)0, "Warning", new String[] { }, 1, false, true);
_permMuteButton = new ShopItem(Material.EMERALD_BLOCK, (byte)0, "Permanent Mute", new String[] { }, 1, false, true);
_permBanButton = new ShopItem(Material.REDSTONE_BLOCK, (byte)0, "Permanent Ban", new String[] { }, 1, false, true);
_permReportBanButton = new ShopItem(Material.ENCHANTED_BOOK, (byte)0, "Permanent Report Ban", new String[] { }, 1, false, true);
getInventory().setItem(10, _chatOffenseButton.getHandle());
getInventory().setItem(12, _exploitingButton.getHandle());
@ -310,6 +312,14 @@ public class PunishPage extends CraftInventoryCustom implements Listener
" ",
examplePrefixNote + "Must supply detailed reason for Mute."
}, 1, false, true), new PunishButton(this, Category.ChatOffense, 4, false, -1));
AddButton(26, new ShopItem(Material.ENCHANTED_BOOK, (byte)0, "Permanent Report Ban", new String[] {
ChatColor.RESET + "Report Ban Duration: " + ChatColor.YELLOW + "Permanent",
" ",
examplePrefix + "Abusing Report Feature",
examplePrefixEx + " /report SomeUser THE STAFF HERE SUCK",
examplePrefixEx + " /report SomeUser MINEPLEX IS A F****** PIECE OF S***"
}, 1, false, true), new PunishButton(this, Category.ReportAbuse, 3, false, -1));
}
if (_plugin.GetClients().Get(_player).GetRank() == Rank.DEVELOPER || _plugin.GetClients().Get(_player).GetRank() == Rank.JNR_DEV)
@ -372,9 +382,12 @@ public class PunishPage extends CraftInventoryCustom implements Listener
case PermMute:
button = _permMuteButton.clone();
break;
case ReportAbuse:
button = _permReportBanButton.clone();
break;
case Other:
button = _permBanButton.clone();
break;
break;
default:
break;
}

View File

@ -1,102 +0,0 @@
package mineplex.core.report;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import mineplex.serverdata.data.Data;
import org.apache.commons.lang3.RandomStringUtils;
/**
* Contains data about a report.
* Active/open reports have their instances are stored in Redis for cross-server access.
*/
public class Report implements Data
{
private static final int TIMEOUT_MINS = 15;
private int _reportId;
public int getReportId() { return _reportId; }
private String _serverName;
public String getServerName() { return _serverName; }
private UUID _suspect;
public UUID getSuspect() { return _suspect; }
// Set of player names and the reason they reported this player
private Map<UUID, String> _reportReasons;
public Map<UUID, String> getReportReasons() { return _reportReasons; }
public Set<UUID> getReporters() { return _reportReasons.keySet(); }
public void addReporter(UUID reporter, String reason) { _reportReasons.put(reporter, reason); }
private UUID _handler = null;
public void setHandler(UUID handler) { _handler = handler; }
public UUID getHandler() { return _handler; }
private ReportCategory _category;
public ReportCategory getCategory() { return _category; }
private long _lastActivity;
public long getLastActivity() { return _lastActivity; }
private String _token = null;
public Report(int reportId, UUID suspect, String serverName, ReportCategory category)
{
_reportId = reportId;
_suspect = suspect;
_serverName = serverName;
_reportReasons = new HashMap<>();
_category = category;
updateLastActivity();
}
/**
* Checks if a report is still active.
* This is determined by checking if the last activity was within the last 15 minutes.
*
* @return true if active, false if expired
*/
public boolean isActive()
{
return _lastActivity + TimeUnit.MINUTES.toMillis(TIMEOUT_MINS) >= System.currentTimeMillis();
}
public void updateLastActivity()
{
_lastActivity = System.currentTimeMillis();
}
public boolean hasToken()
{
return _token != null;
}
/**
* Gets a token in the format of reportId-randomCharacters.
* Currently this is only used for publishing chat abuse reports.
*
* @return the full token
*/
public String getToken()
{
// since we don't always use this, only generate a token when we need it
if (_token == null)
{
_token = RandomStringUtils.randomAlphabetic(8);
}
return _reportId + "-" + _token;
}
@Override
public String getDataId()
{
return String.valueOf(_reportId);
}
}

View File

@ -1,74 +1,63 @@
package mineplex.core.report;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.Material;
import mineplex.core.common.util.C;
import org.apache.commons.lang3.text.WordUtils;
/**
* Contains the reasons a player can be reported for.
*/
public enum ReportCategory
{
/**
* Global category, used for representing values which aren't tied to a specific category (such as abusive report statistics).
*/
GLOBAL(0),
// descriptions borrowed from PunishPage
HACKING(0, 3, Material.IRON_SWORD, C.cRedB + "Hacking", "X-ray, Forcefield, Speed, Fly etc"),
CHAT_ABUSE(1, 1, Material.BOOK_AND_QUILL, C.cDAquaB + "Chat Abuse", "Verbal Abuse, Spam, Harassment, Trolling, etc");
/**
* Hacking category, for reports involving cheats of any sort.
*/
HACKING(1),
private int _id;
private int _notifyThreshold;
private Material _displayMaterial;
private String _title;
private List<String> _lore;
/**
* Chat Abuse category, for reports involving offensive comments made in chat.
*/
CHAT_ABUSE(2),
ReportCategory(int id, int notifyThreshold, Material displayMaterial, String title, String... lore)
/**
* Gameplay category, for reports specific to gameplay (such as bug exploits or issues with the map).
*/
GAMEPLAY(3);
private final int _id;
private final String _name;
ReportCategory(int id)
{
_id = id;
_notifyThreshold = notifyThreshold;
_displayMaterial = displayMaterial;
_title = title;
_lore = new ArrayList<>();
// prefix are lore lines
for (String loreLine : lore) {
_lore.add(C.cGray + loreLine);
}
_name = WordUtils.capitalizeFully(name().replace('_', ' '));
}
/**
* Gets the id, mainly used for database
*
* @return the id
*/
public int getId()
{
return _id;
}
public int getNotifyThreshold()
public String getName()
{
return _notifyThreshold;
return _name;
}
public Material getItemMaterial()
public static ReportCategory getById(int id)
{
return _displayMaterial;
}
public String getTitle()
{
return _title;
}
public List<String> getDescription()
{
return _lore;
}
public static ReportCategory fromId(int id)
{
for (ReportCategory category : values())
for (ReportCategory reportCategory : values())
{
if (category.getId() == id)
if (reportCategory.getId() == id)
{
return category;
return reportCategory;
}
}

View File

@ -0,0 +1,163 @@
package mineplex.core.report;
import java.util.Arrays;
import java.util.Collection;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import mineplex.core.chatsnap.SnapshotRepository;
import mineplex.core.common.jsonchat.ChildJsonMessage;
import mineplex.core.common.jsonchat.ClickEvent;
import mineplex.core.common.jsonchat.HoverEvent;
import mineplex.core.common.jsonchat.JsonMessage;
import mineplex.core.common.util.C;
import mineplex.core.common.util.F;
import mineplex.core.report.data.Report;
import mineplex.core.report.data.ReportMessage;
/**
* Displays a message containing up-to-date details of a report to it's handler.
*/
public class ReportHandlerTask extends BukkitRunnable
{
private final ReportManager _reportManager;
private final long _reportId;
public ReportHandlerTask(ReportManager reportManager, long reportId)
{
_reportManager = reportManager;
_reportId = reportId;
}
private CompletableFuture<Optional<Report>> getReport()
{
return _reportManager.getReportRepository().getReport(_reportId);
}
public void start(JavaPlugin plugin)
{
runTaskTimer(plugin, 1L, 20L * 10);
getReport().thenAccept(reportOptional ->
{
if (reportOptional.isPresent())
{
Report report = reportOptional.get();
report.cancelHandlerTask();
report.setHandlerTask(this);
}
});
}
@Override
public void run()
{
getReport().thenAccept(reportOptional ->
{
if (reportOptional.isPresent())
{
Report report = reportOptional.get();
long reportId = report.getId().orElse((long) -1);
_reportManager.isActiveReport(report).thenAccept(isActive ->
{
if (isActive)
{
_reportManager.getReportRepository().getAccountName(report.getSuspectId())
.thenAccept(suspectName ->
{
String prefix = F.main(ReportManager.getReportPrefix(reportId), "");
ChildJsonMessage jsonMessage = new JsonMessage("\n")
.extra(prefix + C.cAqua + "Report Overview")
.add("\n")
.add(prefix + C.cAqua + "Suspect - " + C.cGold + suspectName)
.add("\n")
.add(prefix + C.cAqua + "Type - " + C.cGold + report.getCategory().getName())
.add("\n" + prefix + "\n")
.add(prefix + C.cGold + report.getMessages().size() + C.cAqua + " total reports")
.add("\n")
.add(Arrays.stream(getReportReasons(report)).map(s -> prefix + s).collect(Collectors.joining("\n")))
.add("\n" + prefix + "\n");
if (report.getCategory() == ReportCategory.CHAT_ABUSE)
{
jsonMessage = jsonMessage
.add(prefix + C.cAqua + "View chat log")
.hover(HoverEvent.SHOW_TEXT, C.cGray + "Opens the chat log in your default browser")
.click(ClickEvent.OPEN_URL, SnapshotRepository.getURL(reportId))
.add("\n");
}
jsonMessage = jsonMessage
.add(prefix + C.cAqua + "Close this report")
.hover(HoverEvent.SHOW_TEXT, C.cGray + "Usage: /reportclose <reason>")
.click(ClickEvent.SUGGEST_COMMAND, "/reportclose ")
.add("\n");
Optional<Integer> handlerIdOptional = report.getHandlerId();
if (handlerIdOptional.isPresent())
{
int handlerId = handlerIdOptional.get();
JsonMessage finalJsonMessage = jsonMessage;
_reportManager.getReportRepository().getAccountUUID(handlerId).thenAccept(handlerUUID ->
{
if (handlerUUID != null)
{
Player handler = Bukkit.getPlayer(handlerUUID);
if (handler != null)
{
finalJsonMessage.sendToPlayer(handler);
}
else
{
// handler offline
cancel();
}
}
});
}
else
{
// no handler (report perhaps aborted), so cancel task
cancel();
}
});
}
else
{
// report has been closed, so this task should be cancelled
cancel();
}
});
}
});
}
private String[] getReportReasons(Report report)
{
Collection<ReportMessage> reportMessages = report.getMessages().values();
String[] output = new String[reportMessages.size()];
int count = 0;
for (ReportMessage reportMessage : reportMessages)
{
// this is blocking, but that's okay as it will only be called asynchronously
String reporterName = _reportManager.getReportRepository().getAccountName(reportMessage.getReporterId()).join();
// triple backslashes so this translates to valid JSON
output[count++] = String.format("%4$s(%d%4$s) %s%s%s - \\\"%s%s%4$s\\\"", count, C.cGold, reporterName, C.cGray, C.cPurple, reportMessage.getMessage());
}
return output;
}
}

View File

@ -1,39 +1,29 @@
package mineplex.core.report;
import mineplex.core.MiniPlugin;
import mineplex.core.report.command.ReportCloseCommand;
import mineplex.core.report.command.ReportCommand;
import mineplex.core.report.command.ReportHandleCommand;
import mineplex.core.report.task.ReportPurgeTask;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.java.JavaPlugin;
import mineplex.core.MiniPlugin;
import mineplex.core.report.command.ReportAbortCommand;
import mineplex.core.report.command.ReportCloseCommand;
import mineplex.core.report.command.ReportCommand;
import mineplex.core.report.command.ReportHandleCommand;
import mineplex.core.report.command.ReportInfoCommand;
import mineplex.core.report.command.ReportStatsCommand;
/**
* Main class for this module, handles initialization and disabling of the module.
*/
public class ReportPlugin extends MiniPlugin
{
private final ReportManager _reportManager;
private ReportPurgeTask _reportPurgeTask;
public ReportPlugin(JavaPlugin plugin, ReportManager reportManager)
{
super("Report", plugin);
_reportManager = reportManager;
// purge old reports every minute
_reportPurgeTask = new ReportPurgeTask(_reportManager);
_reportPurgeTask.runTaskTimerAsynchronously(getPlugin(), 20L * 10, 20L * 60);
}
@Override
public void disable()
{
_reportPurgeTask.cancel();
}
public ReportManager getReportManager()
@ -47,6 +37,9 @@ public class ReportPlugin extends MiniPlugin
addCommand(new ReportCommand(this));
addCommand(new ReportHandleCommand(this));
addCommand(new ReportCloseCommand(this));
addCommand(new ReportStatsCommand(this));
addCommand(new ReportAbortCommand(this));
addCommand(new ReportInfoCommand(this));
}
@EventHandler

View File

@ -0,0 +1,87 @@
package mineplex.core.report;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import mineplex.core.common.util.UtilServer;
import mineplex.core.report.redis.FindPlayer;
import mineplex.core.report.redis.FindPlayerResponse;
import mineplex.core.report.redis.HandlerNotification;
import mineplex.core.report.redis.ReportersNotification;
import mineplex.serverdata.commands.CommandCallback;
import mineplex.serverdata.commands.ServerCommand;
/**
* Handles receiving of report notifications.
*/
public class ReportRedisManager implements CommandCallback
{
private final ReportManager _reportManager;
private final String _serverName;
public ReportRedisManager(ReportManager reportManager, String serverName)
{
_reportManager = reportManager;
_serverName = serverName;
}
@Override
public void run(ServerCommand command)
{
if (command instanceof HandlerNotification)
{
HandlerNotification reportNotification = (HandlerNotification) command;
_reportManager.getReportRepository().getReport(reportNotification.getReportId()).thenAccept(report ->
{
if (report != null)
{
int handlerId = reportNotification.getHandlerId();
_reportManager.getReportRepository().getAccountUUID(handlerId).thenAccept(handlerUUID ->
{
if (handlerUUID != null)
{
Player handler = Bukkit.getPlayer(handlerUUID);
if (handler != null)
{
sendRawMessage(handler, reportNotification.getJson());
}
}
});
}
}
);
}
else if (command instanceof ReportersNotification)
{
ReportersNotification reportersNotification = (ReportersNotification) command;
reportersNotification.getReporterUUIDs().stream()
.map(Bukkit::getPlayer)
.filter(player -> player != null)
.forEach(reporter -> sendRawMessage(reporter, reportersNotification.getJson()));
}
else if (command instanceof FindPlayer)
{
FindPlayer findPlayer = (FindPlayer) command;
if (Bukkit.getPlayer(findPlayer.getId()) != null)
{
new FindPlayerResponse(findPlayer, _serverName).publish();
}
}
else if (command instanceof FindPlayerResponse)
{
FindPlayerResponse foundPlayer = (FindPlayerResponse) command;
_reportManager.onSuspectLocated(foundPlayer.getReportId(), foundPlayer.getServerName());
}
}
private void sendRawMessage(Player player, String rawMessage)
{
Server server = UtilServer.getServer();
server.dispatchCommand(server.getConsoleSender(), "tellraw " + player.getName() + " " + rawMessage);
}
}

View File

@ -1,50 +0,0 @@
package mineplex.core.report;
import mineplex.core.database.MinecraftRepository;
import mineplex.serverdata.database.DBPool;
import mineplex.serverdata.database.DatabaseRunnable;
import mineplex.serverdata.database.RepositoryBase;
import mineplex.serverdata.database.column.ColumnInt;
import mineplex.serverdata.database.column.ColumnVarChar;
import org.bukkit.plugin.java.JavaPlugin;
/**
* Responsible for all database related operations for this module.
*/
public class ReportRepository extends MinecraftRepository
{
private static String CREATE_TICKET_TABLE = "CREATE TABLE IF NOT EXISTS reportTickets (reportId INT NOT NULL, eventDate LONG, playerId INT NOT NULL, server VARCHAR(50), closerId INT NOT NULL, result VARCHAR(25), reason VARCHAR(100), PRIMARY KEY (reportId), FOREIGN KEY (playerId) REFERENCES accounts(id), FOREIGN KEY (closerId) REFERENCES accounts(id));";
private static String INSERT_TICKET = "INSERT INTO reportTickets (reportId, eventDate, playerId, server, closerId, result, reason) VALUES (?, now(), ?, ?, ?, ?, ?);";
public ReportRepository(JavaPlugin plugin)
{
super(plugin, DBPool.getAccount());
}
@Override
protected void initialize()
{
// executeUpdate(CREATE_TICKET_TABLE);
}
@Override
protected void update()
{
}
public void logReport(final int reportId, final int playerId, final String server, final int closerId, final ReportResult result, final String reason)
{
handleDatabaseCall(new DatabaseRunnable(new Runnable()
{
@Override
public void run()
{
executeUpdate(INSERT_TICKET, new ColumnInt("reportId", reportId), new ColumnInt("playerId", playerId),
new ColumnVarChar("server", 50, server), new ColumnInt("closerId", closerId),
new ColumnVarChar("result", 25, result.toString()), new ColumnVarChar("reason", 100, reason));
}
}), "Error logging result for report " + reportId + ".");
}
}

View File

@ -1,58 +1,41 @@
package mineplex.core.report;
import org.bukkit.ChatColor;
import java.time.LocalDateTime;
import java.util.Optional;
/**
* Contains all possible outcomes for a report.
* Stores data about the result of a report.
*/
public enum ReportResult
public class ReportResult
{
ACCEPTED("Global.AcceptedReportsCount", ChatColor.GREEN, "Accept Report (Punish Player)", "Accepted (Player Received Punishment)"),
DENIED("Global.DeniedReportsCount", ChatColor.YELLOW, "Deny Report", "Denied"),
ABUSIVE("Global.AbusiveReportsCount", ChatColor.RED, "Mark Abusive Report", "Abusive Report");
private ReportResultType _resultType;
private String _reason;
private LocalDateTime _closedTime;
private final String _statName;
private final ChatColor _color;
private final String _actionMessage;
private final String _resultMessage;
private final String[] _lore;
ReportResult(String statName, ChatColor color, String actionMessage, String resultMessage, String... lore)
public ReportResult(ReportResultType resultType, String reason)
{
_statName = statName;
_color = color;
_actionMessage = actionMessage;
_resultMessage = resultMessage;
_lore = lore;
this(resultType, reason, LocalDateTime.now());
}
public String getStatName()
public ReportResult(ReportResultType resultType, String reason, LocalDateTime closedTime)
{
return _statName;
_resultType = resultType;
_reason = reason;
_closedTime = closedTime;
}
public ChatColor getColor()
public ReportResultType getType()
{
return _color;
return _resultType;
}
public String getActionMessage()
public Optional<String> getReason()
{
return _actionMessage;
return Optional.ofNullable(_reason);
}
public String getResultMessage()
public LocalDateTime getClosedTime()
{
return _resultMessage;
}
public String[] getLore()
{
return _lore;
}
public String toDisplayMessage()
{
return _color + _resultMessage;
return _closedTime;
}
}

View File

@ -0,0 +1,53 @@
package mineplex.core.report;
import org.apache.commons.lang3.text.WordUtils;
/**
* Contains all possible outcomes for a report.
*/
public enum ReportResultType
{
ACCEPTED(0, false),
DENIED(1, false),
ABUSIVE(2, true),
EXPIRED(3, true);
private final int _id;
private final boolean _globalStat;
private final String _name;
ReportResultType(int id, boolean globalStat)
{
_id = id;
_globalStat = globalStat;
_name = WordUtils.capitalizeFully(name().replace('_', ' '));
}
public int getId()
{
return _id;
}
public boolean isGlobalStat()
{
return _globalStat;
}
public String getName()
{
return _name;
}
public static ReportResultType getById(int id)
{
for (ReportResultType resultType : values())
{
if (resultType.getId() == id)
{
return resultType;
}
}
return null;
}
}

View File

@ -0,0 +1,25 @@
package mineplex.core.report;
import org.apache.commons.lang3.text.WordUtils;
/**
* All possible roles a user can have in a report.
*/
public enum ReportRole
{
SUSPECT,
REPORTER,
HANDLER;
private final String _humanName;
ReportRole()
{
_humanName = WordUtils.capitalize(name().toLowerCase().replace('_', ' '));
}
public String getHumanName()
{
return _humanName;
}
}

View File

@ -0,0 +1,41 @@
package mineplex.core.report;
/**
* Contains all possible teams a player can be a member of and it's database mapping.
*/
public enum ReportTeam
{
RC((short) 0, 30);
private final short _databaseId;
private final int _initialPriority;
ReportTeam(short databaseId, int initialPriority)
{
_databaseId = databaseId;
_initialPriority = initialPriority;
}
public short getDatabaseId()
{
return _databaseId;
}
public int getInitialPriority()
{
return _initialPriority;
}
public static ReportTeam getById(short databaseId)
{
for (ReportTeam team : values())
{
if (team.getDatabaseId() == databaseId)
{
return team;
}
}
return null;
}
}

View File

@ -0,0 +1,54 @@
package mineplex.core.report.command;
import org.bukkit.entity.Player;
import mineplex.core.command.CommandBase;
import mineplex.core.common.Rank;
import mineplex.core.common.util.BukkitFuture;
import mineplex.core.common.util.C;
import mineplex.core.common.util.F;
import mineplex.core.common.util.UtilPlayer;
import mineplex.core.report.ReportManager;
import mineplex.core.report.ReportPlugin;
import mineplex.core.report.data.Report;
/**
* A command which allows a staff member to abort the report which they are currently handling.
* Another staff member may be given this report when executing {@link ReportHandleCommand}.
*/
public class ReportAbortCommand extends CommandBase<ReportPlugin>
{
public ReportAbortCommand(ReportPlugin plugin)
{
super(plugin, Rank.MODERATOR, "reportabort");
}
@Override
public void Execute(Player player, String[] args)
{
if (args == null || args.length == 0)
{
ReportManager reportManager = Plugin.getReportManager();
reportManager.getReportHandling(player).thenApply(BukkitFuture.accept(reportOptional ->
{
if (reportOptional.isPresent())
{
Report report = reportOptional.get();
reportManager.abortReport(report).thenApply(BukkitFuture.accept(voidValue ->
UtilPlayer.message(player, F.main(ReportManager.getReportPrefix(report),
"Report has been aborted and may be handled by another staff member."))));
}
else
{
UtilPlayer.message(player, F.main(Plugin.getName(), "You aren't currently handling a report."));
}
}));
}
else
{
UtilPlayer.message(player, F.main(Plugin.getName(), C.cRed + "Invalid Usage: " + F.elem("/" + _aliasUsed)));
}
}
}

View File

@ -1,19 +1,23 @@
package mineplex.core.report.command;
import org.bukkit.entity.Player;
import mineplex.core.command.CommandBase;
import mineplex.core.common.Rank;
import mineplex.core.common.util.BukkitFuture;
import mineplex.core.common.util.C;
import mineplex.core.common.util.F;
import mineplex.core.common.util.UtilPlayer;
import mineplex.core.report.ReportManager;
import mineplex.core.report.ReportPlugin;
import mineplex.core.report.data.Report;
import mineplex.core.report.ui.ReportResultPage;
import org.bukkit.entity.Player;
/**
* The command used to close the report the user is currently handling.
*/
public class ReportCloseCommand extends CommandBase<ReportPlugin>
{
public ReportCloseCommand(ReportPlugin plugin)
{
super(plugin, Rank.MODERATOR, "reportclose", "rc");
@ -22,25 +26,39 @@ public class ReportCloseCommand extends CommandBase<ReportPlugin>
@Override
public void Execute(final Player player, final String[] args)
{
if(args == null || args.length < 2)
if (args == null || args.length < 1)
{
UtilPlayer.message(player, F.main(Plugin.getName(), C.cRed + "Your arguments are inappropriate for this command!"));
return;
UtilPlayer.message(player, F.main(Plugin.getName(), C.cRed + "Invalid Usage: " + F.elem("/" + _aliasUsed + " <reason>")));
}
else
{
int reportId = Integer.parseInt(args[0]);
String reason = F.combine(args, 1, null, false);
String reason = F.combine(args, 0, null, false);
ReportManager reportManager = Plugin.getReportManager();
if (Plugin.getReportManager().isActiveReport(reportId))
reportManager.getReportHandling(player).whenComplete((reportOptional, throwable) ->
{
ReportResultPage reportResultPage = new ReportResultPage(Plugin, reportId, player, reason);
reportResultPage.openInventory(); // report is closed when player selects the result
}
else
{
UtilPlayer.message(player, F.main(Plugin.getName(), C.cRed + "That report either does not exist or has been closed."));
}
if (throwable == null)
{
if (reportOptional.isPresent())
{
Report report = reportOptional.get();
reportManager.getReportRepository().getAccountName(report.getSuspectId()).thenCompose(BukkitFuture.accept(suspectName ->
{
ReportResultPage reportResultPage = new ReportResultPage(Plugin, report, player, suspectName, reason);
reportResultPage.openInventory(); // report is closed when player selects the result
}));
}
else
{
UtilPlayer.message(player, F.main(Plugin.getName(), C.cRed + "You aren't currently handling a report."));
}
}
else
{
UtilPlayer.message(player, F.main(Plugin.getName(), C.cRed + "An error occurred, please try again later."));
}
});
}
}
}

View File

@ -1,55 +1,90 @@
package mineplex.core.report.command;
import mineplex.core.command.CommandBase;
import mineplex.core.common.Rank;
import mineplex.core.common.util.C;
import mineplex.core.common.util.F;
import mineplex.core.common.util.UtilPlayer;
import mineplex.core.report.ReportPlugin;
import mineplex.core.report.ui.ReportCategoryPage;
import org.bukkit.entity.Player;
import mineplex.core.account.CoreClient;
import mineplex.core.account.CoreClientManager;
import mineplex.core.command.CommandBase;
import mineplex.core.common.Rank;
import mineplex.core.common.util.BukkitFuture;
import mineplex.core.common.util.C;
import mineplex.core.common.util.F;
import mineplex.core.common.util.UtilPlayer;
import mineplex.core.report.ReportManager;
import mineplex.core.report.ReportPlugin;
import mineplex.core.report.ui.ReportCategoryPage;
/**
* The command used by players to create a report.
* When executing this command the user will be prompted to select the type of report.
*/
public class ReportCommand extends CommandBase<ReportPlugin>
{
public ReportCommand(ReportPlugin plugin)
{
super(plugin, Rank.ALL, "report");
}
@Override
public void Execute(final Player player, final String[] args)
public void Execute(final Player reporter, final String[] args)
{
if (!_commandCenter.GetClientManager().hasRank(player, Rank.ULTRA))
CoreClientManager clientManager = _commandCenter.GetClientManager();
if (!clientManager.hasRank(reporter, Rank.TITAN))
{
UtilPlayer.message(player, F.main(Plugin.getName(), C.cRed + "The report feature is currently in a trial phase for Ultra+ players"));
}
else if(args == null || args.length < 2)
{
UtilPlayer.message(player, F.main(Plugin.getName(), C.cRed + "Your arguments are inappropriate for this command!"));
UtilPlayer.message(reporter, F.main(Plugin.getName(), C.cRed + "The report feature is currently in a trial phase for Titan players"));
}
else
{
String playerName = args[0];
Player reportedPlayer = UtilPlayer.searchOnline(player, playerName, false);
String reason = F.combine(args, 1, null, false);
if (reportedPlayer != null)
ReportManager reportManager = Plugin.getReportManager();
boolean canReport = reportManager.canReport(reporter);
if (canReport)
{
if (reportedPlayer == player)
if(args == null || args.length < 2)
{
UtilPlayer.message(player, F.main(Plugin.getName(), C.cRed + "You cannot report yourself."));
UtilPlayer.message(reporter, F.main(Plugin.getName(), C.cRed + "Invalid Usage: " + F.elem("/" + _aliasUsed + " <player> <reason>")));
}
else
{
new ReportCategoryPage(Plugin, player, reportedPlayer, reason).openInventory();
int reporterId = clientManager.getAccountId(reporter);
String playerName = args[0];
Player suspect = UtilPlayer.searchOnline(reporter, playerName, false);
String reason = F.combine(args, 1, null, false);
if (suspect != null)
{
// allow developer (iKeirNez) to report himself (for easy testing reasons)
if (suspect == reporter && !reportManager.isDevMode(reporter.getUniqueId()))
{
UtilPlayer.message(reporter, F.main(Plugin.getName(), C.cRed + "You cannot report yourself."));
}
else
{
CoreClient suspectClient = clientManager.Get(suspect);
new ReportCategoryPage(Plugin, reporter, reporterId, suspectClient, reason).openInventory();
}
}
else
{
clientManager.loadClientByName(playerName, suspectClient ->
{
if (suspectClient != null)
{
new ReportCategoryPage(Plugin, reporter, reporterId, suspectClient, reason).openInventory();
}
else
{
UtilPlayer.message(reporter, F.main(Plugin.getName(), C.cRed + "Unable to find player '"
+ playerName + "'!"));
}
});
}
}
}
else
{
UtilPlayer.message(player, F.main(Plugin.getName(), C.cRed + "Unable to find player '"
+ playerName + "'!"));
UtilPlayer.message(reporter, C.cRed + "You are banned from using the report feature.");
}
}
}

View File

@ -1,35 +1,106 @@
package mineplex.core.report.command;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import mineplex.core.command.CommandBase;
import mineplex.core.common.Rank;
import mineplex.core.common.util.BukkitFuture;
import mineplex.core.common.util.C;
import mineplex.core.common.util.F;
import mineplex.core.common.util.UtilPlayer;
import mineplex.core.report.ReportManager;
import mineplex.core.report.ReportPlugin;
import mineplex.core.report.data.Report;
import mineplex.core.report.data.ReportRepository;
import org.bukkit.entity.Player;
/**
* When executed, the user is appointed handler of the most important report in the report queue (if any).
* A user may only handle 1 report at a time.
*/
public class ReportHandleCommand extends CommandBase<ReportPlugin>
{
public ReportHandleCommand(ReportPlugin plugin)
{
super(plugin, Rank.MODERATOR, "reporthandle", "rh");
}
@Override
public void Execute(final Player player, final String[] args)
{
if(args == null || args.length < 1)
{
if (args == null || args.length == 0)
{
UtilPlayer.message(player, F.main(Plugin.getName(), C.cRed + "Your arguments are inappropriate for this command!"));
ReportManager reportManager = Plugin.getReportManager();
ReportRepository reportRepository = reportManager.getReportRepository();
int accountId = _commandCenter.GetClientManager().getAccountId(player);
reportManager.isHandlingReport(player).thenAccept(isHandlingReport ->
{
if (!isHandlingReport)
{
Map<Report, Double> reportPriorities = Collections.synchronizedMap(new HashMap<>());
boolean devMode = reportManager.isDevMode(player.getUniqueId());
// the below fetches the ids of all unhandled reports and gets a Report object for each of these ids
// the priority of the report is then calculated and the results placed in a map
reportRepository.getUnhandledReports(accountId, devMode).thenCompose(reportRepository::getReports).thenAccept(reports ->
CompletableFuture.allOf(reports.stream().map(report ->
reportManager.calculatePriority(report).thenAccept(priority ->
{
if (priority > 0)
{
reportPriorities.put(report, priority);
}
else
{
// mark the report as expired to keep the database clean
// and reduce future query time
reportManager.expireReport(report);
}
}
)
).toArray(CompletableFuture[]::new)).join()
).thenApply(aVoid ->
{
Map.Entry<Report, Double> mostImportant = null;
for (Map.Entry<Report, Double> entry : reportPriorities.entrySet())
{
if (mostImportant == null || (double) entry.getValue() > mostImportant.getValue())
{
mostImportant = entry;
}
}
return mostImportant == null ? null : mostImportant.getKey();
}).thenCompose(BukkitFuture.accept(report ->
{
if (report != null)
{
reportManager.handleReport(report, player);
}
else
{
UtilPlayer.message(player, F.main(Plugin.getName(), C.cRed + "No report found, report queue is empty."));
}
}));
}
else
{
Bukkit.getScheduler().runTask(Plugin.getPlugin(), () ->
UtilPlayer.message(player, F.main(Plugin.getName(), C.cRed + "You are already handling a report.")));
}
});
}
else
{
int reportId = Integer.parseInt(args[0]);
Plugin.getReportManager().handleReport(reportId, player);
UtilPlayer.message(player, F.main(Plugin.getName(), C.cRed + "Invalid Usage: " + F.elem("/" + _aliasUsed)));
}
}
}

View File

@ -1,42 +0,0 @@
package mineplex.core.report.command;
import mineplex.core.common.jsonchat.JsonMessage;
import mineplex.core.report.Report;
/**
* A message regarding a report which is sent only to the player handling the report.
*/
public class ReportHandlerNotification extends ReportNotification
{
private int _reportId;
private String _server; // the server the incident took place on
public ReportHandlerNotification(Report report, String notification)
{
this(report, new JsonMessage(notification));
}
public ReportHandlerNotification(Report report, JsonMessage notification)
{
super(notification);
if (report.getHandler() == null)
{
throw new IllegalStateException("Report has no handler.");
}
_reportId = report.getReportId();
_server = report.getServerName();
setTargetServers(_server);
}
public int getReportId()
{
return _reportId;
}
public String getServer()
{
return _server;
}
}

View File

@ -0,0 +1,100 @@
package mineplex.core.report.command;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import org.bukkit.entity.Player;
import mineplex.core.command.CommandBase;
import mineplex.core.common.Rank;
import mineplex.core.common.util.C;
import mineplex.core.common.util.F;
import mineplex.core.common.util.UtilPlayer;
import mineplex.core.report.ReportManager;
import mineplex.core.report.ReportPlugin;
import mineplex.core.report.ReportResult;
import mineplex.core.report.data.Report;
import mineplex.core.report.data.ReportMessage;
import mineplex.core.report.data.ReportRepository;
/**
* Provides the sender of the command with information of the supplied report.
*/
public class ReportInfoCommand extends CommandBase<ReportPlugin>
{
public ReportInfoCommand(ReportPlugin plugin)
{
super(plugin, Rank.MODERATOR, "reportinfo");
}
@Override
public void Execute(Player player, String[] args)
{
if (args != null && args.length == 1)
{
try
{
long reportId = Long.parseLong(args[0]);
ReportManager reportManager = Plugin.getReportManager();
ReportRepository repository = reportManager.getReportRepository();
repository.getReport(reportId).thenAccept(reportOptional ->
{
if (reportOptional.isPresent())
{
Report report = reportOptional.get();
String prefix = ReportManager.getReportPrefix(report);
String suspect = repository.getAccountName(report.getSuspectId()).join();
String handler = report.getHandlerId().map(handlerId -> repository.getAccountName(handlerId).join()).orElse("None");
UtilPlayer.message(player, F.main(prefix, "Type: " + F.elem(report.getCategory().getName())));
UtilPlayer.message(player, F.main(prefix, "Suspect: " + F.elem(suspect)));
UtilPlayer.message(player, F.main(prefix, "Handler: " + F.elem(handler)));
UtilPlayer.message(player, F.main(prefix, ""));
UtilPlayer.message(player, F.main(prefix, F.elem("Reporters")));
for (Map.Entry<Integer, ReportMessage> entry : report.getReportMessages().entrySet())
{
String reporter = repository.getAccountName(entry.getKey()).join();
ReportMessage message = entry.getValue();
UtilPlayer.message(player, F.main(prefix, reporter + ": " + F.elem(message.getMessage())));
}
UtilPlayer.message(player, F.main(prefix, ""));
Optional<ReportResult> resultOptional = report.getResult();
String status = resultOptional.map(reportResult -> reportResult.getType().getName()).orElse("Unresolved");
UtilPlayer.message(player, F.main(prefix, "Status: " + F.elem(status)));
if (resultOptional.isPresent())
{
ReportResult result = resultOptional.get();
String handlerMessage = result.getReason().orElse("None specified.");
UtilPlayer.message(player, F.main(prefix, "Handler Message: " + F.elem(handlerMessage)));
}
}
else
{
UtilPlayer.message(player, F.main(Plugin.getName(), C.cRed + "Couldn't find a report with that id, please try again"));
}
}).exceptionally(throwable ->
{
Plugin.getPlugin().getLogger().log(Level.SEVERE, "Error whilst getting report info.", throwable);
return null;
});
}
catch (NumberFormatException e)
{
UtilPlayer.message(player, F.main(Plugin.getName(), C.cRed + "Invalid report id"));
}
}
else
{
UtilPlayer.message(player, F.main(Plugin.getName(), C.cRed + "Invalid Usage: " + F.elem("/" + _aliasUsed + " <report-id>")));
}
}
}

View File

@ -1,33 +0,0 @@
package mineplex.core.report.command;
import mineplex.core.common.jsonchat.JsonMessage;
import mineplex.serverdata.commands.ServerCommand;
/**
* A message regarding a report which should be sent to all moderators with report notifications enabled.
*/
public class ReportNotification extends ServerCommand
{
private String _notification; // in json format
public ReportNotification(String notification)
{
this(new JsonMessage(notification));
}
public ReportNotification(JsonMessage notification)
{
super(); // Send to all servers
_notification = notification.toString();
}
public String getNotification()
{
return _notification;
}
public void run()
{
// Utilitizes a callback functionality to seperate dependencies
}
}

View File

@ -1,70 +0,0 @@
package mineplex.core.report.command;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import mineplex.core.common.util.UtilServer;
import mineplex.core.report.Report;
import mineplex.core.report.ReportManager;
import mineplex.serverdata.commands.CommandCallback;
import mineplex.serverdata.commands.ServerCommand;
/**
* Handles receiving of report notifications.
*/
public class ReportNotificationCallback implements CommandCallback
{
private ReportManager _reportManager;
public ReportNotificationCallback(ReportManager reportManager)
{
_reportManager = reportManager;
}
@Override
public void run(ServerCommand command)
{
if (command instanceof ReportHandlerNotification)
{
ReportHandlerNotification reportNotification = (ReportHandlerNotification) command;
Report report = _reportManager.getReport(reportNotification.getReportId());
if (report != null)
{
UUID handlerUUID = report.getHandler();
if (handlerUUID != null)
{
Player handler = Bukkit.getPlayer(handlerUUID);
if (handler != null)
{
sendRawMessage(handler, reportNotification.getNotification());
}
}
}
}
else if (command instanceof ReportNotification)
{
ReportNotification reportNotification = (ReportNotification) command;
// Message all players that can receive report notifications.
for (Player player : UtilServer.getPlayers())
{
if (_reportManager.hasReportNotifications(player))
{
sendRawMessage(player, reportNotification.getNotification());
}
}
}
}
private void sendRawMessage(Player player, String rawMessage)
{
Server server = UtilServer.getServer();
server.dispatchCommand(server.getConsoleSender(), "tellraw " + player.getName() + " " + rawMessage);
}
}

View File

@ -0,0 +1,88 @@
package mineplex.core.report.command;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import com.google.common.primitives.Longs;
import mineplex.core.command.CommandBase;
import mineplex.core.common.Rank;
import mineplex.core.common.jsonchat.ChildJsonMessage;
import mineplex.core.common.jsonchat.ClickEvent;
import mineplex.core.common.jsonchat.JsonMessage;
import mineplex.core.common.util.BukkitFuture;
import mineplex.core.common.util.C;
import mineplex.core.common.util.F;
import mineplex.core.common.util.UtilPlayer;
import mineplex.core.report.ReportPlugin;
import mineplex.core.report.ReportRole;
/**
* A staff command for viewing report related statistics of a player.
*/
public class ReportStatsCommand extends CommandBase<ReportPlugin>
{
public ReportStatsCommand(ReportPlugin reportPlugin)
{
super(reportPlugin, Rank.MODERATOR, "reportstats", "rs");
}
@Override
public void Execute(Player player, String[] args)
{
if (args != null && args.length == 1)
{
String playerName = args[0];
Player target = Bukkit.getPlayer(playerName);
if (target != null)
{
int accountId = _commandCenter.GetClientManager().getAccountId(target);
Plugin.getReportManager().getReportRepository().getAccountStatistics(accountId).thenCompose(BukkitFuture.accept(stats ->
stats.keySet().forEach(role ->
{
long[] idArray = stats.get(role).stream()
.sorted((l1, l2) -> Longs.compare(l2, l1))
.mapToLong(l -> l)
.toArray();
int reportCount = idArray.length;
// don't show handler statistics if user has never handled a report
if (role != ReportRole.HANDLER || reportCount > 0)
{
// create clickable report ids
ChildJsonMessage jsonMessage = new JsonMessage(F.main(Plugin.getName(), ""))
.extra(C.mElem);
int displayAmount = 5;
for (int i = 0; i < displayAmount; i++)
{
long reportId = idArray[i];
jsonMessage = jsonMessage.add(String.valueOf(reportId))
.click(ClickEvent.RUN_COMMAND, "/reportinfo " + reportId);
if (reportId != displayAmount)
{
jsonMessage = jsonMessage.add(", ");
}
}
UtilPlayer.message(player, F.main(Plugin.getName(), F.elem(role.getHumanName()) + " (" + reportCount + ")"));
jsonMessage.sendToPlayer(player);
}
})
));
}
else
{
UtilPlayer.message(player, F.main(Plugin.getName(), C.cRed + "Player not found."));
}
}
else
{
UtilPlayer.message(player, F.main(Plugin.getName(), C.cRed + "Invalid Usage: " + F.elem("/" + _aliasUsed + " <player>")));
}
}
}

View File

@ -0,0 +1,155 @@
package mineplex.core.report.data;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import mineplex.core.report.ReportCategory;
import mineplex.core.report.ReportHandlerTask;
import mineplex.core.report.ReportResult;
import mineplex.core.report.ReportTeam;
/**
* Holds data for a Report.
*/
public class Report
{
protected Long _reportId;
private final int _suspectId;
private final ReportCategory _category;
// set of player account ids and the reason they reported this player
private final Map<Integer, ReportMessage> _reportMessages = new HashMap<>();
private Integer _handlerId = null;
private Integer _snapshotId = null;
private ReportResult _reportResult = null;
private ReportTeam _assignedTeam = null;
private ReportHandlerTask _handlerTask = null;
public Report(int suspectId, ReportCategory category)
{
this(null, suspectId, category);
}
protected Report(Long reportId, int suspectId, ReportCategory category)
{
_reportId = reportId;
_suspectId = suspectId;
_category = category;
}
public Optional<Long> getId()
{
return Optional.ofNullable(_reportId);
}
public int getSuspectId()
{
return _suspectId;
}
public ReportCategory getCategory()
{
return _category;
}
public Map<Integer, ReportMessage> getMessages()
{
return _reportMessages;
}
public void addReportReason(ReportMessage reportMessage)
{
_reportMessages.put(reportMessage.getReporterId(), reportMessage);
}
public Map<Integer, ReportMessage> getReportMessages()
{
return _reportMessages;
}
public ReportMessage getReportMessage(int accountId)
{
return _reportMessages.get(accountId);
}
public Set<Integer> getReporterIds()
{
return _reportMessages.keySet();
}
public Optional<Integer> getHandlerId()
{
return Optional.ofNullable(_handlerId);
}
public void setHandlerId(Integer handlerId)
{
_handlerId = handlerId;
}
public Optional<Integer> getSnapshotId()
{
return Optional.ofNullable(_snapshotId);
}
public void setSnapshotId(Integer snapshotId)
{
_snapshotId = snapshotId;
}
public Optional<ReportResult> getResult()
{
return Optional.ofNullable(_reportResult);
}
public void setReportResult(ReportResult reportResult)
{
_reportResult = reportResult;
}
public Optional<ReportTeam> getAssignedTeam()
{
return Optional.ofNullable(_assignedTeam);
}
public void setAssignedTeam(ReportTeam assignedTeam)
{
_assignedTeam = assignedTeam;
}
public ReportMessage getLatestMessage()
{
ReportMessage latest = null;
for (ReportMessage reportMessage : _reportMessages.values())
{
if (latest == null || reportMessage.getTimeCreated().isAfter(latest.getTimeCreated()))
{
latest = reportMessage;
}
}
return latest;
}
public Optional<ReportHandlerTask> getHandlerTask()
{
return Optional.ofNullable(_handlerTask);
}
public void setHandlerTask(ReportHandlerTask handlerTask)
{
_handlerTask = handlerTask;
}
public void cancelHandlerTask()
{
if (_handlerTask != null)
{
_handlerTask.cancel();
_handlerTask = null;
}
}
}

View File

@ -0,0 +1,73 @@
package mineplex.core.report.data;
import java.time.Duration;
import java.time.LocalDateTime;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Stores data about a single player report
*
* One or more of these make-up a {@link Report}.
*/
public class ReportMessage
{
private int _reporterId;
private String _message;
private String _server;
private int _serverWeight;
private LocalDateTime _time;
public ReportMessage(int reporterId, String message, String server, int serverWeight)
{
this(reporterId, message, server, serverWeight, LocalDateTime.now());
}
public ReportMessage(int reporterId, String message, String server, int serverWeight, LocalDateTime time)
{
checkNotNull(message);
checkNotNull(server);
checkNotNull(time);
_reporterId = reporterId;
_message = message;
_server = server;
_serverWeight = serverWeight;
_time = time;
}
public int getReporterId()
{
return _reporterId;
}
public String getMessage()
{
return _message;
}
public void setMessage(String message)
{
_message = message;
}
public String getServer()
{
return _server;
}
public int getServerWeight()
{
return _serverWeight;
}
public LocalDateTime getTimeCreated()
{
return _time;
}
public Duration getDurationSinceCreation()
{
return Duration.between(_time, LocalDateTime.now());
}
}

View File

@ -0,0 +1,898 @@
package mineplex.core.report.data;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bukkit.Bukkit;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.mysql.jdbc.Statement;
import mineplex.core.common.util.UtilTime;
import mineplex.core.report.ReportCategory;
import mineplex.core.report.ReportManager;
import mineplex.core.report.ReportResult;
import mineplex.core.report.ReportResultType;
import mineplex.core.report.ReportRole;
import mineplex.core.report.ReportTeam;
import mineplex.serverdata.database.DBPool;
import org.apache.commons.lang3.StringUtils;
/**
* Handles saving and loading report data to and from the database.
*/
public class ReportRepository
{
private static final String INSERT_REPORT = "INSERT INTO reports (suspectId, categoryId, snapshotId, assignedTeam)\n" +
"VALUES (?, ?, ?, ?);";
private static final String UPDATE_REPORT = "UPDATE reports SET snapshotId = ?, assignedTeam = ? WHERE id = ?;";
private static final String SET_REPORT_MESSAGE = "REPLACE INTO reportReasons (reportId, reporterId, reason, `server`, weight, `time`)" +
" VALUES (?, ?, ?, ?, ?, ?);";
private static final String SET_REPORT_HANDLER = "REPLACE INTO reportHandlers (reportId, handlerId)" +
" VALUES (?, ?);";
private static final String SET_REPORT_RESULT = "REPLACE INTO reportResults (reportId, resultId, reason, closedTime)" +
" VALUES (?, ?, ?, ?);";
private static final String SET_HANDLER_ABORTED = "UPDATE reportHandlers" +
" SET aborted = ?" +
" WHERE reportId = ?" +
" AND handlerId = ?;";
private static final String GET_REPORT = "SELECT * FROM reports" +
" LEFT JOIN reportHandlers ON reports.id = reportHandlers.reportId AND reportHandlers.aborted IS FALSE" +
" LEFT JOIN reportResults ON reports.id = reportResults.reportId" +
" WHERE reports.id = ?;";
private static final String GET_REPORT_REASONS = "SELECT * FROM reportReasons" +
" WHERE reportId = ?" +
" ORDER BY `time` ASC;";
private static final String GET_UNHANDLED_REPORTS = "SELECT reports.id FROM reports\n" +
" LEFT JOIN reportResults ON reports.id = reportResults.reportId\n" +
" LEFT JOIN reportHandlers ON reports.id = reportHandlers.reportId\n" +
" LEFT JOIN reportReasons ON reports.id = reportReasons.reportId\n" +
"WHERE reportResults.reportId IS NULL\n" +
" /* Bypass for testing purposes or check player isn't suspect */\n" +
" AND (? IS TRUE OR reports.suspectId != ?)\n" +
" /* If team is assigned, make sure user is member of team */\n" +
" AND (reports.assignedTeam IS NULL OR reports.assignedTeam IN\n" +
" (SELECT teamId FROM reportTeamMemberships WHERE accountId = ?))\n" +
"GROUP BY reports.id\n" +
"HAVING SUM(IF(reportHandlers.handlerId != ?, 1, 0)) = COUNT(reportHandlers.handlerId)\n" +
" /* Check all previous handlers have aborted this report */\n" +
" AND SUM(IF(reportHandlers.aborted IS TRUE, 1, 0)) = COUNT(reportHandlers.handlerId)\n" +
" /* Bypass for testing purposes or check player isn't a reporter */\n" +
" AND (? IS TRUE OR SUM(IF(reportReasons.reporterId != ?, 1, 0)) = COUNT(reportReasons.reporterId));";
private static final String GET_ONGOING_REPORT = "SELECT reports.id FROM reports" +
" LEFT JOIN reportResults ON reports.id = reportResults.reportId" +
" WHERE reportResults.reportId IS NULL" +
" AND reports.suspectId = ?;";
private static final String GET_ONGOING_REPORT_CATEGORY = "SELECT reports.id FROM reports" +
" LEFT JOIN reportResults ON reports.id = reportResults.reportId" +
" WHERE reportResults.reportId IS NULL" +
" AND reports.suspectId = ?" +
" AND reports.categoryId = ?;";
private static final String GET_REPORTS_HANDLING = "SELECT reports.id FROM reports" +
" LEFT JOIN reportResults ON reports.id = reportResults.reportId" +
" INNER JOIN reportHandlers ON reports.id = reportHandlers.reportId" +
" AND reportHandlers.aborted IS FALSE" +
" WHERE reportResults.reportId IS NULL" +
" AND reportHandlers.handlerId = ?;";
private static final String GET_USER_RESULT_COUNT = "SELECT COUNT(reports.id) AS resultCount" +
" FROM reports, reportReasons, reportResults" +
" WHERE reports.id = reportReasons.reportId" +
" AND reports.id = reportResults.reportId" +
" AND reportReasons.reporterId = ?" +
" AND reportResults.resultId = ?;";
private static final String GET_ACCOUNT_NAME = "SELECT id, `name` FROM accounts" +
" WHERE id = ?" +
" LIMIT 1;";
private static final String GET_ACCOUNT_UUID = "SELECT id, uuid FROM accounts" +
" WHERE id IN (%s);";
/** STATISTICS **/
private static final String STATISTICS_GET_REPORTS_MADE = "SELECT reports.id FROM reports, reportReasons\n" +
"WHERE reports.id = reportReasons.reportId\n" +
" AND reportReasons.reporterId = ?;";
private static final String STATISTICS_GET_REPORTS_HANDLED = "SELECT reports.id FROM reports, reportHandlers\n" +
"WHERE reports.id = reportHandlers.reportId\n" +
" AND reportHandlers.handlerId = ?\n" +
" AND reportHandlers.aborted IS FALSE;";
private static final String STATISTICS_GET_REPORTS_AGAINST = "SELECT reports.id FROM reports\n" +
"WHERE reports.suspectId = ?;";
private final ReportManager _reportManager;
private final Logger _logger;
private final Cache<Long, Report> _cachedReports = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterAccess(5, TimeUnit.MINUTES)
.build();
public ReportRepository(ReportManager reportManager, Logger logger)
{
_reportManager = reportManager;
_logger = logger;
}
/**
* Gets the ids of unhandled reports that the supplied user is allowed to handle.
*
* @param accountId the id of the account carrying out the search
* @param devMode if true, allows various restrictions to be bypassed
* @return the ids of unhandled reports the supplied account is allowed to handle
*/
public CompletableFuture<List<Long>> getUnhandledReports(int accountId, boolean devMode)
{
CompletableFuture<List<Long>> future = CompletableFuture.supplyAsync(() ->
{
List<Long> unhandledReports = new ArrayList<>();
try (Connection connection = DBPool.getAccount().getConnection())
{
PreparedStatement preparedStatement = connection.prepareStatement(GET_UNHANDLED_REPORTS);
preparedStatement.setBoolean(1, devMode);
preparedStatement.setInt(2, accountId);
preparedStatement.setInt(3, accountId);
preparedStatement.setInt(4, accountId);
preparedStatement.setBoolean(5, devMode);
preparedStatement.setInt(6, accountId);
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next())
{
unhandledReports.add(resultSet.getLong("id"));
}
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
return unhandledReports;
});
future.exceptionally(throwable ->
{
_logger.log(Level.SEVERE, "Error whilst fetching unhandled reports.", throwable);
return new ArrayList<>();
});
return future;
}
/**
* Gets a list containing the ids of reports the account is handling
* @param accountId the id of the account
* @return a list containing the ids of reports being handled
*/
public CompletableFuture<List<Long>> getReportsHandling(int accountId)
{
CompletableFuture<List<Long>> future = CompletableFuture.supplyAsync(() ->
{
List<Long> reportsHandling = new ArrayList<>();
try (Connection connection = DBPool.getAccount().getConnection())
{
PreparedStatement preparedStatement = connection.prepareStatement(GET_REPORTS_HANDLING);
preparedStatement.setInt(1, accountId);
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next())
{
reportsHandling.add(resultSet.getLong("reports.id"));
}
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
return reportsHandling;
});
future.exceptionally(throwable ->
{
_logger.log(Level.SEVERE, "Error fetching reports being handled by specified account.", throwable);
return new ArrayList<>();
});
return future;
}
/**
* Get reports by id in bulk.
*
* @param reportIds the ids of the reports to fetch
* @return the requested reports
*/
public CompletableFuture<List<Report>> getReports(Collection<Long> reportIds)
{
return CompletableFuture.supplyAsync(() ->
{
try (Connection connection = DBPool.getAccount().getConnection())
{
List<Report> reports = new ArrayList<>();
PreparedStatement preparedStatement = connection.prepareStatement(GET_REPORT);
for (long reportId : reportIds)
{
preparedStatement.setLong(1, reportId);
ResultSet resultSet = preparedStatement.executeQuery();
Report report = loadReport(connection, resultSet);
if (report != null)
{
reports.add(report);
}
}
return reports;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
});
}
/**
* Gets a report by id.
*
* Attempts to fetch the report from local cache, if the report is not present in cache
* then the report is fetched from the database.
*
* @param reportId the id of the report to fetch
* @return the report, null if report by that id wasn't found
*/
public CompletableFuture<Optional<Report>> getReport(long reportId)
{
if (reportId != -1)
{
Report cachedReport = _cachedReports.getIfPresent(reportId);
if (cachedReport != null)
{
return CompletableFuture.completedFuture(Optional.of(cachedReport));
}
else
{
CompletableFuture<Optional<Report>> future = CompletableFuture.supplyAsync(() ->
{
try (Connection connection = DBPool.getAccount().getConnection())
{
PreparedStatement preparedStatement = connection.prepareStatement(GET_REPORT);
preparedStatement.setLong(1, reportId);
ResultSet resultSet = preparedStatement.executeQuery();
return Optional.ofNullable(loadReport(connection, resultSet));
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
});
future.exceptionally(throwable ->
{
_logger.log(Level.SEVERE, "Error fetching report (id: " + reportId + ").", throwable);
return null;
});
return future;
}
}
else
{
return CompletableFuture.completedFuture(null);
}
}
private Report loadReport(Connection connection, ResultSet resultSet) throws SQLException
{
if (resultSet.next())
{
long reportId = resultSet.getLong("id");
int suspectId = resultSet.getInt("suspectId");
ReportCategory reportCategory = ReportCategory.getById(resultSet.getInt("categoryId"));
Report report = new Report(reportId, suspectId, reportCategory);
int snapshotId = resultSet.getInt("snapshotId");
if (!resultSet.wasNull())
{
report.setSnapshotId(snapshotId);
}
int handlerId = resultSet.getInt("handlerId");
if (!resultSet.wasNull())
{
report.setHandlerId(handlerId);
}
short teamId = resultSet.getShort("assignedTeam");
if (!resultSet.wasNull())
{
ReportTeam team = ReportTeam.getById(teamId);
if (team != null)
{
report.setAssignedTeam(team);
}
else
{
_logger.log(Level.WARNING, "Unrecognised report team found in database: " + teamId);
}
}
Set<ReportMessage> reportMessages = getReportReasons(connection, reportId);
reportMessages.forEach(report::addReportReason);
int resultId = resultSet.getInt("resultId");
if (!resultSet.wasNull())
{
ReportResultType resultType = ReportResultType.getById(resultId);
String reason = resultSet.getString("reason");
LocalDateTime closedTime = UtilTime.fromTimestamp(resultSet.getTimestamp("closedTime"));
report.setReportResult(new ReportResult(resultType, reason, closedTime));
}
shouldCacheReport(report).thenAccept(shouldCache ->
{
if (shouldCache)
{
_cachedReports.put(reportId, report);
}
});
return report;
}
return null;
}
public CompletableFuture<List<Report>> getOngoingReports(int accountId)
{
CompletableFuture<List<Report>> future = CompletableFuture.supplyAsync(() ->
{
try (Connection connection = DBPool.getAccount().getConnection())
{
List<Report> reports = new ArrayList<>();
PreparedStatement preparedStatement = connection.prepareStatement(GET_ONGOING_REPORT);
preparedStatement.setInt(1, accountId);
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next())
{
try
{
int id = resultSet.getInt("id");
Optional<Report> reportOptional = getReport(id).join();
if (reportOptional.isPresent())
{
Report report = reportOptional.get();
if (_reportManager.isActiveReport(report).join())
{
reports.add(report);
}
}
}
catch (Exception e)
{
_logger.log(Level.SEVERE, "Error whilst getting ongoing reports.", e);
}
}
return reports;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
});
future.exceptionally(throwable ->
{
_logger.log(Level.SEVERE, "Error getting ongoing report for account: " + accountId + ".", throwable);
return null;
});
return future;
}
public CompletableFuture<List<Long>> getOngoingReports(int accountId, ReportCategory category)
{
CompletableFuture<List<Long>> future = CompletableFuture.supplyAsync(() ->
{
try (Connection connection = DBPool.getAccount().getConnection())
{
PreparedStatement preparedStatement = connection.prepareStatement(GET_ONGOING_REPORT_CATEGORY);
preparedStatement.setInt(1, accountId);
preparedStatement.setInt(2, category.getId());
ResultSet resultSet = preparedStatement.executeQuery();
List<Long> reports = new ArrayList<>();
while (resultSet.next())
{
reports.add(resultSet.getLong("id"));
}
return reports;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
});
future.exceptionally(throwable ->
{
_logger.log(Level.SEVERE, "Error fetching ongoing report for account: " + accountId + ", category: " + category + ".", throwable);
return new ArrayList<>();
});
return future;
}
private Set<ReportMessage> getReportReasons(Connection connection, long reportId)
{
Set<ReportMessage> reportMessages = new HashSet<>();
try
{
PreparedStatement preparedStatement = connection.prepareStatement(GET_REPORT_REASONS);
preparedStatement.setLong(1, reportId);
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next())
{
int reporterId = resultSet.getInt("reporterId");
String reason = resultSet.getString("reason");
String server = resultSet.getString("server");
int weight = resultSet.getInt("weight");
LocalDateTime date = UtilTime.fromTimestamp(resultSet.getTimestamp("time"));
ReportMessage reportMessage = new ReportMessage(reporterId, reason, server, weight, date);
reportMessages.add(reportMessage);
}
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
return reportMessages;
}
public CompletableFuture<Integer> getResultCount(int accountId, ReportResultType resultType)
{
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
try (Connection connection = DBPool.getAccount().getConnection())
{
PreparedStatement preparedStatement = connection.prepareStatement(GET_USER_RESULT_COUNT);
preparedStatement.setInt(1, accountId);
preparedStatement.setInt(2, resultType.getId());
ResultSet resultSet = preparedStatement.executeQuery();
if (resultSet.next())
{
return resultSet.getInt("resultCount");
}
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
return null;
});
future.exceptionally(throwable ->
{
_logger.log(Level.SEVERE, "Error fetching result count for account: " + accountId + ", type: " + resultType + ".", throwable);
return 0;
});
return future;
}
public CompletableFuture<Long> updateReport(Report report)
{
CompletableFuture<Long> future = CompletableFuture.supplyAsync(() ->
{
try (Connection connection = DBPool.getAccount().getConnection())
{
Optional<Long> reportIdOptional = report.getId();
Optional<Integer> snapshotIdOptional = report.getSnapshotId();
Optional<ReportTeam> teamOptional = report.getAssignedTeam();
long reportId;
if (reportIdOptional.isPresent())
{
reportId = reportIdOptional.get();
try (PreparedStatement updateReportStatement = connection.prepareStatement(UPDATE_REPORT))
{
if (snapshotIdOptional.isPresent())
{
updateReportStatement.setInt(1, snapshotIdOptional.get());
}
else
{
updateReportStatement.setNull(1, Types.INTEGER);
}
if (teamOptional.isPresent())
{
updateReportStatement.setShort(2, teamOptional.get().getDatabaseId());
}
else
{
updateReportStatement.setNull(2, Types.TINYINT);
}
updateReportStatement.setLong(3, reportId);
updateReportStatement.execute();
}
}
else
{
try (PreparedStatement insertReportStatement = connection.prepareStatement(INSERT_REPORT, Statement.RETURN_GENERATED_KEYS))
{
insertReportStatement.setInt(1, report.getSuspectId());
insertReportStatement.setInt(2, report.getCategory().getId());
if (snapshotIdOptional.isPresent())
{
insertReportStatement.setInt(3, snapshotIdOptional.get());
}
else
{
insertReportStatement.setNull(3, Types.INTEGER);
}
if (teamOptional.isPresent())
{
insertReportStatement.setInt(4, teamOptional.get().getDatabaseId());
}
else
{
insertReportStatement.setNull(4, Types.TINYINT);
}
insertReportStatement.executeUpdate();
ResultSet resultSet = insertReportStatement.getGeneratedKeys();
if (resultSet.next())
{
reportId = resultSet.getLong(1);
report._reportId = reportId;
}
else
{
throw new IllegalStateException("Query did not return a report id (we need one).");
}
}
}
PreparedStatement setReportMessageStatement = connection.prepareStatement(SET_REPORT_MESSAGE);
for (Map.Entry<Integer, ReportMessage> entry : report.getMessages().entrySet())
{
ReportMessage reportMessage = entry.getValue();
setReportMessageStatement.setLong(1, reportId); // report id
setReportMessageStatement.setInt(2, entry.getKey()); // reporter id
setReportMessageStatement.setString(3, reportMessage.getMessage()); // reason
setReportMessageStatement.setString(4, reportMessage.getServer()); // server
setReportMessageStatement.setInt(5, reportMessage.getServerWeight()); // weight
setReportMessageStatement.setTimestamp(6, UtilTime.toTimestamp(reportMessage.getTimeCreated())); // time
setReportMessageStatement.addBatch();
}
setReportMessageStatement.executeBatch();
Optional<Integer> handlerIdOptional = report.getHandlerId();
if (handlerIdOptional.isPresent())
{
PreparedStatement setReportHandlerStatement = connection.prepareStatement(SET_REPORT_HANDLER);
setReportHandlerStatement.setLong(1, reportId); // report id
setReportHandlerStatement.setInt(2, handlerIdOptional.get()); // handler id
setReportHandlerStatement.execute();
}
Optional<ReportResult> reportResultOptional = report.getResult();
if (reportResultOptional.isPresent())
{
PreparedStatement setReportResultStatement = connection.prepareStatement(SET_REPORT_RESULT);
ReportResult reportResult = reportResultOptional.get();
setReportResultStatement.setLong(1, reportId); // report id
setReportResultStatement.setInt(2, reportResult.getType().getId()); // result id
setReportResultStatement.setString(3, reportResult.getReason().orElse(null)); // reason
setReportResultStatement.setTimestamp(4, new Timestamp(reportResult.getClosedTime().atZone(UtilTime.CENTRAL_ZONE).toInstant().toEpochMilli())); // closed time
setReportResultStatement.execute();
}
return reportId;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
});
future.exceptionally(throwable ->
{
Optional<Long> idOptional = report.getId();
_logger.log(Level.SEVERE, "Error updating report" + idOptional.map(id -> "(#" + id + ")").orElse("") + ".", throwable);
return null;
});
return future;
}
public CompletableFuture<Void> setAborted(long reportId, int handlerId, boolean aborted)
{
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() ->
{
try (Connection connection = DBPool.getAccount().getConnection())
{
PreparedStatement preparedStatement = connection.prepareStatement(SET_HANDLER_ABORTED);
preparedStatement.setBoolean(1, aborted);
preparedStatement.setLong(2, reportId);
preparedStatement.setInt(3, handlerId);
preparedStatement.execute();
return null;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
});
future.exceptionally(throwable ->
{
_logger.log(Level.SEVERE, "Error setting handler report as aborted.", throwable);
return null;
});
return future;
}
public CompletableFuture<Multimap<ReportRole, Long>> getAccountStatistics(int accountId)
{
CompletableFuture<Multimap<ReportRole, Long>> future = CompletableFuture.supplyAsync(() ->
{
try (Connection connection = DBPool.getAccount().getConnection())
{
Multimap<ReportRole, Long> reportIds = HashMultimap.create();
reportIds.putAll(ReportRole.REPORTER, getReports(connection, STATISTICS_GET_REPORTS_MADE, accountId));
reportIds.putAll(ReportRole.HANDLER, getReports(connection, STATISTICS_GET_REPORTS_HANDLED, accountId));
reportIds.putAll(ReportRole.SUSPECT, getReports(connection, STATISTICS_GET_REPORTS_AGAINST, accountId));
return reportIds;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
});
future.exceptionally(throwable ->
{
_logger.log(Level.SEVERE, "Error getting account statistics.", throwable);
return null;
});
return future;
}
private Set<Long> getReports(Connection connection, String sql, int accountId) throws SQLException
{
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setInt(1, accountId);
ResultSet resultSet = preparedStatement.executeQuery();
Set<Long> reportIds = new HashSet<>();
while (resultSet.next())
{
reportIds.add(resultSet.getLong("reports.id"));
}
return reportIds;
}
/**
* Disposes of cached reports which are cached as a result of this user.
* This function is called when a user leaves the server.
*
* @param accountId the account id to clean the cached reports of
*/
public void clearCacheFor(int accountId)
{
Iterator<Report> iterator = _cachedReports.asMap().values().iterator();
while (iterator.hasNext())
{
Report report = iterator.next();
Optional<Integer> handlerIdOptional = report.getHandlerId();
CompletableFuture<Boolean> disposeCacheFuture = CompletableFuture.completedFuture(false);
if (report.getSuspectId() == accountId)
{
if (handlerIdOptional.isPresent())
{
disposeCacheFuture = checkUserOnline(handlerIdOptional.get());
}
else
{
// no handler so un-cache this report
disposeCacheFuture = CompletableFuture.completedFuture(true);
}
}
else if (handlerIdOptional.isPresent() && handlerIdOptional.get() == accountId)
{
disposeCacheFuture = checkUserOnline(report.getSuspectId());
}
disposeCacheFuture.thenAccept(dispose ->
{
if (dispose)
{
iterator.remove();
}
});
}
}
public void clearCache(long reportId)
{
_cachedReports.invalidate(reportId);
}
/**
* Checks if either the suspect or handler (if any) are online.
* If either are online then this will return true, otherwise false.
*
* @param report the report to check if it should be cached
* @return true if this report should be cached, false otherwise
*/
private CompletableFuture<Boolean> shouldCacheReport(Report report)
{
return checkUserOnline(report.getSuspectId()).thenCompose(online ->
{
if (!online)
{
Optional<Integer> handlerIdOptional = report.getHandlerId();
if (handlerIdOptional.isPresent())
{
return checkUserOnline(handlerIdOptional.get());
}
else
{
return CompletableFuture.completedFuture(false);
}
}
return CompletableFuture.completedFuture(true);
});
}
private CompletableFuture<Boolean> checkUserOnline(int accountId)
{
return getAccountUUID(accountId).thenApply(Bukkit::getPlayer).thenApply(player -> player != null);
}
public CompletableFuture<String> getAccountName(int accountId)
{
CompletableFuture<String> future = CompletableFuture.supplyAsync(() ->
{
try (Connection connection = DBPool.getAccount().getConnection())
{
PreparedStatement preparedStatement = connection.prepareStatement(GET_ACCOUNT_NAME);
preparedStatement.setInt(1, accountId);
ResultSet resultSet = preparedStatement.executeQuery();
if (resultSet.next())
{
return resultSet.getString("name");
}
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
return null;
});
future.exceptionally(throwable ->
{
_logger.log(Level.SEVERE, "Error whilst fetching name for account: " + accountId + ".");
return null;
});
return future;
}
public CompletableFuture<UUID> getAccountUUID(int accountId)
{
return getAccountUUIDs(Collections.singletonList(accountId))
.thenApply(integerUUIDMap -> integerUUIDMap.get(accountId));
}
public CompletableFuture<Map<Integer, UUID>> getAccountUUIDs(Collection<Integer> accountIds)
{
CompletableFuture<Map<Integer, UUID>> future = CompletableFuture.supplyAsync(() ->
{
Map<Integer, UUID> accountUUIDs = new HashMap<>();
try (Connection connection = DBPool.getAccount().getConnection())
{
String query = String.format(GET_ACCOUNT_UUID, StringUtils.join(accountIds, ", "));
PreparedStatement preparedStatement = connection.prepareStatement(query);
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next())
{
int accountId = resultSet.getInt("id");
UUID accountUUID = UUID.fromString(resultSet.getString("uuid"));
accountUUIDs.put(accountId, accountUUID);
}
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
return accountUUIDs;
});
future.exceptionally(throwable ->
{
_logger.log(Level.SEVERE, "Error whilst fetching UUID(s).", throwable);
return new HashMap<>();
});
return future;
}
}

View File

@ -0,0 +1,159 @@
package mineplex.core.report.data;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import com.google.common.collect.TreeBasedTable;
import mineplex.core.report.ReportCategory;
import mineplex.core.report.ReportResultType;
import mineplex.core.report.ReportTeam;
/**
* Holds report specific data for a user.
* This includes player reputation
*/
public class ReportUser
{
private final static TreeBasedTable<ReportCategory, ReportResultType, Integer> RESULT_WORTH = TreeBasedTable.create();
static {
RESULT_WORTH.put(ReportCategory.GLOBAL, ReportResultType.ABUSIVE, -20);
RESULT_WORTH.put(ReportCategory.GLOBAL, ReportResultType.ACCEPTED, 10);
RESULT_WORTH.put(ReportCategory.CHAT_ABUSE, ReportResultType.DENIED, -5);
RESULT_WORTH.put(ReportCategory.HACKING, ReportResultType.DENIED, -1);
RESULT_WORTH.put(ReportCategory.GAMEPLAY, ReportResultType.DENIED, -1);
}
private final int _accountId;
private final Set<ReportTeam> _teams = new HashSet<>();
private final Map<ReportCategory, Map<ReportResultType, Integer>> _reputation;
public ReportUser(int accountId)
{
_accountId = accountId;
_reputation = Collections.synchronizedMap(new EnumMap<>(ReportCategory.class)); // allows map to be modified in database thread
for (ReportCategory category : ReportCategory.values())
{
Map<ReportResultType, Integer> resultValues = new EnumMap<>(ReportResultType.class);
for (ReportResultType resultType : ReportResultType.values())
{
// ensure global stats are only applied to GLOBAL and non-global stats are not applied to GLOBAL
if (resultType.isGlobalStat() == (category == ReportCategory.GLOBAL))
{
resultValues.put(resultType, 0);
}
}
_reputation.put(category, resultValues);
}
}
public int getAccountId()
{
return _accountId;
}
public Set<ReportTeam> getTeams()
{
return _teams;
}
public void addTeam(ReportTeam team)
{
_teams.add(team);
}
public void removeTeam(ReportTeam team)
{
_teams.remove(team);
}
public boolean hasTeam(ReportTeam team)
{
return _teams.contains(team);
}
/**
* Sets the value of a result type within the specified category.
*
* @param category the category type
* @param resultType the result type
* @param newValue the new value
*/
public void setValue(ReportCategory category, ReportResultType resultType, int newValue)
{
_reputation.get(category).put(resultType, newValue);
}
/**
* Gets the value of a result type within the specified category.
*
* @param category the category type
* @param resultType the result type
* @return the value
*/
public int getValue(ReportCategory category, ReportResultType resultType)
{
Map<ReportResultType, Integer> resultValues = _reputation.get(category);
if (resultValues == null || !resultValues.containsKey(resultType))
{
throw new UnsupportedOperationException("Cannot retrieve " + resultType.name() + " value for category " + category.name() + " (no global value either).");
}
return resultValues.get(resultType);
}
/**
* Gets the reputation of this user for the specified category.
* This takes into account the amount of accepted, denied and abusive reports the user has made.
*
* @param category the category to get the reputation for
* @return the reputation
*/
public int getReputation(ReportCategory category)
{
int accepted = getReputationPart(category, ReportResultType.ACCEPTED);
int denied = getReputationPart(category, ReportResultType.DENIED);
int abusive = getReputationPart(ReportCategory.GLOBAL, ReportResultType.ABUSIVE);
return Math.max(accepted + denied + abusive, 1);
}
/**
* Gets the users reputation for the specified category and result type.
*
* @param category the category
* @param resultType the result type
* @return the reputation
*/
private int getReputationPart(ReportCategory category, ReportResultType resultType)
{
return getValue(category, resultType) * getResultWorth(category, resultType);
}
/**
* Gets how much a result is worth for the category provided.
* If no worth value is found for the specified category, the value for {@link ReportCategory#GLOBAL} will be returned.
*
* @param reportCategory the category
* @param resultType the result type
* @return the worth value
*/
private static int getResultWorth(ReportCategory reportCategory, ReportResultType resultType)
{
if (RESULT_WORTH.contains(reportCategory, resultType))
{
return RESULT_WORTH.get(reportCategory, resultType);
}
else
{
return RESULT_WORTH.get(ReportCategory.GLOBAL, resultType);
}
}
}

View File

@ -0,0 +1,213 @@
package mineplex.core.report.data;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.stream.Collectors;
import org.bukkit.plugin.java.JavaPlugin;
import com.google.common.base.Preconditions;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import mineplex.core.common.util.UtilFuture;
import mineplex.core.report.ReportCategory;
import mineplex.core.report.ReportResultType;
import mineplex.core.report.ReportTeam;
import mineplex.serverdata.database.DBPool;
/**
* Handles creating {@link ReportUser} instances from report data stored in the database.
*/
public class ReportUserRepository
{
private static final String GET_TEAMS = "SELECT teamId FROM reportTeamMemberships WHERE accountId = ?;";
private static final String INSERT_TEAM = "INSERT IGNORE INTO reportTeamMemberships (accountId, teamId) VALUES (?, ?);";
private static final String GET_TEAM_MEMBERS = "SELECT accountId FROM reportTeamMemberships WHERE teamId = ?;";
private static final String GRAB_RESULT_COUNT = "SELECT reports.categoryId, results.resultId, COUNT(*) AS count" +
" FROM reports, reportReasons reasons, reportResults results, reportResultTypes resultTypes" +
" WHERE results.reportId = reports.id" +
" AND reasons.reportId = reports.id" +
" AND resultTypes.id = results.resultId" +
" AND reasons.reporterId = ?" +
" GROUP BY reports.categoryId, results.resultId;";
private final JavaPlugin _plugin;
private final Cache<Integer, ReportUser> _cachedUsers = CacheBuilder.newBuilder()
.maximumSize(1000)
.weakValues()
.build();
public ReportUserRepository(JavaPlugin plugin)
{
_plugin = plugin;
}
public CompletableFuture<List<Integer>> getTeamMembers(ReportTeam team)
{
return CompletableFuture.supplyAsync(() ->
{
try (Connection connection = DBPool.getAccount().getConnection())
{
PreparedStatement preparedStatement = connection.prepareStatement(GET_TEAM_MEMBERS);
preparedStatement.setInt(1, team.getDatabaseId());
ResultSet resultSet = preparedStatement.executeQuery();
List<Integer> memberIds = new ArrayList<>();
while (resultSet.next())
{
memberIds.add(resultSet.getInt("accountId"));
}
return memberIds;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
});
}
public CompletableFuture<List<ReportUser>> getUsers(Collection<Integer> accountIds)
{
return UtilFuture.sequence(
accountIds.stream()
.map(this::getUser)
.collect(Collectors.toList())
).thenApply(users ->
users.stream().filter(user -> user != null)
.collect(Collectors.toList())
);
}
public CompletableFuture<ReportUser> getUser(int accountId)
{
ReportUser cachedUser = _cachedUsers.getIfPresent(accountId);
if (cachedUser != null)
{
return CompletableFuture.completedFuture(cachedUser);
}
else
{
return CompletableFuture.supplyAsync(() ->
{
ReportUser user = new ReportUser(accountId);
try (Connection connection = DBPool.getAccount().getConnection())
{
loadTeams(connection, user);
loadStatistics(connection, user);
_cachedUsers.put(accountId, user);
return user;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}).exceptionally(throwable ->
{
_plugin.getLogger().log(Level.SEVERE, "Error fetching ReportUser (id: " + accountId + ").", throwable);
return null;
});
}
}
private void loadStatistics(Connection connection, ReportUser user) throws SQLException
{
int accountId = user.getAccountId();
PreparedStatement preparedStatement = connection.prepareStatement(GRAB_RESULT_COUNT);
preparedStatement.setInt(1, accountId);
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next())
{
try
{
int categoryId = resultSet.getInt("reports.categoryId");
int resultTypeId = resultSet.getInt("results.resultId");
int count = resultSet.getInt("count");
ReportCategory category = ReportCategory.getById(categoryId);
ReportResultType resultType = ReportResultType.getById(resultTypeId);
Preconditions.checkNotNull(category, "Invalid category id: " + categoryId);
Preconditions.checkNotNull(resultType, "Invalid result type id: " + resultType);
if (resultType.isGlobalStat())
{
category = ReportCategory.GLOBAL;
}
user.setValue(category, resultType, count);
}
catch (Exception ex)
{
_plugin.getLogger().log(Level.SEVERE, "Error getting ReportUser (id: " + accountId + ").", ex);
}
}
}
private void loadTeams(Connection connection, ReportUser user) throws SQLException
{
PreparedStatement preparedStatement = connection.prepareStatement(GET_TEAMS);
preparedStatement.setInt(1, user.getAccountId());
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next())
{
short teamId = resultSet.getShort("teamId");
ReportTeam team = ReportTeam.getById(teamId);
if (team != null)
{
user.addTeam(team);
}
else
{
_plugin.getLogger().log(Level.WARNING, "No definition for team with id: " + teamId);
}
}
}
public CompletableFuture<Void> updateUser(ReportUser user)
{
return CompletableFuture.supplyAsync(() ->
{
try (Connection connection = DBPool.getAccount().getConnection())
{
insertTeams(connection, user);
return null;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
});
}
private void insertTeams(Connection connection, ReportUser user) throws SQLException
{
PreparedStatement preparedStatement = connection.prepareStatement(INSERT_TEAM);
for (ReportTeam team : user.getTeams())
{
preparedStatement.setInt(1, user.getAccountId());
preparedStatement.setShort(2, team.getDatabaseId());
preparedStatement.addBatch();
}
preparedStatement.executeBatch();
}
}

View File

@ -0,0 +1,47 @@
package mineplex.core.report.redis;
import java.util.UUID;
import mineplex.serverdata.commands.ServerCommand;
/**
* A command sent to all servers for use when attempting to locate a player.
*
* If the server receiving this command does indeed have the requested player connected to their server then it
* should respond with {@link FindPlayerResponse}.
*/
public class FindPlayer extends ServerCommand
{
private long _reportId;
private UUID _id;
private int _accountId;
private String _responseTarget;
public FindPlayer(long reportId, UUID id, int accountId, String responseTarget)
{
_reportId = reportId;
_id = id;
_accountId = accountId;
_responseTarget = responseTarget;
}
public long getReportId()
{
return _reportId;
}
public UUID getId()
{
return _id;
}
public int getAccountId()
{
return _accountId;
}
public String getResponseTarget()
{
return _responseTarget;
}
}

View File

@ -0,0 +1,30 @@
package mineplex.core.report.redis;
import mineplex.serverdata.commands.ServerCommand;
/**
* Sent when a {@link FindPlayer} command has been sent and the requested player is connected
* to this server instance.
*/
public class FindPlayerResponse extends ServerCommand
{
private long _reportId;
private String _serverName;
public FindPlayerResponse(FindPlayer findPlayer, String serverName)
{
super(findPlayer.getResponseTarget());
_reportId = findPlayer.getReportId();
_serverName = serverName;
}
public long getReportId()
{
return _reportId;
}
public String getServerName()
{
return _serverName;
}
}

View File

@ -0,0 +1,39 @@
package mineplex.core.report.redis;
import mineplex.core.common.jsonchat.JsonMessage;
import mineplex.core.report.data.Report;
import mineplex.core.report.data.ReportMessage;
import mineplex.serverdata.commands.ServerCommand;
/**
* A message regarding a report which is sent only to the player handling the report.
*/
public class HandlerNotification extends ServerCommand
{
private final long _reportId;
private final int _handlerId;
private final String _message; // in json format
public HandlerNotification(Report report, JsonMessage jsonMessage)
{
super();
_reportId = report.getId().orElseThrow(() -> new IllegalStateException("Report has no id set."));
_handlerId = report.getHandlerId().orElseThrow(() -> new IllegalStateException("Report has no handler."));
_message = jsonMessage.toString();
}
public long getReportId()
{
return _reportId;
}
public int getHandlerId()
{
return _handlerId;
}
public String getJson()
{
return _message;
}
}

View File

@ -0,0 +1,33 @@
package mineplex.core.report.redis;
import java.util.Set;
import java.util.UUID;
import mineplex.core.common.jsonchat.JsonMessage;
import mineplex.serverdata.commands.ServerCommand;
/**
* When this packet is received by a server, it will check to see if any of the reporters are online.
* If so, it will send the supplied notification to them.
*/
public class ReportersNotification extends ServerCommand
{
private Set<UUID> _reporters;
private String _message; // in json format
public ReportersNotification(Set<UUID> ids, JsonMessage jsonMessage)
{
_reporters = ids;
_message = jsonMessage.toString();
}
public Set<UUID> getReporterUUIDs()
{
return _reporters;
}
public String getJson()
{
return _message;
}
}

View File

@ -1,83 +0,0 @@
package mineplex.core.report.task;
import java.util.Map;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.scheduler.BukkitRunnable;
import mineplex.core.chatsnap.publishing.SnapshotPublisher;
import mineplex.core.common.jsonchat.ClickEvent;
import mineplex.core.common.jsonchat.JsonMessage;
import mineplex.core.common.util.C;
import mineplex.core.report.Report;
import mineplex.core.report.ReportManager;
import mineplex.core.report.command.ReportHandlerNotification;
import org.apache.commons.lang3.StringUtils;
/**
* Displays a message containing up-to-date details of a report to it's handler.
*/
public class ReportHandlerMessageTask extends BukkitRunnable
{
private static final String DECORATION = C.cAqua + "------------------------------------";
private ReportManager _reportManager;
private Report _report;
public ReportHandlerMessageTask(ReportManager reportManager, Report report)
{
_reportManager = reportManager;
_report = report;
}
@Override
public void run()
{
int reportId = _report.getReportId();
if (_reportManager.isActiveReport(reportId))
{
String suspectName = Bukkit.getOfflinePlayer(_report.getSuspect()).getName();
JsonMessage jsonMessage = new JsonMessage(DECORATION)
.extra("\n")
.add(C.cAqua + "Reviewing Ticket " + C.cGold + "#" + reportId)
.add("\n\n")
.add(C.cPurple + StringUtils.join(getReportReasons(), "\n" + C.cPurple))
.add("\n\n")
.add(C.cAqua + "Suspect: " + C.cGold + suspectName)
.add("\n")
.add((_report.hasToken() ? C.cAqua + "Chat Log: " + C.cGold + "Click Here" + "\n" : ""))
.click(ClickEvent.OPEN_URL, SnapshotPublisher.getURL(_report.getToken()))
.add("\n")
.add(C.cAqua + "Type " + C.cGold + "/reportclose " + reportId + " <reason>" + C.cAqua + " to close this report.")
.click(ClickEvent.SUGGEST_COMMAND, "/reportclose " + reportId)
.add("\n")
.add(DECORATION);
new ReportHandlerNotification(_report, jsonMessage).publish();
}
else
{
// report has been closed, so this task should be cancelled
cancel();
}
}
public String[] getReportReasons()
{
Map<UUID, String> reportReasons = _report.getReportReasons();
String[] output = new String[reportReasons.size()];
int count = 0;
for (Map.Entry<UUID, String> entry : reportReasons.entrySet())
{
String reporterName = Bukkit.getOfflinePlayer(entry.getKey()).getName();
// triple backslashes so this translates to valid JSON
output[count++] = "\\\"" + entry.getValue() + "\\\" - " + reporterName;
}
return output;
}
}

View File

@ -1,87 +0,0 @@
package mineplex.core.report.task;
import java.util.ArrayList;
import org.bukkit.scheduler.BukkitRunnable;
import mineplex.core.common.util.C;
import mineplex.core.report.Report;
import mineplex.core.report.ReportManager;
/**
* Checks reports "owned" by this instance for inactivity, purges inactive reports.
*/
public class ReportPurgeTask extends BukkitRunnable
{
private ReportManager _reportManager;
public ReportPurgeTask(ReportManager reportManager)
{
_reportManager = reportManager;
}
@Override
public void run()
{
for (int reportId : new ArrayList<>(_reportManager.getActiveReports())) // create copy as this will be run async
{
Report report = _reportManager.getReport(reportId);
if (report != null)
{
if (checkForPurge(report))
{
notifyHandler(report);
}
}
else
{
// report has been leftover for some reason
purgeReport(reportId);
}
}
}
/**
* Checks if a report should be purged and carries it out if so.
*
* @param report the report to check for purging (and act accordingly)
* @return true if the report was purged, false otherwise
*/
public boolean checkForPurge(Report report)
{
if (!report.isActive())
{
int reportId = report.getReportId();
purgeReport(reportId);
return true;
}
return false;
}
/**
* Purges a report.
*
* @param reportId the report id to purge
*/
public void purgeReport(int reportId)
{
_reportManager.removeReport(reportId);
_reportManager.removeActiveReport(reportId);
}
/**
* Notifies the handler of a report (if any) that the report was purged.
*
* @param report the report which was purged
*/
public void notifyHandler(Report report)
{
// would result in spam if many reports with no handlers are purged (causing each member of staff to receive a message for every purged report)
if (report.getHandler() != null)
{
_reportManager.sendHandlerNotification(report, ReportManager.getReportPrefix(report) + C.cRed + "Purging report due to inactivity.");
}
}
}

View File

@ -0,0 +1,48 @@
package mineplex.core.report.ui;
import java.util.HashMap;
import java.util.Map;
import org.bukkit.Material;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.inventory.ItemStack;
import mineplex.core.common.util.C;
import mineplex.core.gui.SimpleGuiItem;
import mineplex.core.itemstack.ItemBuilder;
import mineplex.core.report.ReportTeam;
/**
* A gui button which when clicked will forward a report onto the supplied team.
*/
public class ReportAssignTeamButton extends SimpleGuiItem
{
private static final Map<ReportTeam, ItemStack> DISPLAY_ITEMS = new HashMap<ReportTeam, ItemStack>(){{
ItemStack rcItem = new ItemBuilder(Material.PAPER)
.setTitle(C.cAqua + "Forward to RC")
.addLore("A member of the rules committee will review this report instead.")
.build();
put(ReportTeam.RC, rcItem);
}};
private final ReportResultPage _page;
private final ReportTeam _team;
public ReportAssignTeamButton(ReportResultPage page, ReportTeam team)
{
this(page, team, DISPLAY_ITEMS.get(team));
}
public ReportAssignTeamButton(ReportResultPage page, ReportTeam team, ItemStack displayItem)
{
super(displayItem);
_page = page;
_team = team;
}
@Override
public void click(ClickType clickType)
{
_page.assignTeam(_team);
}
}

View File

@ -1,9 +1,15 @@
package mineplex.core.report.ui;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.EnumMap;
import java.util.Map;
import org.bukkit.Material;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.inventory.ItemStack;
import mineplex.core.common.util.C;
import mineplex.core.gui.SimpleGuiItem;
import mineplex.core.itemstack.ItemBuilder;
import mineplex.core.report.ReportCategory;
/**
@ -11,19 +17,41 @@ import mineplex.core.report.ReportCategory;
*/
public class ReportCategoryButton extends SimpleGuiItem
{
private ReportCategoryPage _reportCategoryPage;
private ReportCategory _category;
// initialize the display items we use
private static final Map<ReportCategory, ItemStack> ITEM_STACKS = new EnumMap<ReportCategory, ItemStack>(ReportCategory.class)
{{
ItemStack itemHack = new ItemBuilder(Material.IRON_SWORD)
.setTitle(C.cRedB + "Hacking")
.addLore(C.cGray + "X-ray, Forcefield, Speed, Fly etc")
.build();
put(ReportCategory.HACKING, itemHack);
public ReportCategoryButton(ReportCategoryPage reportCategoryPage, ReportCategory category) {
super(category.getItemMaterial(), 1, (short) 0);
ItemStack itemChatAbuse = new ItemBuilder(Material.BOOK_AND_QUILL)
.setTitle(C.cDAquaB + "Chat Abuse")
.addLore(C.cGray + "Verbal Abuse, Spam, Harassment, Trolling, etc")
.build();
put(ReportCategory.CHAT_ABUSE, itemChatAbuse);
ItemMeta itemMeta = getItemMeta();
itemMeta.setDisplayName(category.getTitle());
itemMeta.setLore(category.getDescription());
setItemMeta(itemMeta);
ItemStack itemGameplay = new ItemBuilder(Material.ENDER_PEARL)
.setTitle("Gameplay")
.addLore(C.cGray + "Map and Bug Exploits")
.build();
put(ReportCategory.GAMEPLAY, itemGameplay);
}};
this._reportCategoryPage = reportCategoryPage;
this._category = category;
private final ReportCategoryPage _reportCategoryPage;
private final ReportCategory _category;
public ReportCategoryButton(ReportCategoryPage reportCategoryPage, ReportCategory reportCategory)
{
this(reportCategoryPage, reportCategory, ITEM_STACKS.get(reportCategory));
}
public ReportCategoryButton(ReportCategoryPage reportCategoryPage, ReportCategory reportCategory, ItemStack itemStack)
{
super(itemStack);
_reportCategoryPage = reportCategoryPage;
_category = reportCategory;
}
@Override

View File

@ -1,16 +1,21 @@
package mineplex.core.report.ui;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import mineplex.core.account.CoreClient;
import mineplex.core.chatsnap.SnapshotManager;
import mineplex.core.chatsnap.SnapshotMessage;
import mineplex.core.common.util.C;
import mineplex.core.common.util.F;
import mineplex.core.common.util.UtilPlayer;
import mineplex.core.gui.SimpleGui;
import mineplex.core.report.ReportCategory;
import mineplex.core.report.Report;
import mineplex.core.report.ReportManager;
import mineplex.core.report.ReportPlugin;
/**
@ -18,45 +23,87 @@ import mineplex.core.report.ReportPlugin;
*/
public class ReportCategoryPage extends SimpleGui
{
private static final Map<Integer, ReportCategory> CATEGORY_SLOTS = Collections.unmodifiableMap(new HashMap<Integer, ReportCategory>()
{{
int rowStartSlot = 9 * 2; // end of row 2
put(rowStartSlot + 3, ReportCategory.HACKING);
put(rowStartSlot + 5, ReportCategory.CHAT_ABUSE);
}});
private final ReportPlugin _plugin;
private final Player _reporter;
private final int _reporterId;
private final CoreClient _suspect;
private final String _reason;
private ReportPlugin _reportPlugin;
private Player _reportee;
private Player _offender;
private String _reason;
public ReportCategoryPage(ReportPlugin reportPlugin, Player reportee, Player offender, String reason)
public ReportCategoryPage(ReportPlugin plugin, Player reporter, int reporterId, CoreClient suspect, String reason)
{
super(reportPlugin.getPlugin(), reportee, "Report " + offender.getName(), 9 * 5);
super(plugin.getPlugin(), reporter, "Report " + suspect.getName(), 9 * 3);
this._reportPlugin = reportPlugin;
this._reportee = reportee;
this._offender = offender;
this._reason = reason;
_plugin = plugin;
_reporter = reporter;
_reporterId = reporterId;
_suspect = suspect;
_reason = reason;
buildPage();
}
private void buildPage()
{
for (Map.Entry<Integer, ReportCategory> entry : CATEGORY_SLOTS.entrySet())
{
ReportCategory category = entry.getValue();
setItem(entry.getKey(), new ReportCategoryButton(this, category));
}
setItem(11, new ReportCategoryButton(this, ReportCategory.HACKING));
setItem(13, new ReportCategoryButton(this, ReportCategory.CHAT_ABUSE));
setItem(15, new ReportCategoryButton(this, ReportCategory.GAMEPLAY));
}
public void addReport(ReportCategory category)
{
Report report = _reportPlugin.getReportManager().reportPlayer(_reportee, _offender, category, _reason);
_reportee.closeInventory();
_reporter.closeInventory();
unregisterListener();
_reportee.sendMessage(C.cGreen + "Report sent successfully (" + C.cGold + "#" + report.getReportId() + C.cGreen + ").");
if (category == ReportCategory.CHAT_ABUSE)
{
if (hasSentMessage(_suspect.getAccountId()))
{
createReport(category);
}
else
{
UtilPlayer.message(_reporter, F.main(_plugin.getName(), C.cRed + "You have not received a message from that player"));
}
}
else
{
createReport(category);
}
}
private boolean hasSentMessage(int accountId)
{
SnapshotManager snapshotManager = _plugin.getReportManager().getSnapshotManager();
int suspectId = _suspect.getAccountId();
List<SnapshotMessage> suspectMessages = snapshotManager.getMessagesFrom(accountId).stream()
.filter(message -> message.getSenderId() == suspectId || message.getRecipientIds().contains(suspectId))
.collect(Collectors.toList());
return suspectMessages.size() > 0;
}
private void createReport(ReportCategory category)
{
_plugin.getReportManager().createReport(_reporterId, _suspect.getAccountId(), category, _reason)
.thenAccept(report -> {
boolean error = true;
if (report != null)
{
Optional<Long> reportIdOptional = report.getId();
if (reportIdOptional.isPresent())
{
_reporter.sendMessage(F.main(ReportManager.getReportPrefix(reportIdOptional.get()), C.cGreen + "Successfully created report."));
error = false;
}
}
if (error)
{
_reporter.sendMessage(C.cRed + "An error occurred whilst reporting that player, please try again.");
}
});
}
public void unregisterListener()

View File

@ -1,27 +1,63 @@
package mineplex.core.report.ui;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import org.bukkit.DyeColor;
import org.bukkit.Material;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import mineplex.core.common.util.C;
import mineplex.core.gui.SimpleGuiItem;
import mineplex.core.report.ReportResult;
import mineplex.core.itemstack.ItemBuilder;
import mineplex.core.report.ReportResultType;
/**
* Represents a button which can be clicked to determine the result of a report.
*/
public class ReportResultButton extends SimpleGuiItem
{
private ReportResultPage _reportResultPage;
private ReportResult _result;
// initialize button display itemsR
private static final Map<ReportResultType, ItemStack> ITEM_STACKS = new EnumMap<ReportResultType, ItemStack>(ReportResultType.class)
{{
ItemStack itemAccept = new ItemBuilder(Material.WOOL)
.setData(DyeColor.GREEN.getData())
.setTitle(C.cGreen + "Accept Report")
.addLore("%suspect% is guilty without a doubt.")
.build();
put(ReportResultType.ACCEPTED, itemAccept);
public ReportResultButton(ReportResultPage reportResultPage, ReportResult result, ItemStack displayItem)
ItemStack itemDeny = new ItemBuilder(Material.WOOL)
.setData(DyeColor.YELLOW.getData())
.setTitle(C.cYellow + "Deny Report")
.addLore("There is not enough evidence against %suspect%.")
.build();
put(ReportResultType.DENIED, itemDeny);
ItemStack itemAbuse = new ItemBuilder(Material.WOOL)
.setData(DyeColor.RED.getData())
.setTitle(C.cRed + "Flag Abuse")
.addLore("The reporter(s) were abusing the report system.")
.build();
put(ReportResultType.ABUSIVE, itemAbuse);
}};
private ReportResultPage _page;
private ReportResultType _resultType;
public ReportResultButton(ReportResultPage page, ReportResultType resultType)
{
this(page, resultType, ITEM_STACKS.get(resultType));
}
public ReportResultButton(ReportResultPage page, ReportResultType resultType, ItemStack displayItem)
{
super(displayItem);
_reportResultPage = reportResultPage;
_result = result;
_page = page;
_resultType = resultType;
}
@Override
@ -33,7 +69,7 @@ public class ReportResultButton extends SimpleGuiItem
for (int i = 0; i < lore.size(); i++)
{
lore.set(i, lore.get(i).replace("%suspect%", _reportResultPage.getPlayer().getName()));
lore.set(i, lore.get(i).replace("%suspect%", _page.getSuspectName()));
}
itemMeta.setLore(lore);
@ -43,6 +79,6 @@ public class ReportResultButton extends SimpleGuiItem
@Override
public void click(ClickType clickType)
{
_reportResultPage.setResult(_result);
_page.closeReport(_resultType);
}
}

View File

@ -1,69 +1,71 @@
package mineplex.core.report.ui;
import org.bukkit.DyeColor;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.bukkit.inventory.ItemStack;
import mineplex.core.common.util.C;
import mineplex.core.common.util.F;
import mineplex.core.common.util.UtilPlayer;
import mineplex.core.gui.SimpleGui;
import mineplex.core.itemstack.ItemBuilder;
import mineplex.core.report.ReportManager;
import mineplex.core.report.ReportPlugin;
import mineplex.core.report.ReportResult;
import mineplex.core.report.ReportResultType;
import mineplex.core.report.ReportTeam;
import mineplex.core.report.data.Report;
/**
* User interface shown to a moderator when closing a report to determine the result of the report.
*/
public class ReportResultPage extends SimpleGui
{
private static final ItemStack ITEM_ACCEPT = new ItemBuilder(Material.WOOL)
.setData(DyeColor.GREEN.getData())
.setTitle(C.cGreen + "Accept Report")
.addLore("%suspect% is cheating without a doubt.")
.build();
private static final ItemStack ITEM_DENY = new ItemBuilder(Material.WOOL)
.setData(DyeColor.YELLOW.getData())
.setTitle(C.cYellow + "Deny Report")
.addLore("There is not enough evidence against %suspect%.")
.build();
private static final ItemStack ITEM_ABUSE = new ItemBuilder(Material.WOOL)
.setData(DyeColor.RED.getData())
.setTitle(C.cRed + "Flag Abuse")
.addLore("The reporter(s) were abusing the report system.")
.build();
private ReportManager _reportManager;
private int _reportId;
private Player _reportCloser;
private Report _report;
private String _suspectName;
private String _reason;
public ReportResultPage(ReportPlugin reportPlugin, int reportId, Player reportCloser, String reason)
public ReportResultPage(ReportPlugin reportPlugin, Report report, Player reportCloser, String suspectName, String reason)
{
super(reportPlugin.getPlugin(), reportCloser, "Report Result", 9 * 3);
super(reportPlugin.getPlugin(), reportCloser, "Close Report", 9 * 4);
_reportManager = reportPlugin.getReportManager();
_reportId = reportId;
_reportCloser = reportCloser;
_report = report;
_suspectName = suspectName;
_reason = reason;
buildPage();
}
private void buildPage()
public String getSuspectName()
{
setItem(11, new ReportResultButton(this, ReportResult.ACCEPTED, ITEM_ACCEPT));
setItem(13, new ReportResultButton(this, ReportResult.DENIED, ITEM_DENY));
setItem(15, new ReportResultButton(this, ReportResult.ABUSIVE, ITEM_ABUSE));
return _suspectName;
}
public void setResult(ReportResult result)
private void buildPage()
{
_reportCloser.closeInventory();
setItem(11, new ReportResultButton(this, ReportResultType.ACCEPTED));
setItem(13, new ReportResultButton(this, ReportResultType.DENIED));
setItem(15, new ReportResultButton(this, ReportResultType.ABUSIVE));
//setItem(34, );
setItem(35, new ReportAssignTeamButton(this, ReportTeam.RC));
}
public void closeReport(ReportResultType result)
{
getPlayer().closeInventory();
unregisterListener();
_reportManager.closeReport(_reportId, _reportCloser, _reason, result);
ReportResult reportResult = new ReportResult(result, _reason);
_reportManager.closeReport(_report, getPlayer(), reportResult);
}
public void assignTeam(ReportTeam team)
{
getPlayer().closeInventory();
unregisterListener();
_reportManager.assignTeam(_report, team).thenAccept(aVoid ->
UtilPlayer.message(getPlayer(),
F.main(ReportManager.getReportPrefix(_report),
"Report forwarded to " + F.elem(team.name()) + " team")));
}
public void unregisterListener()

View File

@ -93,7 +93,8 @@ public class ThankManager extends MiniDbClientPlugin<ThankData>
*/
public void claimThanks(Player player, Callback<ClaimThankResult> callback)
{
int accountId = ClientManager.getAccountId(player);
final String playerName = player.getName();
final int accountId = ClientManager.getAccountId(player);
if (accountId == -1)
{
@ -106,7 +107,11 @@ public class ThankManager extends MiniDbClientPlugin<ThankData>
{
ClaimThankResult result = _thankRepository.claimThank(accountId);
runSync(() -> {
if (result != null && result.getClaimed() > 0) Set(player, new ThankData(0));
if (result != null && result.getClaimed() > 0)
{
Set(player, new ThankData(0));
_donationManager.rewardCoinsUntilSuccess(null, "Thank", player.getName(), accountId, result.getClaimed());
}
callback.run(result);
});
}
@ -152,11 +157,10 @@ public class ThankManager extends MiniDbClientPlugin<ThankData>
try
{
boolean success = _thankRepository.thank(receiverAccountId, senderAccountId, receiverReward, reason, ignoreCooldown);
runSync(() -> {
if (success)
{
_donationManager.rewardCoinsUntilSuccess(null, "Thank", receiverName, receiverAccountId, receiverReward);
// Reward Shards for the sender now. The player being thanked can claim their shards at Carl.
_donationManager.rewardCoinsUntilSuccess(null, "Thank", senderName, senderAccountId, senderReward);
}

View File

@ -9,6 +9,9 @@ import mineplex.core.antihack.AntiHack;
import mineplex.core.antihack.AntiHackGuardian;
import mineplex.core.blockrestore.BlockRestore;
import mineplex.core.chat.Chat;
import mineplex.core.chatsnap.SnapshotManager;
import mineplex.core.chatsnap.SnapshotPlugin;
import mineplex.core.chatsnap.SnapshotRepository;
import mineplex.core.command.CommandCenter;
import mineplex.core.common.MinecraftVersion;
import mineplex.core.common.Pair;
@ -35,6 +38,8 @@ import mineplex.core.portal.Portal;
import mineplex.core.preferences.PreferencesManager;
import mineplex.core.punish.Punish;
import mineplex.core.recharge.Recharge;
import mineplex.core.report.ReportManager;
import mineplex.core.report.ReportPlugin;
import mineplex.core.resourcepack.ResourcePackManager;
import mineplex.core.serverConfig.ServerConfiguration;
import mineplex.core.spawn.Spawn;
@ -231,9 +236,9 @@ public class Clans extends JavaPlugin
Pair.create(MinecraftVersion.Version1_9, "http://file.mineplex.com/ResClans19.zip")
}, true);
//SnapshotManager snapshotManager = new SnapshotManager(new SnapshotPublisher(this));
//new SnapshotPlugin(this, snapshotManager);
//new ReportPlugin(this, new ReportManager(this, preferenceManager, statsManager, snapshotManager, _commandCenter.Instance.GetClientManager(), serverStatusManager.getCurrentServerName()));
SnapshotManager snapshotManager = new SnapshotManager(this, new SnapshotRepository(serverStatusManager.getCurrentServerName(), getLogger()));
new SnapshotPlugin(this, snapshotManager, _clientManager);
new ReportPlugin(this, new ReportManager(this, snapshotManager, _clientManager, incognito, punish, serverStatusManager.getCurrentServerName(), 1));
// Enable custom-gear related managers
new CustomTagFix(this, packetHandler);

View File

@ -191,7 +191,7 @@ public class HubManager extends MiniPlugin implements IChatMessageFormatter
FacebookManager facebookManager = new FacebookManager(plugin, clientManager, donationManager, inventoryManager);
YoutubeManager youtubeManager = new YoutubeManager(plugin, clientManager, donationManager);
PlayWireManager playWireManager = new PlayWireManager(plugin, clientManager, donationManager);
_bonusManager = new BonusManager(plugin, clientManager, playWireManager, donationManager, pollManager , npcManager, hologramManager, statsManager, _inventoryManager, petManager, facebookManager, youtubeManager, _gadgetManager, thankManager);
_bonusManager = new BonusManager(plugin, null, playWireManager, clientManager, donationManager, pollManager , npcManager, hologramManager, statsManager, _inventoryManager, petManager, facebookManager, youtubeManager, _gadgetManager, thankManager);
World world = _spawn.getWorld();
_treasureManager = new TreasureManager(_plugin, clientManager, serverStatusManager, donationManager, _inventoryManager, petManager, _gadgetManager, _blockRestore, hologramManager, statsManager, _bonusManager.getRewardManager());

View File

@ -11,6 +11,9 @@ import mineplex.core.blockrestore.BlockRestore;
import mineplex.core.boosters.BoosterManager;
import mineplex.core.brawl.fountain.FountainManager;
import mineplex.core.chat.Chat;
import mineplex.core.chatsnap.SnapshotManager;
import mineplex.core.chatsnap.SnapshotPlugin;
import mineplex.core.chatsnap.SnapshotRepository;
import mineplex.core.command.CommandCenter;
import mineplex.core.common.events.ServerShutdownEvent;
import mineplex.core.creature.Creature;
@ -43,6 +46,8 @@ import mineplex.core.profileCache.ProfileCacheManager;
import mineplex.core.projectile.ProjectileManager;
import mineplex.core.punish.Punish;
import mineplex.core.recharge.Recharge;
import mineplex.core.report.ReportManager;
import mineplex.core.report.ReportPlugin;
import mineplex.core.resourcepack.ResourcePackManager;
import mineplex.core.serverConfig.ServerConfiguration;
import mineplex.core.sponsorbranding.BrandingManager;
@ -181,6 +186,11 @@ public class Hub extends JavaPlugin implements IRelation
new GlobalPacketManager(this, clientManager, serverStatusManager, inventoryManager, donationManager, petManager, statsManager, hubManager.getBonusManager().getRewardManager());
//new Replay(this, packetHandler);
SnapshotManager snapshotManager = new SnapshotManager(this, new SnapshotRepository(serverStatusManager.getCurrentServerName(), getLogger()));
ReportManager reportManager = new ReportManager(this, snapshotManager, clientManager, incognito, punish, serverStatusManager.getCurrentServerName(), 3);
new SnapshotPlugin(this, snapshotManager, clientManager);
new ReportPlugin(this, reportManager);
AprilFoolsManager.Initialize(this, clientManager, disguiseManager);
CombatManager combatManager = new CombatManager(this);

View File

@ -202,7 +202,7 @@ public class HubManager extends MiniClientPlugin<HubClient> implements IChatMess
YoutubeManager youtubeManager = new YoutubeManager(plugin, clientManager, donationManager);
PlayWireManager playWireManager = new PlayWireManager(plugin, clientManager, donationManager);
_bonusManager = new BonusManager(plugin, clientManager, playWireManager, donationManager, pollManager , npcManager, hologramManager, statsManager, _inventoryManager, petManager, facebookManager, youtubeManager, _gadgetManager, thankManager);
_bonusManager = new BonusManager(plugin, null, playWireManager, clientManager, donationManager, pollManager , npcManager, hologramManager, statsManager, _inventoryManager, petManager, facebookManager, youtubeManager, _gadgetManager, thankManager);
_treasureManager = new TreasureManager(_plugin, clientManager, serverStatusManager, donationManager, _inventoryManager, petManager, _gadgetManager, _blockRestore, hologramManager, statsManager, _bonusManager.getRewardManager());
CosmeticManager cosmeticManager = new CosmeticManager(_plugin, clientManager, donationManager, _inventoryManager, _gadgetManager, _mountManager, petManager, _treasureManager, boosterManager);
@ -683,7 +683,7 @@ public class HubManager extends MiniClientPlugin<HubClient> implements IChatMess
event.setMessage(event.getMessage().substring(1, event.getMessage().length()));
event.setFormat(levelStr + C.cDPurple + C.Bold + "Party " + C.cWhite + C.Bold + playerName + " " + C.cPurple + "%2$s");
for (UUID uuid: party.getMembersByUUID())
for (String uuid: party.getMembers())
{
Player other = Bukkit.getPlayer(uuid);

View File

@ -1,62 +0,0 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.mineplex</groupId>
<artifactId>mineplex-app</artifactId>
<version>dev-SNAPSHOT</version>
<relativePath>../app.xml</relativePath>
</parent>
<name>ReportServer</name>
<artifactId>mineplex-reportserver</artifactId>
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.4.2</version>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
<dependency>
<groupId>com.mineplex</groupId>
<artifactId>mineplex-serverdata</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Main-Class>mineplex.reportserver.ReportServer</Main-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -1,57 +0,0 @@
package mineplex.reportserver;
import java.io.File;
import java.io.FileFilter;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import org.apache.commons.lang3.Validate;
/**
* Class responsible for deleting old files (file age is determined by the "last modified" value).
*/
public class FilePurger implements Runnable
{
private static final FileFilter FILE_FILTER = file -> file.isFile() && file.getName().endsWith(".json");
private final File _dataDir;
private final Logger _logger;
public FilePurger(File dataDir, Logger logger)
{
_dataDir = dataDir;
_logger = logger;
Validate.notNull(_dataDir, "Data directory cannot be null.");
Validate.isTrue(_dataDir.exists() && dataDir.isDirectory(), "Path non-existent or not a directory: %s", _dataDir.getAbsolutePath());
Validate.notNull(_logger, "Logger cannot be null.");
}
@Override
public void run()
{
int purgeCount = 0;
for (File file : _dataDir.listFiles(FILE_FILTER))
{
long lastModified = file.lastModified();
long timeSince = System.currentTimeMillis() - lastModified;
int days = (int) TimeUnit.MILLISECONDS.toDays(timeSince);
if (days >= 15) // keep files for 15 days
{
if (!file.delete())
{
_logger.warning("Cannot delete file: " + file.getAbsolutePath());
}
else
{
purgeCount++;
}
}
}
_logger.info("Purged " + purgeCount + " old chat snapshots.");
}
}

View File

@ -1,142 +0,0 @@
package mineplex.reportserver;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.apache.commons.lang3.Validate;
import redis.clients.jedis.JedisPubSub;
/**
* Listens for commands from Redis (such as "deploy" or "destroy") and executes them.
*/
public class RedisCommandHandler extends JedisPubSub
{
private static final Gson _gson = new GsonBuilder()
.setPrettyPrinting()
.create();
private static final JsonParser _jsonParser = new JsonParser();
private final File _directory;
private final Logger _logger;
private final ExecutorService _executorService = Executors.newCachedThreadPool();
public RedisCommandHandler(File directory, Logger logger)
{
_directory = directory;
_logger = logger;
Validate.notNull(_directory, "Directory cannot be null.");
Validate.isTrue(directory.exists() && directory.isDirectory(), "Path non-existent or not a directory: %s", directory.getPath());
Validate.notNull(_logger, "Logger cannot be null.");
}
@Override
public void onMessage(String channel, String dataString)
{
try
{
if (channel.equals(ReportServer.CHANNEL_DEPLOY))
{
String json = dataString;
JsonObject jsonObject = _jsonParser.parse(json).getAsJsonObject();
String token = jsonObject.get("token").getAsString();
File target = new File(_directory, token + ".json");
_logger.info("Chat snapshot received [" + token + "], writing to file.");
if (target.exists() && !jsonObject.has("snapshots"))
{
try (BufferedReader bufferedReader = new BufferedReader(new FileReader(target)))
{
JsonObject originalJsonObject = _jsonParser.parse(bufferedReader).getAsJsonObject();
JsonObject usernamesObject = jsonObject.get("usernames").getAsJsonObject();
// retrieve snapshots from original file and add to jsonObject
jsonObject.add("snapshots", originalJsonObject.get("snapshots").getAsJsonArray());
// add new UUID->Usernames, update existing usernames
for (Map.Entry<String, JsonElement> entry : originalJsonObject.get("usernames").getAsJsonObject().entrySet())
{
usernamesObject.addProperty(entry.getKey(), entry.getValue().getAsJsonPrimitive().getAsString());
}
// re-write json after updating
json = _gson.toJson(jsonObject);
}
catch (Exception e)
{
_logger.log(Level.SEVERE, "Exception whilst updating an original snapshot.", e);
}
}
writeFile(target, json);
}
else if (channel.equals(ReportServer.CHANNEL_DESTROY))
{
// dataString = token
File target = new File(_directory, dataString + ".json");
_logger.info("Destroy command received [" + dataString + "].");
if (target.exists() && !target.delete())
{
_logger.warning("Failed to delete: " + target.getPath());
}
}
}
catch (Exception e)
{
_logger.log(Level.SEVERE, "Error whilst receiving redis message.", e);
}
}
private void writeFile(File file, String json)
{
_executorService.submit(() -> Files.write(file.toPath(), Arrays.asList(json.split("\n"))));
}
@Override
public void onPMessage(String s, String s1, String s2)
{
}
@Override
public void onSubscribe(String s, int i)
{
}
@Override
public void onUnsubscribe(String s, int i)
{
}
@Override
public void onPUnsubscribe(String s, int i)
{
}
@Override
public void onPSubscribe(String s, int i)
{
}
}

View File

@ -1,106 +0,0 @@
package mineplex.reportserver;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.DurationFormatUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
/**
* Establishes and maintains a connection to Redis.
*/
public class RedisConnectionHandler implements Runnable
{
private final String _name;
private final JedisPool _jedisPool;
private final RedisCommandHandler _handler;
private final String[] _channels;
private final Logger _logger;
private long _lastConnectionMillis = -1;
private Throwable _lastThrowable = null;
public RedisConnectionHandler(String name, JedisPool jedisPool, RedisCommandHandler handler, String[] channels, Logger logger)
{
_name = name;
_jedisPool = jedisPool;
_handler = handler;
_channels = channels;
_logger = logger;
Validate.isTrue(channels.length > 0, "Must provide at least one channel.");
}
@Override
public void run()
{
while (!Thread.interrupted())
{
try
{
registerChannelHandlers();
}
catch (Throwable e)
{
// Only log new errors (prevents same error being spammed)
if (_lastThrowable == null || !e.getClass().equals(_lastThrowable.getClass()))
{
if (_lastThrowable == null) // connection just failed
{
_lastConnectionMillis = System.currentTimeMillis();
}
_logger.log(Level.SEVERE, prefixMessage(
"Exception in Redis connection"
+ (_lastConnectionMillis != -1 ? " (no connection for " + getLastConnectionDuration() + ")" : "")
+ ", attempting to regain connection."
), e);
_lastThrowable = e;
}
try
{
Thread.sleep(1000 * 5);
}
catch (InterruptedException ignored) {}
}
}
_jedisPool.destroy();
_logger.warning("Thread interrupted, end of connection.");
}
private void registerChannelHandlers()
{
try (Jedis jedis = _jedisPool.getResource())
{
connectionEstablished();
jedis.subscribe(_handler, _channels);
}
}
private void connectionEstablished()
{
// subscribe blocks so we need to do all this before
_logger.info(
_lastThrowable == null
? prefixMessage("Connected.")
: prefixMessage(String.format("Connected after %s.", getLastConnectionDuration()))
);
_lastThrowable = null;
}
private String prefixMessage(String message)
{
return String.format("[%s] %s", _name, message);
}
private String getLastConnectionDuration()
{
return DurationFormatUtils.formatDurationWords(System.currentTimeMillis() - _lastConnectionMillis, true, true);
}
}

View File

@ -1,112 +0,0 @@
package mineplex.reportserver;
import java.io.File;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import mineplex.serverdata.Utility;
import mineplex.serverdata.redis.RedisConfig;
import mineplex.serverdata.servers.ConnectionData;
import mineplex.serverdata.servers.ServerManager;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.lang3.Validate;
import redis.clients.jedis.JedisPool;
/**
* Main class for the Report server, parses command line arguments and initializes the Report server.
*/
public class ReportServer
{
public static final String CHANNEL_DEPLOY = "reportserver:deploy";
public static final String CHANNEL_DESTROY = "reportserver:destroy";
private static final String[] CHANNELS = new String[]{CHANNEL_DEPLOY, CHANNEL_DESTROY};
public static void main(String[] args)
{
System.setProperty("java.util.logging.SimpleFormatter.format", "%4$s: %5$s%6$s%n"); // Nicer log output
Logger logger = Logger.getLogger("ReportServer");
logger.info("Starting report server.");
Options options = new Options();
Option dirOption = Option.builder("dataDir")
.hasArg()
.longOpt("dataDirectory")
.desc("Sets the data directory where the JSON files will be stored.")
.type(File.class)
.build();
options.addOption(dirOption);
try
{
CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(options, args);
File dataDirectory = (File) cmd.getParsedOptionValue(dirOption.getOpt());
if (dataDirectory == null)
{
dataDirectory = new File("data");
}
new ReportServer(ServerManager.getDefaultConfig(), dataDirectory, logger);
}
catch (ParseException e)
{
logger.log(Level.SEVERE, "Failed to parse arguments.", e);
}
}
private final File _dataDirectory;
private final Logger _logger;
private final RedisCommandHandler _handler;
private final ScheduledExecutorService _executorService = Executors.newScheduledThreadPool(1);
public ReportServer(RedisConfig redisConfig, File dataDirectory, Logger logger)
{
_dataDirectory = dataDirectory;
_logger = logger;
Validate.notNull(_dataDirectory, "Data directory cannot be null.");
// thrown if path exists but is not a directory
Validate.isTrue(!_dataDirectory.exists() || _dataDirectory.isDirectory(), "Not a directory: %s", _dataDirectory.getPath());
// throws if directory doesn't exist and cannot be created
Validate.isTrue(_dataDirectory.exists() || _dataDirectory.mkdir(), "Unable to create directory: " + _dataDirectory.getPath());
_handler = new RedisCommandHandler(_dataDirectory, _logger);
initializeConnectionsConfig(redisConfig);
schedulePurgeTask();
}
private void initializeConnectionsConfig(RedisConfig redisConfig)
{
redisConfig.getConnections(false, null).forEach(this::initializeConnection);
}
private void initializeConnection(ConnectionData connectionData)
{
JedisPool jedisPool = Utility.generatePool(connectionData);
String connectionName = connectionData.getName();
Thread thread = new Thread(new RedisConnectionHandler(connectionName, jedisPool, _handler, CHANNELS, _logger), connectionName + " - Redis PubSub Thread");
thread.setDaemon(true);
thread.start();
}
private void schedulePurgeTask()
{
_executorService.scheduleAtFixedRate(new FilePurger(_dataDirectory, _logger), 0, 30, TimeUnit.MINUTES);
}
}

View File

@ -1,56 +0,0 @@
const TAG_UUID = "data-player-uuid";
var elements = document.querySelectorAll('[' + TAG_UUID + ']'); // NodeList type
for (var i = 0; i < elements.length; i++)
{
var element = elements.item(i);
element.addEventListener("click", (function(element)
{
return function(e)
{
var uuid = element.getAttribute("data-player-uuid");
copy(uuid);
alertElement('<b>Copied UUID to clipboard.</b>', element, 1500);
}
})(element));
}
/**
* Attempts to copy the supplied text into the users clipboard.
* If this fails they are presented with a box allowing them to copy and paste the text.
* This will only work on a supported browser and if this action is caused by some sort of player input.
*
* @param text the text to put in the users clipboard
*/
function copy(text)
{
var copyElement = document.createElement('input');
copyElement.setAttribute('type', 'text');
copyElement.setAttribute('value', text);
copyElement = document.body.appendChild(copyElement);
copyElement.select();
try
{
document.execCommand('copy');
}
catch (e)
{
console.log("document.execCommand('copy'); is not supported by this browser.");
prompt('Copy the value below. (CTRL + C, ENTER)', text);
}
finally
{
copyElement.remove();
}
}
function alertElement(html, element, millis)
{
var original = element.innerHTML;
element.innerHTML = html;
setTimeout(function() { element.innerHTML = original; }, millis);
}

View File

@ -1,128 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<script src="js/jquery.js"></script>
<link rel="stylesheet" href="css/bootstrap.min.css">
<script src="js/bootstrap.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
<link rel="stylesheet" href="css/tiger.css">
<link href='https://fonts.googleapis.com/css?family=Crete+Round' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Oswald' rel='stylesheet' type='text/css'>
<title>Report #1234 &middot; Mineplex</title>
<script>
$("#test").click(function (){
alert("test!");
});
</script>
</head>
<body>
<div id="wrapper">
<div id="header">
<img src="img/logo.png" height="70px" width="70px" />
<h1>Report System</h1>
<!-- <h2><i class="fa fa-camera"></i>&nbsp;&nbsp; Chat Snap</h2> -->
</div>
<div id="search">
<div class="input-group">
<input type="text" class="form-control" placeholder="Enter a chat snap report ID...">
<span class="input-group-btn">
<button class="btn btn-secondary" type="button"><i class="fa fa-search"></i> Search</button>
</span>
</div>
</div>
<div id="content">
<div>
<hr>
<h2 style="font-family: 'Oswald', sans-serif; text-align: center;">
Report #1234
</h2>
<hr>
</div>
<div class="row">
<div id="chat" class="col-lg-7">
<h4><i class="fa fa-comments"></i>&nbsp;&nbsp;&nbsp;Chat Log</h4>
<hr>
<div id="log" class="text-muted ">
<span class="label label-info chat" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;">Chat</span> <span class="black">WilliamTiger:</span> hey
<br>
<span class="label label-info chat" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;">Chat</span> <span class="black">b2_mp:</span> you're a tiger
<br>
<span class="label label-info chat" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;">Chat</span> <span class="black">Mysticate:</span> that's right!!!
<br>
<span class="label label-info chat" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;">Chat</span> <span class="black">WilliamBurns:</span> f*** off not true
<br>
<span class="label label-info chat" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;">Chat</span> <span class="black">Phinary:</span> Please watch your language!
<br>
<span class="label label-primary chat pm" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;">PM</span> <span class="black">jaws12 -> Phinary:</span> somebody report him!
<br>
<span class="label label-primary chat pm" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;">PM</span> <span class="black"> Phinary -> jaws12:</span> Okay I will
</div>
</div>
<div id="users" class="col-lg-5">
<h4><i class="fa fa-info-circle"></i>&nbsp;&nbsp;&nbsp;Information</h4>
<hr>
<div class="row">
<div class="col-lg-1">
<i class="fa fa-calendar"></i><br>
<i class="fa fa-clock-o"></i><br>
<i class="fa fa-user-plus"></i><br>
<i class="fa fa-user-times"></i><br>
<i class="fa fa-gavel"></i><br>
</div>
<div class="col-lg-11">
<span class="label label-pill label-default">12/8/15</span><br>
<span class="label label-pill label-default">4:18 PM</span><br>
<span class="label label-pill label-success">Reported by Phinary, jaws12</span><br>
<span class="label label-pill label-danger">Suspect is WilliamBurns</span><br>
<span class="label label-pill label-warning">Staff Member assigned is Artix</span><br>
</div>
</div>
<br>
<h4><i class="fa fa-users"></i>&nbsp;&nbsp;&nbsp;Users</h4>
<hr>
<img src="http://cravatar.eu/avatar/WilliamTiger/55.png" class="pull-left" />
&nbsp;&nbsp;<b class="name">WilliamTiger</b> <span class="label label-staff name">JR.DEV</span><br>
&nbsp;<code style="font-size: 11px;">bfc8dadf-c568-4e31-9776-8e710669b02e</code>
<br><br>
<img src="http://cravatar.eu/avatar/b2_mp/55.png" class="pull-left" />
&nbsp;&nbsp;<b class="name">b2_mp</b> <span class="label label-staff name">LEADER</span><br>
&nbsp;<code style="font-size: 11px;">efaf9a17-2304-4f42-8433-421523c308dc</code>
<br><br>
<img src="http://cravatar.eu/avatar/jaws12/55.png" class="pull-left" />
&nbsp;&nbsp;<b class="name">jaws12</b> <span class="label label-staff name">DEV</span><br>
&nbsp;<code style="font-size: 11px;">00c6d020-4346-4f8a-a9b5-67d4bc3251bf</code>
<br><br>
<img src="http://cravatar.eu/avatar/WilliamBurns/55.png" class="pull-left" />
&nbsp;&nbsp;<b class="name">WilliamBurns</b> <span class="label label-ultra name">ULTRA</span><br>
&nbsp;<code style="font-size: 11px;">f9eedba5-668d-4ee5-9ee8-98edee840608</code>
<br><br>
<img src="http://cravatar.eu/avatar/Phinary/55.png" class="pull-left" />
&nbsp;&nbsp;<b class="name">Phinary</b> <span class="label label-staff name">LEADER</span><br>
&nbsp;<code style="font-size: 11px;">b33207e2-0dc5-4cbd-b3ee-6c860727f722</code>
<br><br>
<img src="http://cravatar.eu/avatar/Mysticate/55.png" class="pull-left" />
&nbsp;&nbsp;<b class="name">Mysticate</b> <span class="label label-staff name">JR.DEV</span><br>
&nbsp;<code style="font-size: 11px;">5c359761-d55b-43a4-9b75-2cc64f8d027f</code>
<br><br>
</div>
</div>
</div>
<div id="footer">
<a href="http://www.mineplex.com"><img src="img/logo-full.png" width="225px" /></a>
<div class="btn-group pull-right indent-link" style="font-family: 'Crete Round', serif; padding-top: 10px;">
<a href="http://www.mineplex.com" class="btn btn-link btn-small text-muted">Home</a>
<a href="http://www.mineplex.com/shop/" class="btn btn-link btn-small text-muted">Shop</a>
<a href="http://www.mineplex.com/forums/" class="btn btn-link btn-small text-muted">Forums</a>
<a href="http://www.mineplex.com/supporthub/" class="btn btn-link btn-small text-muted">Support</a>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,74 +0,0 @@
<?php class Report
{
/** @var Int */
private $id;
/** @var String */
private $serverName;
/** @var Player|Null */
private $handler;
/** @var Player */
private $suspect;
/** @var SplObjectStorage */
private $reporters;
/**
* Report constructor.
* @param Int $id
* @param String $serverName
* @param Player|Null $handler
* @param Player $suspect
* @param SplObjectStorage $reporters
*/
function Report($id, $serverName, $handler, $suspect, $reporters)
{
$this->id = $id;
$this->serverName = $serverName;
$this->handler = $handler;
$this->suspect = $suspect;
$this->reporters = $reporters;
}
/**
* @return Int
*/
public function getId()
{
return $this->id;
}
/**
* @return String
*/
public function getServerName()
{
return $this->serverName;
}
/**
* @return Player|Null
*/
public function getHandler()
{
return $this->handler;
}
/**
* @return Player
*/
public function getSuspect()
{
return $this->suspect;
}
/**
* @return SplObjectStorage
*/
public function getReporters()
{
return $this->reporters;
}
}

View File

@ -1,468 +0,0 @@
<?php
require_once('snapshot.php');
require_once('report.php');
require_once('message.php');
require_once('player.php');
const dataDir = 'data/';
const collapsedMessageCount = 20;
// In Java this is "DateTimeFormatter.ISO_LOCAL_DATE_TIME"
const jsonDateTimeFormat = 'Y-m-d\TH:i:s';
/** @var mysqli[] $connections */
$connections = array(); // String index = Connection name
/** PARSE DB CONNECTIONS */
$dbConfigFile = new SplFileObject('database-config.dat');
if ($dbConfigFile->isFile())
{
while (!$dbConfigFile->eof())
{
$line = trim($dbConfigFile->fgets());
if ($line) // check not empty line
{
$parts = explode(' ', $line);
$fullUrl = $parts[1];
$urlParts = explode('/', $fullUrl);
$host = $urlParts[0];
$database = $urlParts[1];
$name = $parts[0];
$username = $parts[2];
$password = $parts[3];
$connection = new mysqli($host, $username, $password, $database);
if ($connection->connect_error) {
die("Connection \"$name\" failed: $connection->connect_error");
}
$connections[$name] = $connection;
}
}
}
else
{
die('database-config.dat does not exist or is not a file.');
}
/**
* @param String $name
* @return mysqli
*/
function getConnection($name)
{
global $connections;
return $connections[$name];
}
/**
* @param String $filename
* @return array JSON data array.
*/
function toDataArray($filename)
{
return json_decode(file_get_contents($filename), true);
}
/**
* @param String $identifier
* @param array $snapshotData Snapshot data array.
* @return Snapshot
*/
function toSnapshot($identifier, $snapshotData)
{
$timezone = new DateTimeZone($snapshotData["timezone"]);
$players = toPlayers($snapshotData['usernames']);
$messages = toMessages($players, $snapshotData['snapshots'], $timezone);
$generated = parseDateTime($snapshotData["generated"], $timezone);
return new Snapshot($identifier, $messages, $players, $generated);
}
/**
* @param array $reportData
* @param Player[] $players
* @return Report
*/
function toReport($reportData, $players)
{
$id = $reportData["id"];
$serverName = $reportData["serverName"];
$handler = array_key_exists("handler", $reportData) ? getPlayer($players, $reportData["handler"]) : null;
$suspect = getPlayer($players, $reportData["suspect"]);
$reporters = toReporters($players, $reportData["reporters"]);
return new Report($id, $serverName, $handler, $suspect, $reporters);
}
/**
* @param Player[] $players
* @param array $messagesData Messages data array.
* @param DateTimeZone $timezone
* @return Message[]
*/
function toMessages($players, $messagesData, $timezone)
{
$messages = array();
for ($i = 0; $i < count($messagesData); $i++)
{
$messages[$i] = getMessage($players, $messagesData[$i], $timezone);
}
usort($messages, 'compareMessageTimes');
return $messages;
}
/**
* @param Message $messageA
* @param Message $messageB
* @return int
*/
function compareMessageTimes($messageA, $messageB)
{
return $messageA->getTimestamp()->getTimestamp() - $messageB->getTimestamp()->getTimestamp();
}
/**
* @param Player[] $players
* @param array $messageData Message data array.
* @param DateTimeZone $timezone
* @return Message
*/
function getMessage($players, $messageData, $timezone)
{
$sender = getPlayer($players, $messageData['sender']);
$recipients = getPlayersFromUUIDs($players, $messageData['recipients']);
$dateTime = parseDateTime($messageData['time'], $timezone);
$type = Message::getTypeFromString($messageData['type']);
$message = $messageData['message'];
return new Message($sender, $recipients, $dateTime, $type, $message);
}
/**
* @param Player[] $players
* @param String[] $uuidArray the UUIDs of the players to fetch
* @return Player[]
*/
function getPlayersFromUUIDs($players, $uuidArray)
{
$matchedPlayers = array();
for ($i = 0; $i < count($uuidArray); $i++)
{
$matchedPlayers[$i] = getPlayer($players, $uuidArray[$i]);
}
return $matchedPlayers;
}
/**
* @param Player[] $playersArray
* @return Player[]
*/
function toPlayers($playersArray) // String UUID as Key
{
$players = array();
foreach ($playersArray as $uuid => $username)
{
$connection = getConnection("ACCOUNT");
$statement = $connection->prepare("SELECT `rank` FROM `accounts` WHERE `uuid` = ?;");
$statement->bind_param('s', $uuid);
$statement->execute();
$statement->bind_result($rank);
$statement->fetch();
$statement->close();
$players[$uuid] = new Player($uuid, $username, $rank);
}
return $players;
}
/**
* @param $playersArray
* @param $reportersArray
* @return SplObjectStorage
*/
function toReporters($playersArray, $reportersArray)
{
$reporters = new SplObjectStorage();
foreach ($reportersArray as $reporterUUID => $reason)
{
$reporters[getPlayer($playersArray, $reporterUUID)] = $reason;
}
return $reporters;
}
/**
* @param Player[] $players
* @param String $uuid
* @return Player
*/
function getPlayer($players, $uuid)
{
$player = $players[$uuid];
if ($player != null)
{
return $player;
}
else
{
throw new RuntimeException('Player for UUID not found.');
}
}
/**
* @param String $dateTime
* @param DateTimeZone $timezone
* @return DateTime
*/
function parseDateTime($dateTime, $timezone)
{
return DateTime::createFromFormat(jsonDateTimeFormat, $dateTime, $timezone);
}
/**
* Converts an interval to minutes, days or months, depending on the size.
*
* @param DateInterval $interval
* @return string
*/
function approximateHumanInterval($interval)
{
if ($interval->m > 0)
{
$humanString = $interval->m . ' month' . ($interval->m != 0 ? 's' : '');
}
else if ($interval->d > 0)
{
$humanString = $interval->d . ' day' . ($interval->d != 0 ? 's' : '');
}
else if ($interval->h > 0)
{
$humanString = $interval->h . ' hour' . ($interval->h != 0 ? 's' : '');
}
else
{
$humanString = $interval->i . ' minute' . ($interval->i != 0 ? 's' : '');
}
return $humanString;
}
/**
* @param String $input
* @return String
*/
function removeBadCharacters($input)
{
return preg_replace('/[^A-Za-z0-9_\-]/', '_', $input);
}
function getExpandedURL()
{
$vars = $_GET;
$vars['expanded'] = true;
return '?' . http_build_query($vars);
}
$validIdentifier = isset($_GET['identifier']);
$identifierError = "";
$identifier = null;
$expanded = null;
$filePath = null;
$snapshot = null;
$report = null;
if ($validIdentifier)
{
$identifier = removeBadCharacters($_GET['identifier']); // prevents escaping
$filePath = dataDir . $identifier . '.json';
if (file_exists($filePath))
{
$dataArray = toDataArray($filePath);
$snapshot = toSnapshot($identifier, $dataArray);
$report = toReport($dataArray['report'], $snapshot->getPlayers());
$expanded = isset($_GET['expanded']) && $_GET['expanded'];
}
else
{
$validIdentifier = false;
$identifierError = "Invalid identifier.";
}
}
?>
<!DOCTYPE html>
<html>
<head>
<script src="js/jquery.js"></script>
<link rel="stylesheet" href="css/bootstrap.min.css">
<script src="js/bootstrap.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
<link rel="stylesheet" href="css/tiger.css">
<link href='https://fonts.googleapis.com/css?family=Crete+Round' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Oswald' rel='stylesheet' type='text/css'>
<title>
<?php if ($validIdentifier): ?>
Report #<?= $report->getId() ?>
<?php else: ?>
Report System
<?php endif; ?>
&middot; Mineplex
</title>
</head>
<body>
<div id="wrapper">
<div id="header">
<img src="img/logo.png" height="70px" width="70px" />
<h1>Report System</h1>
<!-- <h2><i class="fa fa-camera"></i>&nbsp;&nbsp; Chat Snap</h2> -->
</div>
<div id="search">
<form id="identifier-input" name="identifier-input" action="view.php" method="get">
<div class="input-group">
<input name="identifier" type="text" class="form-control" placeholder="Enter an identifier provided in-game...">
<span class="input-group-btn">
<button class="btn btn-secondary" type="submit" form="identifier-input"><i class="fa fa-search"></i> Search</button>
</span>
</div>
</form>
</div>
<?php if (isset($_GET['identifier']) && !$validIdentifier && !empty($identifierError)): ?>
<div id="content" class="center-block" style="text-align: center; background-color: rgba(204, 34, 42, 0.52);">
<p class="error-oh-no" style="font-size: 60px;">What did you do?!?!?</p>
<img src="img/shaun.gif" />
<p class="error-oh-no" style="font-size: 40px;">Error: <?= $identifierError ?></p>
<br>
</div>
<?php else: ?>
<?php if (!isset($_GET['identifier'])) exit();
// INITIALIZE
// Get messages and the amount that we are going to display
$messages = $snapshot->getMessages();
$messageCount = count($messages);
$displayAmount = $expanded || $messageCount <= collapsedMessageCount ? $messageCount : collapsedMessageCount;
// Calculate age from timestamp
$dateTime = $snapshot->getTimeGenerated();
$age = approximateHumanInterval($dateTime->diff(new DateTime('now', $dateTime->getTimezone())));
// Put all reporter usernames in array for easy access later
$reporterUsernames = array();
foreach ($report->getReporters() as $reporter)
{
$reporterUsernames[count($reporterUsernames)] = $reporter->getUsername();
}
?>
<div id="content">
<div>
<hr>
<h2 style="font-family: 'Oswald', sans-serif; text-align: center;">
Report #<?= $report->getId() ?>
</h2>
<hr>
</div>
<div class="row">
<div id="chat" class="col-lg-7">
<h4><i class="fa fa-comments"></i>&nbsp;&nbsp;&nbsp;Chat Log</h4>
<hr>
<div id="log" class="text-muted ">
<?php for ($i = 0; $i < $displayAmount; $i++):
$message = $messages[$i];
$typeId = $message->getType();
$typeDisplayName = Message::$TYPE_DISPLAY_NAMES[$typeId];
$isPM = $typeId == Message::TYPE_PM;
// If this is a PM, then the "-> <recipient>" suffix will be applied.
$involved = $message->getSender()->getUsername() . ($isPM ? " -> " . $message->getRecipients()[0]->getUsername() : "");
?>
<span class="label <?= $isPM ? "label-primary chat pm" : "label-info chat" ?>" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;"><?= $typeDisplayName ?></span>
<span class="black"><?= $involved; ?>:</span> <?= $message->getMessage(); ?>
<?php if ($i < $displayAmount - 1): // Don't break on the last element ?>
<br />
<?php endif; ?>
<?php endfor; ?>
</div>
<?php if (!$expanded && $displayAmount < $messageCount): ?>
<br />
<a href="<?= getExpandedURL() ?>">Show All (<?= $messageCount ?> messages)</a>
<?php endif; ?>
</div>
<div id="users" class="col-lg-5">
<h4><i class="fa fa-info-circle"></i>&nbsp;&nbsp;&nbsp;Information</h4>
<hr>
<div class="row">
<div class="col-lg-12">
<i class="fa fa-clock-o fa-fw"></i>
<span class="label label-pill label-default" title="<?= $dateTime->format('Y/m/d H:i:s T') ?>"><?= $age . ' old' ?></span>
<br>
<i class="fa fa-user-plus fa-fw"></i>
<span class="label label-pill label-success">Reported by <?= implode(", ", $reporterUsernames) ?></span>
<br>
<i class="fa fa-user-times fa-fw"></i>
<span class="label label-pill label-danger">Suspect is <?= $report->getSuspect()->getUsername() ?></span>
<br>
<i class="fa fa-gavel fa-fw"></i>
<span class="label label-pill label-warning">
<?php if ($report->getHandler() != null): ?>
Staff Member assigned is <?= $report->getHandler()->getUsername() ?>
<?php else: ?>
No Staff Member assigned
<?php endif; ?>
</span>
<br>
</div>
</div>
<br>
<h4><i class="fa fa-users"></i>&nbsp;&nbsp;&nbsp;Users</h4>
<hr>
<?php foreach($snapshot->getPlayers() as $player): ?>
<img src="http://cravatar.eu/avatar/<?= $player->getUUID() ?>/55.png" class="pull-left" />
&nbsp;&nbsp;<b class="name"><?= $player->getUsername() ?></b> <span class="label label-staff name"><?= $player->getRank() ?></span><br> <!-- TODO different styling for different ranks -->&nbsp;
<code style="font-size: 11px;"><?= $player->getUUID() ?></code>
<br><br>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endif; ?>
<div id="footer">
<a href="http://www.mineplex.com"><img src="img/logo-full.png" width="225px" /></a>
<div class="btn-group pull-right indent-link" style="font-family: 'Crete Round', serif; padding-top: 10px;">
<a href="http://www.mineplex.com" class="btn btn-link btn-small text-muted">Home</a>
<a href="http://www.mineplex.com/shop/" class="btn btn-link btn-small text-muted">Shop</a>
<a href="http://www.mineplex.com/forums/" class="btn btn-link btn-small text-muted">Forums</a>
<a href="http://www.mineplex.com/supporthub/" class="btn btn-link btn-small text-muted">Support</a>
</div>
</div>
</div>
</body>
</html>
<?php foreach ($connections as $connection) {
$connection->close();
} ?>

View File

@ -40,6 +40,10 @@ h2,h3,h4,h5,h6 {
min-height: 500px;
}
#chat {
padding-bottom: 1rem;
}
#footer {
border-top: solid 2px rgba(204, 204, 204, 0.64);
padding-left: 20%;
@ -85,6 +89,10 @@ h2,h3,h4,h5,h6 {
font-size: 14px;
}
#log .label {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
.black {
color: black;
}
@ -98,6 +106,10 @@ h2,h3,h4,h5,h6 {
padding-right: 10px;
}
.suspect {
color: #ED9898;
}
#test-bar {
align-content: center;
padding-top: 20px;

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 227 KiB

After

Width:  |  Height:  |  Size: 227 KiB

View File

Before

Width:  |  Height:  |  Size: 971 KiB

After

Width:  |  Height:  |  Size: 971 KiB

View File

@ -0,0 +1,17 @@
$(document).ready(function() {
/**
* Remove leading and trailing whitespace in username span
*/
$('#log').find("> .log-line > .remove-whitespace > span").filter(function() {
return $(this).text() === ': ';
}).prev().each(function() {
$(this).text($(this).text().trim());
});
/**
* Remove newlines and whitespace in tags with the '.remove-whitespace' class.
*/
$('.remove-whitespace').contents().filter(function() {
return this.nodeType = Node.TEXT_NODE && /\S/.test(this.nodeValue) === false;
}).remove();
});

Some files were not shown because too many files have changed in this diff Show More