package chatty.util.chatlog;
import chatty.Chatty;
import chatty.Helper;
import chatty.User;
import chatty.util.DateTime;
import chatty.util.DateTime.Formatting;
import chatty.util.StringUtil;
import chatty.util.api.ChannelInfo;
import chatty.util.api.StreamInfo.ViewerStats;
import chatty.util.api.pubsub.ModeratorActionData;
import chatty.util.settings.Settings;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
/**
* Translates chat specific messages into loggable lines and sends them to the
* LogManager.
*
* @author tduva
*/
public class ChatLog {
private static final Logger LOGGER = Logger.getLogger(ChatLog.class.getName());
private SimpleDateFormat sdf;
private final Map<String, Compact> compactForChannels;
private final Settings settings;
/**
* Reference to the LogManager. This is null if the log path was invalid and
* couldn't be created.
*/
private final LogManager log;
public ChatLog(Settings settings) {
this.settings = settings;
Path path = getPath();
if (path == null) {
log = null;
} else {
String logSplit = settings.getString("logSplit");
boolean logSubdirectories = settings.getBoolean("logSubdirectories");
boolean lockFiles = settings.getBoolean("logLockFiles");
this.log = new LogManager(path, logSplit, logSubdirectories, lockFiles);
}
compactForChannels = new HashMap<>();
try {
String timestamp = settings.getString("logTimestamp");
if (!timestamp.equals("off")) {
sdf = new SimpleDateFormat(timestamp+" ");
}
} catch (IllegalArgumentException ex) {
sdf = null;
}
}
/**
* Gets either the default log path, or the custom one from the settings, if
* not empty.
*
* @return The Path to write the log files to, or null if the path could not
* be created
*/
private Path getPath() {
String pathToUse = Chatty.getUserDataDirectory()+"logs";
String customPath = settings.getString("logPath");
if (!customPath.isEmpty()) {
pathToUse = customPath;
}
try {
return Paths.get(pathToUse);
} catch (InvalidPathException ex) {
LOGGER.warning("Invalid path for chatlog: "+pathToUse);
return null;
}
}
public void start() {
if (log != null) {
log.start();
}
}
public void message(String channel, User user, String message, boolean action) {
if (isEnabled(channel)) {
String line;
String name = user.getFullNick();
if (!user.hasRegularDisplayNick()) {
name += " ("+user.getName()+")";
}
if (action) {
line = timestamp()+"<"+name+">* "+message;
} else {
line = timestamp()+"<"+name+"> "+message;
}
writeLine(channel, line);
}
}
public void info(String channel, String message) {
if (isTypeEnabled("Info") && isEnabled(channel)) {
writeLine(channel, timestamp()+message);
}
}
public void modAction(ModeratorActionData data) {
if (!Helper.validateStream(data.stream)) {
return;
}
String channel = Helper.toChannel(data.stream);
if (isTypeEnabled("ModAction") && isEnabled(channel)) {
writeLine(channel, timestamp()+String.format("MOD_ACTION: %s (%s%s)",
data.created_by,
data.moderation_action,
data.args.isEmpty() ? "" : " "+StringUtil.join(data.args, " ")));
}
}
public void viewerstats(String channel, ViewerStats stats) {
if (isTypeEnabled("Viewerstats") && isEnabled(channel)) {
if (stats != null && stats.isValid()) {
writeLine(channel, timestamp()+stats);
//System.out.println(stats);
}
}
}
public void viewercount(String channel, int viewercount) {
if (isTypeEnabled("Viewercount") && isEnabled(channel)) {
writeLine(channel, timestamp()+"VIEWERS: "
+Helper.formatViewerCount(viewercount));
}
}
public void system(String channel, String message) {
if (isEnabled(channel) && isTypeEnabled("System")) {
writeLine(channel, timestamp()+message);
}
}
private void writeLine(String channel, String message) {
if (log != null) {
compactClose(channel);
log.writeLine(channel, message);
}
}
public void userBanned(String channel, String nick, long duration,
String reason, ChannelInfo info) {
String text = nick;
if (duration > 0) {
text += " ("+duration+"s)";
}
if (reason != null && !reason.isEmpty()) {
text += " ["+reason+"]";
}
if (info != null) {
text += " {"+DateTime.formatAccountAge(info.createdAt, Formatting.COMPACT)+"}";
}
compact(channel, "BAN", text);
}
public void compact(String channel, String type, String info) {
if (isEnabled(channel)) {
if ((type.equals("MOD") || type.equals("UNMOD")) && isTypeEnabled("Mod")
|| (type.equals("JOIN") || type.equals("PART")) && isTypeEnabled("JoinPart")
|| type.equals("BAN") && isTypeEnabled("Ban")) {
compactAdd(channel, type, info);
}
}
}
private void compactAdd(String channel, String type, String info) {
synchronized(compactForChannels) {
getCompact(channel).add(type, info);
}
}
private void compactClose(String channel) {
synchronized(compactForChannels) {
if (channel != null) {
getCompact(channel).close();
} else {
for (Compact c : compactForChannels.values()) {
c.close();
}
}
}
}
private Compact getCompact(String channel) {
Compact c = compactForChannels.get(channel);
if (c == null) {
c = new Compact(channel);
compactForChannels.put(channel, c);
}
return c;
}
private String timestamp() {
if (sdf == null) {
return "";
}
return DateTime.currentTime(sdf);
}
/**
* Close chatlogging, which writes any remaining lines in the buffer closes
* all files and stops the thread. This waits for the thread to finish, so
* it can take some time.
*/
public void close() {
if (log != null) {
compactClose(null);
log.close();
}
}
public void closeChannel(String channel) {
if (log != null) {
compactClose(channel);
log.writeLine(channel, null);
}
}
private boolean isEnabled(String channel) {
if (log == null) {
return false;
}
if (channel == null || channel.isEmpty()) {
return false;
}
String mode = settings.getString("logMode");
if (mode.equals("off")) {
return false;
}
if (mode.equals("always")) {
return true;
}
if (mode.equals("blacklist") && !settings.listContains("logBlacklist", channel)) {
return true;
}
if (mode.equals("whitelist") && settings.listContains("logWhitelist", channel)) {
return true;
}
return false;
}
private boolean isTypeEnabled(String type) {
return settings.getBoolean("log"+type);
}
private class Compact {
private static final String SEPERATOR = ", ";
private static final int MAX_LENGTH = 10;
private static final int MAX_TIME = 2000;
private final String channel;
private StringBuilder text;
private int length;
private long start;
private String mode;
public Compact(String channel) {
this.channel = channel;
}
/**
* Prints something in compact mode, meaning that nick events of the
* same type appear in the same line, for as long as possible.
*
* This is mainly used for a compact way of printing
* joins/parts/mod/unmod.
*
* @param type
* @param user
*/
protected void add(String type, String info) {
String seperator = SEPERATOR;
if (start(type)) {
// If compact mode has actually been started for this print,
// print prefix first
text = new StringBuilder();
text.append(timestamp());
text.append(type);
text.append(": ");
seperator = "";
}
text.append(seperator);
text.append(info);
length++;
// If max number of compact prints happened, close compact mode to
// start a new line
if (length >= MAX_LENGTH) {
close();
}
}
/**
* Enters compact mode, closes it first if necessary.
*
* @param type
* @return
*/
private boolean start(String type) {
// Check if max time has passed, and if so close first
long timePassed = System.currentTimeMillis() - start;
if (timePassed > MAX_TIME) {
close();
}
// If this is another type, close first
if (!type.equals(mode)) {
close();
}
// Only start if not already/still going
if (mode == null) {
mode = type;
start = System.currentTimeMillis();
length = 0;
return true;
}
return false;
}
/**
* Leaves compact mode (if necessary) and logs the buffered text.
*/
protected void close() {
if (mode != null && log != null) {
log.writeLine(channel, text.toString());
//System.out.println("Compact: "+text.toString());
mode = null;
}
}
}
}