commit b72c61a0193bf2985cdd887fad4762246a89d4cf Author: Dmitry Isaenko Date: Sun Dec 16 17:27:44 2018 +0300 v0.1 diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/InnaIrcBot.iml b/InnaIrcBot.iml new file mode 100644 index 0000000..e1b5c72 --- /dev/null +++ b/InnaIrcBot.iml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/InnaIrcBot/BotStart.java b/src/InnaIrcBot/BotStart.java new file mode 100644 index 0000000..518d6cd --- /dev/null +++ b/src/InnaIrcBot/BotStart.java @@ -0,0 +1,40 @@ +/** + * InnaIrcBot + * @author Dmitry Isaenko + * @version 0.1 "Батлкрузер" + * Russia, 2018. + * */ +package InnaIrcBot; + +import InnaIrcBot.Config.StorageFile; +import InnaIrcBot.Config.StorageReader; +import InnaIrcBot.ProvidersConsumers.DataProvider; + +public class BotStart { +//TODO: Steam link, flood control, kikbana +//TODO: setDaemon(true) +//TODO: multiple connections to one server not allowed + + public static void main(String[] args){ + if (args.length != 0) { + if (args.length >= 2) { + if (args[0].equals("--configuration") || args[0].equals("-c")) { + new Connections(args); + } else if (args[0].equals("--generate") || args[0].equals("-g")) { + StorageReader.generateDefaultConfig(args[1]); + } else if (args[0].equals("--version") || args[0].equals("-v")) { + System.out.println(GlobalData.getAppVersion()); + } + } + else if (args[0].equals("--generate") || args[0].equals("-g")){ + StorageReader.generateDefaultConfig(null); + } + } + else { + System.out.println("Usage:\n" + +" \t-c, --configuration [ ...]\tRead Config\n" + +"\t-g, --generate\t[name.config]\t\t\t\t\t\t\tGenerate Config\n" + +"\t-v, --version\t\t\t\t\t\t\t\t\t\t\tGet application version"); + } + } +} \ No newline at end of file diff --git a/src/InnaIrcBot/Commanders/ChanelCommander.java b/src/InnaIrcBot/Commanders/ChanelCommander.java new file mode 100644 index 0000000..37aab9f --- /dev/null +++ b/src/InnaIrcBot/Commanders/ChanelCommander.java @@ -0,0 +1,235 @@ +package InnaIrcBot.Commanders; + +import InnaIrcBot.ProvidersConsumers.StreamProvider; + +import java.io.*; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.regex.Pattern; + //TODO: FLOOD, JOIN FLOOD + // TODO: @ configuration level: if in result we have empty string, no need to pass it to server +public class ChanelCommander implements Runnable { + private BufferedReader reader; + private String server; + private String chanel; + //TODO: add timers + private HashMap joinMap; // Mask(Pattern) ->, Action | Where Action[0] could be: raw + private HashMap msgMap; // Mask(Pattern) ->, Action | Where Action[0] could be: raw + private HashMap nickMap; // Mask(Pattern) ->, Action | Where Action[0] could be: raw + + private boolean joinFloodTrackNeed = false; + private JoinFloodHandler jfh; + + public ChanelCommander(BufferedReader streamReader, String serverName, String chan, String configFilePath){ + this.reader = streamReader; + this.server = serverName; + this.chanel = chan; + + this.joinMap = new HashMap<>(); + this.msgMap = new HashMap<>(); + this.nickMap = new HashMap<>(); + readConfing(configFilePath); + } + + @Override + public void run() { + System.out.println("Thread for ChanelCommander started"); // TODO:REMOVE DEBUG + String data; + String[] dataStrings; + try { + while ((data = reader.readLine()) != null) { + dataStrings = data.split(" ",3); + //event initiatorArg messageArg + switch (dataStrings[0]) { + case "NICK": + nickCame(dataStrings[2]+dataStrings[1].replaceAll("^.+?!","!")); + break; // todo: need to track join flood + case "JOIN": + if (joinFloodTrackNeed) + jfh.track(simplifyNick(dataStrings[1])); + joinCame(dataStrings[1]); + break; + case "PRIVMSG": + privmsgCame(dataStrings[1], dataStrings[2]); + break; + /* + case "PART": // todo: need to track join flood? Fuck that. Track using JOIN + break; + case "QUIT": // todo: need this? + break; + case "TOPIC": // todo: need this? + break; + case "MODE": // todo: need this? + break; + case "KICK": // todo: need this? + break; */ + default: + break; + } + } + } catch (java.io.IOException e){ + System.out.println("Internal issue: thread ChanelCommander->run() caused I/O exception:\n\t"+e); // TODO: reconnect + } + System.out.println("Thread for ChanelCommander ended"); // TODO:REMOVE DEBUG + } + + // Do we need old nick? + private void nickCame(String newNick){ + came(nickMap, newNick, newNick); + } + private void joinCame(String who){ + came(joinMap, who, who); + } + private void privmsgCame(String who, String what){ + came(msgMap, what, who); + } + + private void came(HashMap map, String arg1, String arg2){ + for (String pattern : map.keySet()) + if (Pattern.matches(pattern, arg1)){ // NOTE: validation based on new nick //TODO: parse here + String[] cmdOrMsg = map.get(pattern); + for (int i = 0; i whatToSend; + switch (cmdOrMsg[i]) { + case "\\chanmsg": + whatToSend = new ArrayList<>(); + for (i++; (i < cmdOrMsg.length) && !(cmdOrMsg[i].startsWith("\\")); i++) + whatToSend.add(cmdOrMsg[i]); + msgAction(whatToSend.toArray(new String[0]), arg2, false); + break; + case "\\privmsg": + whatToSend = new ArrayList<>(); + for (i++; (i < cmdOrMsg.length) && !(cmdOrMsg[i].startsWith("\\")); i++) + whatToSend.add(cmdOrMsg[i]); + msgAction(whatToSend.toArray(new String[0]), arg2, true); + break; + case "\\ban": + banAction(arg2); + i++; + break; + case "\\kick": + whatToSend = new ArrayList<>(); + for (i++; (i < cmdOrMsg.length) && !(cmdOrMsg[i].startsWith("\\")); i++) + whatToSend.add(cmdOrMsg[i]); + kickAction(whatToSend.toArray(new String[0]), arg2); + break; + case "\\kickban": + whatToSend = new ArrayList<>(); + for (i++; (i < cmdOrMsg.length) && !(cmdOrMsg[i].startsWith("\\")); i++) + whatToSend.add(cmdOrMsg[i]); + banAction(arg2); + kickAction(whatToSend.toArray(new String[0]), arg2); + break; + case "\\raw": + StringBuilder whatToSendRaw = new StringBuilder(); + for (i++; (i < cmdOrMsg.length) && !(cmdOrMsg[i].startsWith("\\")); i++) + whatToSendRaw.append(cmdOrMsg[i]); + StreamProvider.writeToStream(server, whatToSendRaw.toString()); //TODO + break; //todo: add script + default: + i++; + } + } + } + } + ///////// ///////// + private void msgAction(String[] messages, String who, boolean sendToPrivate){ + StringBuilder executiveStr = new StringBuilder(); + executiveStr.append("PRIVMSG "); + if(sendToPrivate) { + executiveStr.append(simplifyNick(who)); + executiveStr.append(" :"); + } + else { + executiveStr.append(chanel); + executiveStr.append(" :"); + executiveStr.append(simplifyNick(who)); + executiveStr.append(": "); + } + + for (int i = 1; i= 3 && directive[0] != null && !directive[0].startsWith("#") && directive[1] != null && directive[2] != null){ + //System.out.println(Arrays.toString(directive)); // TODO:debug + switch (directive[0].toLowerCase()){ + case "join": + joinMap.put(directive[1], Arrays.copyOfRange(directive, 2, directive.length)); + break; + case "msg": + msgMap.put(directive[1], Arrays.copyOfRange(directive, 2, directive.length)); + break; + case "nick": + nickMap.put(directive[1], Arrays.copyOfRange(directive, 2, directive.length)); + break; + case "joinfloodcontrol": + if (!directive[1].isEmpty() && !directive[2].isEmpty() && Pattern.matches("^[0-9]+?$", directive[1].trim()) && Pattern.matches("^[0-9]+?$", directive[2].trim())) { + int events = Integer.valueOf(directive[1].trim()); + int timeFrame = Integer.valueOf(directive[2].trim()); + if (events > 0 && timeFrame > 0) { + jfh = new JoinFloodHandler(events, timeFrame, server, chanel); + joinFloodTrackNeed = true; + } else { + System.out.println("Internal issue: thread ChanelCommander->parse(): 'Number of events' and/or 'Time Frame in seconds' should be greater then 0"); + } + } + else + System.out.println("Internal issue: thread ChanelCommander->parse(): 'Number of events' and/or 'Time Frame in seconds' should be numbers greater then 0"); + break; + } + } + } + private String simplifyNick(String nick){ return nick.replaceAll("!.*$",""); } + + private void readConfing(String confFilesPath){ + if (!confFilesPath.endsWith(File.separator)) + confFilesPath += File.separator; + File file = new File(confFilesPath+server+chanel+".csv"); // TODO: add/search for filename + if (!file.exists()) + return; + try { + BufferedReader br = new BufferedReader(new FileReader(file)); + String line; + while ((line = br.readLine()) != null) { + parse(line.split("\t")); + } + } catch (FileNotFoundException e) { + System.out.println("Internal issue: thread ChanelCommander->readConfig() can't find file:\t\n"+e); + } catch (IOException e){ + System.out.println("Internal issue: thread ChanelCommander->readConfig() I/O exception:\t\n"+e); + } + } +} \ No newline at end of file diff --git a/src/InnaIrcBot/Commanders/JoinFloodHandler.java b/src/InnaIrcBot/Commanders/JoinFloodHandler.java new file mode 100644 index 0000000..4e9875b --- /dev/null +++ b/src/InnaIrcBot/Commanders/JoinFloodHandler.java @@ -0,0 +1,60 @@ +package InnaIrcBot.Commanders; + +import InnaIrcBot.ProvidersConsumers.StreamProvider; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +//TODO: implement method to +public class JoinFloodHandler { + private int eventBufferSize; // How many events should happens before we start validation + private int timeFrameInSeconds; // For which period critical amount of events should happens + private String server; + private String chanel; + private HashMap usersOnChanel; + + public JoinFloodHandler(int events, int timeFrameInSeconds, String serverName, String chanelName){ + this.eventBufferSize = events; + this.timeFrameInSeconds = timeFrameInSeconds; + this.server = serverName; + this.chanel = chanelName; + this.usersOnChanel = new HashMap<>(); + } + + public void track(String userNick){ + if(usersOnChanel.containsKey(userNick)){ + LocalDateTime timeOfFirstEvent = usersOnChanel.get(userNick).addLastGetFirst(); + if (timeOfFirstEvent.isAfter(LocalDateTime.now().minusSeconds(timeFrameInSeconds))) { // If first event in the queue happened after 'timeFrameSeconds ago from now' + StreamProvider.writeToStream(server, "PRIVMSG "+chanel+" :"+userNick+": join flood ("+eventBufferSize+" connections in "+timeFrameInSeconds+"seconds).\n"+ + "MODE "+chanel+" +b "+userNick+"!*@*"); // Shut joins-liker down. By nick TODO: consider other ban methods + usersOnChanel.remove(userNick); // Delete viewing history (in case op/hop decided to unban) + } + } + else { + usersOnChanel.put(userNick, new LprFIFOQueue(eventBufferSize)); // Create buffer for tracking new user joined + usersOnChanel.get(userNick).addLastGetFirst(); // Write his/her first join time + } + } +} + +class LprFIFOQueue { + private int size; + private ArrayList dateTimes;// todo stack or deque + + LprFIFOQueue(int size){ + this.size = size; + this.dateTimes = new ArrayList<>(); + } + + LocalDateTime addLastGetFirst(){ // FIFO-like + // set + if (dateTimes.size() >= size) + dateTimes.remove(0); + dateTimes.add(LocalDateTime.now()); // add current time + // get + if (dateTimes.size() < size) + return LocalDateTime.MIN; // todo: check if null is better + else + return dateTimes.get(0); + } +} \ No newline at end of file diff --git a/src/InnaIrcBot/Commanders/PrivateMsgCommander.java b/src/InnaIrcBot/Commanders/PrivateMsgCommander.java new file mode 100644 index 0000000..2ac78b4 --- /dev/null +++ b/src/InnaIrcBot/Commanders/PrivateMsgCommander.java @@ -0,0 +1,316 @@ +package InnaIrcBot.Commanders; + +import InnaIrcBot.GlobalData; +import InnaIrcBot.ProvidersConsumers.StreamProvider; +import InnaIrcBot.ReconnectControl; + +import java.util.ArrayList; + +public class PrivateMsgCommander { // TODO: add black list: add users after failed login queries ; temporary ban + private String serverName; + private ArrayList administrators; + private String password; + + public PrivateMsgCommander(String server, String adminPassword){ + this.serverName = server; + this.administrators = new ArrayList<>(); + this.password = adminPassword.trim(); + } + + public void receiver(String sender, String message){ + if (!password.isEmpty()) { + if (administrators.contains(sender) && !message.isEmpty()) { + String[] cmd = message.split("(\\s)|(\t)+?", 2); + cmd[0] = cmd[0].toLowerCase(); + + switch (cmd[0]){ + case "tell": + if ((cmd.length == 2) && (cmd[1].split("(\\s)|(\t)+?",2).length == 2)) { + String[] tellArgs = cmd[1].split("(\\s)|(\t)+?", 2); + tell(tellArgs[0], tellArgs[1]); + } + else + tell(simplifyNick(sender), "Pattern: tell "); + break; + case "join": + if (cmd.length == 2) + join(cmd[1]); + else + tell(simplifyNick(sender), "Pattern: join "); + break; + case "quit": + if (cmd.length == 2) + quit(cmd[1]); + else + quit(""); + break; + case "nick": + if (cmd.length == 2) + nick(cmd[1]); + else + tell(simplifyNick(sender), "Pattern: nick "); + break; + case "part": //TODO: update + if (cmd.length == 2) + part(cmd[1]); + else + tell(simplifyNick(sender), "Pattern: part [reason]"); + break; + case "ctcp": + if ((cmd.length == 2) && (cmd[1].split("(\\s)|(\t)+?",2).length == 2)) { + String[] ctcpArgs = cmd[1].split("(\\s)|(\t)+?", 2); + ctcp(ctcpArgs[0], ctcpArgs[1]); + } + else + tell(simplifyNick(sender), "Pattern: ctcp "); + break; + case "notice": + if ((cmd.length == 2) && (cmd[1].split("(\\s)|(\t)+?",2).length == 2)) { + String[] noticeArgs = cmd[1].split("(\\s)|(\t)+?", 2); + notice(noticeArgs[0], noticeArgs[1]); + } + else + tell(simplifyNick(sender), "Pattern: notice "); + break; + case "umode": + if ((cmd.length == 2) && (cmd[1].split("(\\s)|(\t)+?",2).length == 2)) { + String[] modeArgs = cmd[1].split("(\\s)|(\t)+?", 2); + umode(modeArgs[0], modeArgs[1]); + } + else + tell(simplifyNick(sender), "Pattern: umode <[+|-]mode_single_char>"); + break; + case "raw": + if (cmd.length == 2) + raw(cmd[1]); + else + tell(simplifyNick(sender), "Pattern: raw "); + break; + case "cmode": + if ((cmd.length >= 2) && (cmd[1].split("(\\s)|(\t)+?",3).length >= 2)) { + String[] args = cmd[1].split("(\\s)|(\t)+?", 3); + if (args.length == 2) + cmode(args[0], args[1], null); + else if(args.length == 3) + cmode(args[0], args[1], args[2]); + } + else + tell(simplifyNick(sender), "Pattern: cmode [| ]"); + + break; + case "k": + case "kick": + if ((cmd.length >= 2) && (cmd[1].split("(\\s)|(\t)+?",3).length >= 2)) { + String[] args = cmd[1].split("(\\s)|(\t)+?", 3); + if (args.length == 2) + kick(args[0], args[1], null); + else if(args.length == 3) + kick(args[0], args[1], args[2]); + } + else + tell(simplifyNick(sender), "Pattern: [k|kick] [reason]"); + break; + case "b": + case "ban": + if ((cmd.length == 2) && (cmd[1].split("(\\s)|(\t)+?",2).length == 2)) { + String[] banArgs = cmd[1].split("(\\s)|(\t)+?", 2); + ban(banArgs[0], banArgs[1]); + } + else + tell(simplifyNick(sender), "Pattern: [b|ban] "); + break; + case "-b": + case "unban": + if ((cmd.length == 2) && (cmd[1].split("(\\s)|(\t)+?",2).length == 2)) { + String[] banArgs = cmd[1].split("(\\s)|(\t)+?", 2); + unban(banArgs[0], banArgs[1]); + } + else + tell(simplifyNick(sender), "Pattern: [-b|unban] "); + break; + case "kb": + case "kickban": + if ((cmd.length >= 2) && (cmd[1].split("(\\s)|(\t)+?",3).length >= 2)) { + String[] args = cmd[1].split("(\\s)|(\t)+?", 3); + if (args.length == 2) + kickban(args[0], args[1], null); + else if(args.length == 3) + kickban(args[0], args[1], args[2]); + } + else + tell(simplifyNick(sender), "Pattern: [kb|kickban] "); + break; + case "v": + case "voice": + if ((cmd.length == 2) && (cmd[1].split("(\\s)|(\t)+?",2).length == 2)) { + String[] voiceArgs = cmd[1].split("(\\s)|(\t)+?", 2); + voice(voiceArgs[0], voiceArgs[1]); + } + else + tell(simplifyNick(sender), "Pattern: [v|voice] "); + break; + case "-v": + case "unvoice": + if ((cmd.length == 2) && (cmd[1].split("(\\s)|(\t)+?",2).length == 2)) { + String[] voiceArgs = cmd[1].split("(\\s)|(\t)+?", 2); + unvoice(voiceArgs[0], voiceArgs[1]); + } + else + tell(simplifyNick(sender), "Pattern: [-v|unvoice] "); + break; + case "h": + case "hop": + if ((cmd.length == 2) && (cmd[1].split("(\\s)|(\t)+?",2).length == 2)) { + String[] hopArgs = cmd[1].split("(\\s)|(\t)+?", 2); + hop(hopArgs[0], hopArgs[1]); + } + else + tell(simplifyNick(sender), "Pattern: [h|hop] "); + break; + case "-h": + case "unhop": + if ((cmd.length == 2) && (cmd[1].split("(\\s)|(\t)+?",2).length == 2)) { + String[] hopArgs = cmd[1].split("(\\s)|(\t)+?", 2); + unhop(hopArgs[0], hopArgs[1]); + } + else + tell(simplifyNick(sender), "Pattern: [-h|unhop] "); + break; + case "o": + case "op": + if ((cmd.length == 2) && (cmd[1].split("(\\s)|(\t)+?",2).length == 2)) { + String[] operatorArgs = cmd[1].split("(\\s)|(\t)+?", 2); + op(operatorArgs[0], operatorArgs[1]); + } + else + tell(simplifyNick(sender), "Pattern: [o|operator] "); + break; + case "-o": + case "unop": + if ((cmd.length == 2) && (cmd[1].split("(\\s)|(\t)+?",2).length == 2)) { + String[] args = cmd[1].split("(\\s)|(\t)+?", 2); + unop(args[0], args[1]); + } + else + tell(simplifyNick(sender), "Pattern: [-o|unoperator] "); + break; + case "topic": + if ((cmd.length == 2) && (cmd[1].split("(\\s)|(\t)+?",2).length == 2)) { + String[] args = cmd[1].split("(\\s)|(\t)+?", 2); + topic(args[0], args[1]); + } + else + tell(simplifyNick(sender), "Pattern: topic "); + break; + case "invite": + if ((cmd.length == 2) && (cmd[1].split("(\\s)|(\t)+?",2).length == 2)) { + String[] args = cmd[1].split("(\\s)|(\t)+?", 2); + invite(args[0], args[1]); + } + else + tell(simplifyNick(sender), "Pattern: invite "); + break; + default: + tell(simplifyNick(sender), "Unknown command. Could be: join, part, quit, tell, nick, ctcp, notice, umode, cmode, raw, kick[k], ban[b], unban[-b], kickban[kb], voice[v], unvoice[-v], hop[h], unhop[-h], op[o], unop[-o], topic, invite and (login)"); + } // TODO: chanel limits set/remove + } else { + if (!message.isEmpty() && message.startsWith("login ")) { + login(sender, message.replaceAll("^([\t\\s]+)?login([\t\\s]+)|([\t\\s]+$)", "")); + } + } + } + } + + private void join(String channel){ + raw("JOIN "+channel); + } + private void part(String channel){ + raw("PART "+channel); + } + private void quit(String message){ + if (message.isEmpty()){ + raw("QUIT :"+ GlobalData.getAppVersion()); + } + else + raw("QUIT :"+message); + ReconnectControl.update(serverName, false); + //ReconnectControl.update(serverName, true); + //System.exit(0); // TODO: change to normal exit + } + private void tell(String channelUser, String message){ + raw("PRIVMSG "+channelUser+" :"+message); + } + private void nick(String newNick){ + raw("NICK "+newNick); + } + private void ctcp(String object, String command){ + raw("PRIVMSG "+object+" :\u0001"+command.toUpperCase()+"\u0001"); + } + private void notice(String channelUser, String message){ + raw("NOTICE "+channelUser+" :"+message); + } + private void umode(String object, String mode){ + raw("MODE "+object+" "+mode); + } + private void cmode(String object, String mode, String user){ + if (user == null) + raw("MODE "+object+" "+mode); + else + raw("MODE "+object+" "+mode+" "+user); + } + private void raw(String rawText){ + StreamProvider.writeToStream(serverName, rawText); + } + private void kick(String chanel, String user, String reason){ + if (reason == null) + raw("KICK "+chanel+" "+user+" :requested"); + else + raw("KICK "+chanel+" "+user+" :"+reason); + } + private void ban(String chanel, String user){ + cmode(chanel, "+b", simplifyNick(user)+"*!*@*"); // TODO: work on patter.n + if (user.contains("@")){ + cmode(chanel, "+b", "*!*@"+user.replaceAll("^.+@","")); + } + } + private void unban(String chanel, String user){ + cmode(chanel, "-b", simplifyNick(user)+"*!*@*"); + if (user.contains("@")){ + cmode(chanel, "-b", "*!*@"+user.replaceAll("^.+@","")); + } + } + private void kickban(String chanel, String user, String reason){ + cmode(chanel, "+b", simplifyNick(user)+"*!*@*"); + kick(chanel, user, reason); + } + private void voice(String chanel, String user){ + cmode(chanel, "+v", user); + } + private void unvoice(String chanel, String user){ + cmode(chanel, "-v", user); + } + private void hop(String chanel, String user){ + cmode(chanel, "+h", user); + } + private void unhop(String chanel, String user){ + cmode(chanel, "-h", user); + } + private void op(String chanel, String user){ + cmode(chanel, "+o", user); + } + private void unop(String chanel, String user){ + cmode(chanel, "-o", user); + } + private void topic(String channel, String topic){ raw("TOPIC "+channel+" :"+topic); } + private void invite(String user, String chanel){ + raw("INVITE "+user+" "+chanel); + } + + private void login(String candidate, String key){ + if (key.equals(password)) { + administrators.add(candidate); + tell(simplifyNick(candidate), "Granted."); + } + } + private String simplifyNick(String nick){ return nick.replaceAll("!.*$",""); } +} \ No newline at end of file diff --git a/src/InnaIrcBot/Config/StorageFile.java b/src/InnaIrcBot/Config/StorageFile.java new file mode 100644 index 0000000..fd5389d --- /dev/null +++ b/src/InnaIrcBot/Config/StorageFile.java @@ -0,0 +1,63 @@ +package InnaIrcBot.Config; + +public class StorageFile { + private final String serverName; + private final int serverPort; + private final String[] channels; + private final String userNick; + private final String userIdent; + private final String userRealName; + private final String userNickPass; + private final String userNickAuthStyle; + private final String userMode; + private final boolean rejoinOnKick; + private final String logDriver; + private final String[] logDriverParameters; + private final String botAdministratorPassword; + private final String chanelConfigurationsPath; + + public String getServerName() { return serverName; } + public int getServerPort() { return serverPort; } + public String[] getChannels() { return channels; } + public String getUserNick() { return userNick; } + public String getUserIdent() { return userIdent; } + public String getUserRealName() { return userRealName; } + public String getUserNickPass() { return userNickPass; } + public String getUserNickAuthStyle() { return userNickAuthStyle; } + public String getUserMode() { return userMode; } + public boolean getRejoinOnKick() { return rejoinOnKick; } + public String getLogDriver() { return logDriver; } + public String[] getLogDriverParameters() { return logDriverParameters; } + public String getBotAdministratorPassword() { return botAdministratorPassword; } + public String getChanelConfigurationsPath() { return chanelConfigurationsPath; } + + public StorageFile(String serverName, + int serverPort, + String[] channels, + String userNick, + String userIdent, + String userRealName, + String userNickPass, + String userNickAuthStyle, + String userMode, + boolean rejoinOnKick, + String logDriver, + String[] logDriverParameters, + String botAdministratorPassword, + String chanelConfigurationsPath){ + this.serverName = serverName; + this.serverPort = serverPort; + this.channels = channels; + this.userIdent = userIdent; + this.userNick = userNick; + this.userRealName = userRealName; + this.userNickPass = userNickPass; + this.userNickAuthStyle = userNickAuthStyle; + this.userMode = userMode; + this.rejoinOnKick = rejoinOnKick; + this.logDriver = logDriver; + this.logDriverParameters = logDriverParameters; + this.botAdministratorPassword = botAdministratorPassword; + this.chanelConfigurationsPath = chanelConfigurationsPath; + } +} diff --git a/src/InnaIrcBot/Config/StorageReader.java b/src/InnaIrcBot/Config/StorageReader.java new file mode 100644 index 0000000..6f89afb --- /dev/null +++ b/src/InnaIrcBot/Config/StorageReader.java @@ -0,0 +1,95 @@ +package InnaIrcBot.Config; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.*; +import java.nio.charset.StandardCharsets; + +public class StorageReader { + public static StorageFile readConfig(String pathToFile){ // TODO: NULL or object + StorageFile storageObject = null; + + File configFile = new File(pathToFile); + + try (Reader fileReader = new InputStreamReader(new FileInputStream(configFile))) { + storageObject = new Gson().fromJson(fileReader, StorageFile.class); + return validateConfig(storageObject); + } catch (java.io.FileNotFoundException e){ + System.out.println("Configuration file not found."); + return null; + } catch (java.io.IOException e){ + System.out.println("Configuration file is empty or incorrect."); + return null; + } + } + private static StorageFile validateConfig(StorageFile sf){ //TODO: more validation + + if(sf.getServerName().isEmpty()){ + System.out.println("Server not defined in configuration file."); + return null; + } + else if(sf.getServerPort() <= 0){ + System.out.println("Server port set incorrectly in configuration file."); + return null; + } + else + return sf; + } + public static void generateDefaultConfig(String pathToFile){ + File savingFile; + if (pathToFile == null) { // no pathToFile? create new in homeDir + pathToFile = System.getProperty("user.dir") + +File.separator + +"myBotConfig.conf"; + savingFile = new File(pathToFile); + } + else if(pathToFile.endsWith(File.separator)) { // ends with .../ then create in dir + pathToFile = pathToFile + "myBotConfig.conf"; + savingFile = new File(pathToFile); + if (!savingFile.getParentFile().exists()) + savingFile.getParentFile().mkdirs(); + } + else { // check if it's dir, if yes, then create inside + savingFile = new File(pathToFile); + if (savingFile.exists() && savingFile.isDirectory()) { + pathToFile = pathToFile + File.separator + "myBotConfig.conf"; + savingFile = new File(pathToFile); + } + else if (!savingFile.getParentFile().exists()) + savingFile.getParentFile().mkdirs(); + } + try { + savingFile.createNewFile(); + Writer writerFile = new OutputStreamWriter(new FileOutputStream(savingFile.getAbsolutePath()), StandardCharsets.UTF_8); + + StorageFile storageFileObject = new StorageFile("srv", + 6667, + new String[] {"#lpr", + "#main"}, + "user_nick", + "ident", + "bot", + "", + "freenode", + "ix", + true, + "Files", + new String[] {System.getProperty("user.home")}, + "pswd", + System.getProperty("user.home") + ); + + Gson writingStorageObject = new GsonBuilder().setPrettyPrinting().create(); + writingStorageObject.toJson(storageFileObject, writerFile); + writerFile.close(); + System.out.println("Configuration file created: " + pathToFile); + } catch (java.io.FileNotFoundException e){ + System.out.println("Configuration file not found or can't create:\n\t"+e); + } catch (java.io.UnsupportedEncodingException e){ + System.out.println("Unsupported encoding of the configuration file:\n\t"+e); + } catch (java.io.IOException e){ + System.out.println("Unable to write configuration file: I/O exception:\n\t"+e); + } + } +} diff --git a/src/InnaIrcBot/Connections.java b/src/InnaIrcBot/Connections.java new file mode 100644 index 0000000..780b940 --- /dev/null +++ b/src/InnaIrcBot/Connections.java @@ -0,0 +1,82 @@ +package InnaIrcBot; + +import InnaIrcBot.Config.StorageFile; +import InnaIrcBot.Config.StorageReader; +import InnaIrcBot.ProvidersConsumers.DataProvider; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; + +public class Connections { +//todo PING Server if not available + private HashMap configs; + private ArrayList connectionsList; + + Connections(String[] args){ + this.configs = new HashMap<>(); + this.connectionsList = new ArrayList<>(); + StorageFile tempConf = null; + + for (int i = 1; i < args.length; i++){ + if ((tempConf = StorageReader.readConfig(args[i])) != null) { + configs.put(tempConf.getServerName(), tempConf); + } + else + System.out.println("Connections->constructor: configuration argument dropped: "+args[i]); + } + if (!configs.isEmpty()){ + handleThreads(); + } + else { + System.out.println("Connections->constructor: Nothing to execute."); + } + } + private void createAndStartThread(String serverName){ + //if connectionsList already contains record with name, it should be removed from there first + // if there are few configs with same server name then.. fuckup + + Runnable runnableConnection = new DataProvider(configs.get(serverName)); + Thread threadConnection = new Thread(runnableConnection, serverName); + threadConnection.start(); + connectionsList.add(threadConnection); + } + private void handleThreads() { + // Crate array of threads + for (String serverName : configs.keySet()) + createAndStartThread(serverName); + // Watch threads + Iterator it = connectionsList.iterator(); + + while (it.hasNext()) { + System.out.println("\n" + it.next().getName() + "\n"); + } + + while (!connectionsList.isEmpty()) { // While we have something in connectionList + while (it.hasNext()) { // Proceed for every thread in the list + Thread curThread = it.next(); + if (!curThread.isAlive()) // If thread is dead + if (ReconnectControl.get(curThread.getName())) { // And ReconnectControl says that this thread shouldn't be dead + ReconnectControl.delete(curThread.getName()); // [Try to] remove rule-record from ReconnectControl structure + connectionsList.remove(curThread); + createAndStartThread(curThread.getName()); + System.out.println("DEBUG: Thread "+curThread.getName()+" going to restart after unexpected finish.\n\t"+connectionsList.toString()); + break; + } else { // And ReconnectControl says that this thread death expected + ReconnectControl.delete(curThread.getName()); // [Try to] remove rule-record from ReconnectControl structure + connectionsList.remove(curThread); + System.out.println("DEBUG: Thread "+curThread.getName()+" removed from observable list after expected finish.\n\t"+connectionsList.toString()); + break; + } + } + if (connectionsList.isEmpty()) { + System.out.println("connectionsList.isEmpty()"); + break; + } + else { + it = connectionsList.iterator(); + } + + } + } +} diff --git a/src/InnaIrcBot/GlobalData.java b/src/InnaIrcBot/GlobalData.java new file mode 100644 index 0000000..076074a --- /dev/null +++ b/src/InnaIrcBot/GlobalData.java @@ -0,0 +1,8 @@ +package InnaIrcBot; + +public class GlobalData { + public static final String version = "InnaIrcBot v0.1 \"Батлкрузер\""; + public static synchronized String getAppVersion(){ + return version; + } +} diff --git a/src/InnaIrcBot/LogDriver/BotDriver.java b/src/InnaIrcBot/LogDriver/BotDriver.java new file mode 100644 index 0000000..2824197 --- /dev/null +++ b/src/InnaIrcBot/LogDriver/BotDriver.java @@ -0,0 +1,38 @@ +package InnaIrcBot.LogDriver; + +import java.util.HashMap; + +public class BotDriver { + private static HashMap serverDriver = new HashMap<>(); + /** + * Define driver for desired server + * */ // TODO: add proxy worker for using with multiple drivers + public static synchronized boolean setFileDriver(String serverName, String driver, String[] driverParams){ + if (!driver.isEmpty() && driverParams != null && driverParams.length > 0 && driverParams[0] != null && !driverParams[0].isEmpty()) { + String[][] drvAndParams = { + {driver}, + driverParams + }; + serverDriver.put(serverName, drvAndParams); + return true; + } + else + return false; + } + public static synchronized Worker getWorker(String serverName, String chanelName){ + if (serverDriver.containsKey(serverName)) { + switch (serverDriver.get(serverName)[0][0]) { + case "Files": + return new BotFilesWorker(serverName, serverDriver.get(serverName)[1], chanelName); + case "SQLite": + return new BotSQLiteWorker(serverName, serverDriver.get(serverName)[1], chanelName); + case "Zero": + return new BotZeroWorker(); + default: + System.out.println("Configuration issue: BotDriver->getWorker() can't find required driver \""+serverDriver.get(serverName)[0][0]+"\".Using \"ZeroWorker\"."); + return new BotZeroWorker(); + } + } + return null; + } +} diff --git a/src/InnaIrcBot/LogDriver/BotFilesWorker.java b/src/InnaIrcBot/LogDriver/BotFilesWorker.java new file mode 100644 index 0000000..28454be --- /dev/null +++ b/src/InnaIrcBot/LogDriver/BotFilesWorker.java @@ -0,0 +1,163 @@ +package InnaIrcBot.LogDriver; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.regex.Pattern; + +public class BotFilesWorker implements Worker { + private String filePath; + private boolean consistent = false; + private DateTimeFormatter dateFormat; + + private LocalDate fileWriterDay; + private FileWriter fileWriter; + + public BotFilesWorker(String server, String[] driverParameters, String channel){ + if (System.getProperty("os.name").startsWith("Windows")){ + channel = channel.replaceAll("\",",","); + } + else { + channel = channel.replaceAll("/",","); + } + + driverParameters[0] = driverParameters[0].trim(); //Consider parameters[0] as dirLocation + String dirLocation; + if (driverParameters[0].endsWith(File.separator)) + dirLocation = driverParameters[0]+server; + else + dirLocation = driverParameters[0]+File.separator+server; + File dir = new File(dirLocation); + dir.mkdirs(); + if (!dir.exists()) { + System.out.println("Unable to create directory to store files: " + dirLocation); //TODO: notify requester + this.consistent = false; + } + this.filePath = dirLocation+File.separator+channel; + + dateFormat = DateTimeFormatter.ofPattern("HH:mm:ss"); + if (resetFileWriter(false)) + this.consistent = true; + } + + private boolean resetFileWriter(boolean reassign){ + try { + if (reassign) + this.fileWriter.close(); + this.fileWriterDay = LocalDate.now(); + this.fileWriter = new FileWriter(this.filePath+"_"+LocalDate.now().toString()+".txt", true); + return true; + } + catch (java.io.IOException e){ + System.out.println("Internal issue: BotFilesWorker->constructor() can't create file to store logs: "+this.filePath); + return false; + } + } + @Override + public boolean isConsistent() { + return consistent; + } + + /** + * argument[0] should be always 'from whom' + * argument[1] should be always 'subject' + * */ + @Override + public void logAdd(String event, String initiatorArg, String messageArg) { + switch (event){ + case "PRIVMSG": + PRIVMSG(initiatorArg, messageArg); + break; + case "JOIN": + JOIN(initiatorArg, messageArg); + break; + case "MODE": + MODE(initiatorArg, messageArg); + break; + case "KICK": + KICK(initiatorArg, messageArg); + break; + case "PART": + PART(initiatorArg, messageArg); + break; + case "QUIT": + QUIT(initiatorArg, messageArg); + break; + case "NICK": + NICK(initiatorArg, messageArg); + break; + case "TOPIC": + TOPIC(initiatorArg, messageArg); + break; + default: + this.prettyPrint("["+LocalTime.now().format(dateFormat)+"] "+event+" "+initiatorArg+" "+messageArg+"\n"); // TODO: QA @ big data + break; + } + } + @Override + public void close() { + try { + fileWriter.close(); + } + catch (java.io.IOException e){ + System.out.println("Internal issue: BotFilesWorker->close() failed\n\tUnable to properly close file: "+this.filePath); // Live with it. + } + + } + private void prettyPrint(String string){ + if (LocalDate.now().isAfter(fileWriterDay)) + resetFileWriter(true); + try { + fileWriter.write(string); + fileWriter.flush(); + } catch (IOException e) { + System.out.println("Internal issue: BotFilesWorker->prettyPrint() failed\n\tUnable to write logs of "+this.filePath+" because of internal failure in LocalTime representation."); + consistent = false; + } + } + private String genDate(){ + return "["+LocalTime.now().format(dateFormat)+"] "; + } + private String getUserNameOnly(String userNameFull){return userNameFull.replaceAll("!.+$", "");} + private String getUserNameAndHost(String userNameFull){return userNameFull.replaceAll("!.+$", "")+" [!"+userNameFull.replaceAll("^.+!", "")+"] ";} + + private void PRIVMSG(String initiatorArg, String messageArg){ + String msg = messageArg.substring(messageArg.indexOf(":")+1); + if (!Pattern.matches("^\\u0001ACTION .+\\u0001", msg)){ + this.prettyPrint(genDate()+"<"+getUserNameOnly(initiatorArg)+"> "+msg+"\n"); + } + else { + this.prettyPrint(genDate()+getUserNameOnly(initiatorArg)+msg.replaceAll("(^\\u0001ACTION)|(\\u0001$)","")+"\n"); + } + } + private void JOIN(String initiatorArg, String messageArg){ + this.prettyPrint(genDate()+">> "+getUserNameAndHost(initiatorArg)+"joined "+messageArg+"\n"); + } + private void MODE(String initiatorArg, String messageArg){ + String initiatorChain; + if (initiatorArg.contains("!")) + initiatorChain = getUserNameAndHost(initiatorArg)+"set"; + else + initiatorChain = initiatorArg+" set"; + this.prettyPrint(genDate()+"-!- "+initiatorChain+messageArg.substring(messageArg.indexOf(" "))+"\n"); + } + private void KICK(String initiatorArg, String messageArg){ + this.prettyPrint(genDate()+"!<< "+messageArg.replaceAll("(^.+?\\s)|(\\s.+$)", "")+ + " kicked by "+getUserNameAndHost(initiatorArg)+"with reason: "+messageArg.replaceAll("^.+?:", "")+"\n"); + } + private void PART(String initiatorArg, String messageArg){ + this.prettyPrint(genDate()+"<< "+getUserNameAndHost(initiatorArg)+"parted: "+messageArg.replaceAll("^.+?:","")+"\n"); + } + private void QUIT(String initiatorArg, String messageArg){ + this.prettyPrint(genDate()+"<< "+getUserNameAndHost(initiatorArg)+" quit: "+messageArg.replaceAll("^.+?:","")+"\n"); + } + private void NICK(String initiatorArg, String messageArg){ + this.prettyPrint(genDate()+"-!- "+getUserNameAndHost(initiatorArg)+"changed nick to: "+messageArg+"\n"); + } + private void TOPIC(String initiatorArg, String messageArg) { + this.prettyPrint(genDate()+"-!- "+getUserNameAndHost(initiatorArg)+"has changed topic to: "+messageArg.replaceAll("^.+?:", "")+"\n"); + } +} diff --git a/src/InnaIrcBot/LogDriver/BotSQLiteWorker.java b/src/InnaIrcBot/LogDriver/BotSQLiteWorker.java new file mode 100644 index 0000000..a47a955 --- /dev/null +++ b/src/InnaIrcBot/LogDriver/BotSQLiteWorker.java @@ -0,0 +1,201 @@ +package InnaIrcBot.LogDriver; + +import org.sqlite.SQLiteConfig; +import org.sqlite.SQLiteOpenMode; + +import java.io.File; +import java.sql.*; + +public class BotSQLiteWorker implements Worker { + + private Connection connection; + private boolean consistent = false; + private PreparedStatement preparedStatement; +/** + * Don't even think of changing this balalaika. + * */ + public BotSQLiteWorker(String server, String[] driverParameters, String channel){ // TODO: threads on SQLite level // remember: One file one DB + driverParameters[0] = driverParameters[0].trim(); + File dir = new File(driverParameters[0]); + dir.mkdirs(); + if (!dir.exists()) { + System.out.println("Unable to create directory to store DB file: " + driverParameters[0]); //TODO: notify requester + this.consistent = false; + } + String connectionURL; + if (driverParameters[0].endsWith(File.separator)) + connectionURL = "jdbc:sqlite:"+driverParameters[0]+server+".db"; + else + connectionURL = "jdbc:sqlite:"+driverParameters[0]+File.separator+server+".db"; + + String safeChanName = channel.trim().replaceAll("\"","\\\""); // TODO: use trim in every driver/worker? + try { + SQLiteConfig sqlConfig = new SQLiteConfig(); + sqlConfig.setOpenMode(SQLiteOpenMode.NOMUTEX); //SQLITE_OPEN_NOMUTEX : multithreaded mode + + this.connection = DriverManager.getConnection(connectionURL, sqlConfig.toProperties()); + if (connection != null){ + // Create table if not created + Statement statement = connection.createStatement(); + String query = "CREATE TABLE IF NOT EXISTS \""+safeChanName+"\" (" + + " id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," + + " unixtime INTEGER," + + " event TEXT," + + " subject TEXT," + + " message TEXT," + + " object TEXT" + +");"; + statement.executeUpdate(query); + + // Check table representation + ResultSet rs = statement.executeQuery("PRAGMA table_info(\""+safeChanName+"\");"); // executeQuery never null + boolean[] schemaResultCheck = {false, false, false, false, false, false}; + + while (rs.next()) { + switch (rs.getInt("cid")) { + case 0: + if (rs.getString("name").equals("id") + && rs.getString("type").equals("INTEGER") + && (rs.getInt("notnull") == 1) + && (rs.getString("dflt_value") == null) + && (rs.getInt("pk") == 1)) + schemaResultCheck[0] = true; + //System.out.println("Got 0"); + break; + case 1: + if (rs.getString("name").equals("unixtime") + && rs.getString("type").equals("INTEGER") + && (rs.getInt("notnull") == 0) + && (rs.getString("dflt_value") == null) + && (rs.getInt("pk") == 0)) + schemaResultCheck[1] = true; + //System.out.println("Got 1"); + break; + case 2: + if (rs.getString("name").equals("event") + && rs.getString("type").equals("TEXT") + && (rs.getInt("notnull") == 0) + && (rs.getString("dflt_value") == null) + && (rs.getInt("pk") == 0)) + schemaResultCheck[2] = true; + //System.out.println("Got 2"); + break; + case 3: + if (rs.getString("name").equals("subject") + && rs.getString("type").equals("TEXT") + && (rs.getInt("notnull") == 0) + && (rs.getString("dflt_value") == null) + && (rs.getInt("pk") == 0)) + schemaResultCheck[3] = true; + //System.out.println("Got 3"); + break; + case 4: + if (rs.getString("name").equals("message") + && rs.getString("type").equals("TEXT") + && (rs.getInt("notnull") == 0) + && (rs.getString("dflt_value") == null) + && (rs.getInt("pk") == 0)) + schemaResultCheck[4] = true; + //System.out.println("Got 4"); + break; + case 5: + if (rs.getString("name").equals("object") + && rs.getString("type").equals("TEXT") + && (rs.getInt("notnull") == 0) + && (rs.getString("dflt_value") == null) + && (rs.getInt("pk") == 0)) + schemaResultCheck[5] = true; + //System.out.println("Got 5"); + break; + default: + for (int i = 0; i <= 5; i++) { + schemaResultCheck[i] = false; // If more then 5 elements, ruin results + } + } + } + // Validating result: it table in DB have expected schema. If not, removing and recreating table. + for (boolean element: schemaResultCheck) { + if (!element) { + System.out.println("BotSQLiteWorker: Found already existing table for channel with incoorect syntax: removing table and re-creating."); + statement.executeUpdate("DROP TABLE \"" + safeChanName + "\";"); + statement.executeUpdate(query); + break; + } + } + this.consistent = true; + + this.preparedStatement = connection.prepareStatement( + "INSERT INTO \""+safeChanName + +"\" (unixtime, event, subject, message, object) " + +"VALUES (?, ?, ?, ?, ?);"); + } + else { + this.consistent = false; + } + } + catch (SQLException e){ + System.out.println("Internal issue: BotSQLiteWorker->constructor() failed\n\t"+e); + this.consistent = false; + } + + } + private long getDate(){ + return System.currentTimeMillis() / 1000L; // UNIX time + } + @Override + public boolean isConsistent() {return consistent; } + + @Override + public void logAdd(String event, String initiatorArg, String messageArg) { + try { + preparedStatement.setLong(1, getDate()); + preparedStatement.setString(2, event); + preparedStatement.setString(3, initiatorArg); + switch (event) { + case "NICK": + case "JOIN": + preparedStatement.setString(4, messageArg); + preparedStatement.setString(5, null); + break; + case "PART": + case "QUIT": + case "TOPIC": + preparedStatement.setString(4, messageArg.replaceAll("^.+?:", "")); + preparedStatement.setString(5, null); + break; + case "MODE": + preparedStatement.setString(4, messageArg.replaceAll("(^(.+?\\s){1})|(\\s.+$)","")); + preparedStatement.setString(5, messageArg.replaceAll("^(.+?\\s){2}", "")); + break; + case "KICK": + preparedStatement.setString(4,messageArg.replaceAll("^.+?:", "")); + preparedStatement.setString(5,messageArg.replaceAll("(^.+?\\s)|(\\s.+$)", "")); + break; + case "PRIVMSG": + preparedStatement.setString(4,messageArg.replaceAll("^:", "")); + preparedStatement.setString(5,null); + break; + default: + preparedStatement.setString(4,messageArg); + preparedStatement.setString(5,null); + break; + } + preparedStatement.executeUpdate(); + } + catch (SQLException e){ + System.out.println("Internal issue: BotSQLiteWorker->logAdd() failed\n\t"+e); + this.consistent = false; + } + } + + @Override + public void close() { + try { + //System.out.println("SQLite drier closed"); + this.connection.close(); + } + catch (SQLException e){ + System.out.println("Internal issue: BotSQLiteWorker->close() failed\n\t" + e); + } + } +} \ No newline at end of file diff --git a/src/InnaIrcBot/LogDriver/BotZeroWorker.java b/src/InnaIrcBot/LogDriver/BotZeroWorker.java new file mode 100644 index 0000000..17237f4 --- /dev/null +++ b/src/InnaIrcBot/LogDriver/BotZeroWorker.java @@ -0,0 +1,12 @@ +package InnaIrcBot.LogDriver; + +public class BotZeroWorker implements Worker{ + @Override + public boolean isConsistent() {return true;} + + @Override + public void logAdd(String event, String initiatorArg, String messageArg) {} + + @Override + public void close() {} +} diff --git a/src/InnaIrcBot/LogDriver/Worker.java b/src/InnaIrcBot/LogDriver/Worker.java new file mode 100644 index 0000000..8f111a3 --- /dev/null +++ b/src/InnaIrcBot/LogDriver/Worker.java @@ -0,0 +1,13 @@ +package InnaIrcBot.LogDriver; + +public interface Worker { + boolean consistent = false; + + boolean isConsistent(); + + void logAdd(String event, + String initiatorArg, + String messageArg); + + void close(); +} diff --git a/src/InnaIrcBot/ProvidersConsumers/ChanConsumer.java b/src/InnaIrcBot/ProvidersConsumers/ChanConsumer.java new file mode 100644 index 0000000..8762110 --- /dev/null +++ b/src/InnaIrcBot/ProvidersConsumers/ChanConsumer.java @@ -0,0 +1,150 @@ +package InnaIrcBot.ProvidersConsumers; + +import InnaIrcBot.Commanders.ChanelCommander; +import InnaIrcBot.LogDriver.BotDriver; +import InnaIrcBot.LogDriver.Worker; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; + + +public class ChanConsumer implements Runnable { + private BufferedReader reader; + private String serverName; + private String channelName; + private Worker writerWorker; + private ArrayList userList; + private String nick; + private boolean rejoin; + private Map chanList; + private String configFilePath; + + private PrintWriter chanelCommanderPipe; + + private boolean endThread = false; + + ChanConsumer(BufferedReader streamReader, String serverName, String channelName, String ownNick, String[] usersOnChan, boolean rejoinAlways, Map map, String configFilePath){ + this.reader = streamReader; + this.serverName = serverName; + this.channelName = channelName; + this.writerWorker = BotDriver.getWorker(serverName, channelName); + this.userList = new ArrayList<>(); + this.userList.addAll(Arrays.asList(usersOnChan)); + this.nick = ownNick; + this.rejoin = rejoinAlways; + this.chanList = map; + this.configFilePath = configFilePath; + // Create chanel commander thread, get pipe + this.chanelCommanderPipe = getChanelCommander(); + } + + public void run(){ + String data; + String[] dataStrings; + System.out.println("["+LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"))+"] THREAD "+serverName+":"+this.channelName +" started"); // TODO:REMOVE DEBUG + try { + while ((data = reader.readLine()) != null) { + dataStrings = data.split(" ",3); + + if (!trackUsers(dataStrings[0], dataStrings[1], dataStrings[2])) + continue; + + writerWorker.logAdd(dataStrings[0], dataStrings[1], dataStrings[2]); + // Send to chanel commander thread + chanelCommanderPipe.println(data); + chanelCommanderPipe.flush(); + //System.out.println("|"+dataStrings[0]+"|"+dataStrings[1]+"|"+dataStrings[2]+"|"); + //System.out.println("Thread: "+this.channelName +"\n\tArray:"+ userList); + + if (endThread) { + reader.close(); + chanList.get(channelName).close(); + chanList.remove(channelName); + break; + } + } + } catch (java.io.IOException e){ + System.out.println("Internal issue: thread ChanConsumer->run() caused I/O exception:\n\t"+e); // TODO: reconnect + } + writerWorker.close(); + //Chanel commander thread's pipe should be closed + chanelCommanderPipe.close(); + System.out.println("["+LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"))+"] THREAD "+serverName+":"+this.channelName +" ended"); // TODO:REMOVE DEBUG + } + + private boolean trackUsers(String event, String initiatorArg, String subjectArg){ + switch (event) { + case "PRIVMSG": // most common, we don't have to handle anything else + return true; + case "JOIN": + addUsers(initiatorArg); + return true; + case "PART": + delUsers(initiatorArg); + return true; + case "QUIT": // TODO fix: use regex + if (userList.contains(initiatorArg.replaceAll("!.+$", ""))) { + delUsers(initiatorArg); + return true; + } else + return false; // user quit, but he/she is not in this channel + case "KICK": + if (rejoin && nick.equals(subjectArg.substring(subjectArg.indexOf(" ") + 1, subjectArg.indexOf(" :")))) + StreamProvider.writeToStream(serverName, "JOIN " + channelName); + delUsers(subjectArg.substring(subjectArg.indexOf(" ") + 1, subjectArg.indexOf(" :"))); + return true; + case "NICK": + if (userList.contains(initiatorArg.replaceAll("!.+$", ""))) { + swapUsers(initiatorArg, subjectArg); + return true; + } else { + return false; // user changed nick, but he/she is not in this channel + } + default: + return true; + } + } + + private void addUsers(String user){ + if (!userList.contains(user.replaceAll("!.+$", ""))) + userList.add(user.replaceAll("!.+$", "")); + } + private void delUsers(String user){ + if (user.replaceAll("!.+$", "").equals(nick)) { + endThread = true; + } + userList.remove(user.replaceAll("!.+$", "")); + } + private void swapUsers(String userNickOld, String userNickNew){ + userList.remove(userNickOld.replaceAll("!.+$", "")); + userList.add(userNickNew); + if (userNickOld.replaceAll("!.+$", "").equals(nick)) + this.nick = userNickNew; + } + // Create ChanelCommander + private PrintWriter getChanelCommander(){ + PipedOutputStream streamOut = new PipedOutputStream(); + try { + BufferedReader streamBufferedReader = new BufferedReader( + new InputStreamReader( + new PipedInputStream(streamOut), StandardCharsets.UTF_8) + ); + + ChanelCommander commander = new ChanelCommander(streamBufferedReader, serverName, channelName, configFilePath); + + new Thread(commander).start(); + + return new PrintWriter(streamOut); + + } catch (IOException e) { + System.out.println("Internal issue: ChanConsumer->getChanelCommander() I/O exception while initialized child objects."); // caused by Socket + endThread = true; + return null; + } + } +} diff --git a/src/InnaIrcBot/ProvidersConsumers/DataProvider.java b/src/InnaIrcBot/ProvidersConsumers/DataProvider.java new file mode 100644 index 0000000..49a0b9c --- /dev/null +++ b/src/InnaIrcBot/ProvidersConsumers/DataProvider.java @@ -0,0 +1,233 @@ +package InnaIrcBot.ProvidersConsumers; + +import InnaIrcBot.Config.StorageFile; +import InnaIrcBot.LogDriver.BotDriver; +import InnaIrcBot.ReconnectControl; + +import java.io.*; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + + +public class DataProvider implements Runnable { + private StorageFile configFile; + private String serverName; + private String userNick; + private BufferedReader rawStreamReader; + + private boolean ableToRun = true; + /** + * Initiate connection and prepare input/output streams for run() + * */ + public DataProvider(StorageFile storageFile){ + this.configFile = storageFile; + this.serverName = storageFile.getServerName(); + + int port = storageFile.getServerPort(); + + try { + InetAddress inetAddress = InetAddress.getByName(serverName); + Socket socket = new Socket(); // TODO: set timeout? + for (int i=0; i<5; i++) { + socket.connect(new InetSocketAddress(inetAddress, port), 5000); // 10sec = 10000 + if (socket.isConnected()) + break; + } + if (socket.isConnected()) + System.out.println("Socket connected"); + else { + System.out.println("Unable to connect to remote server after 5 retry."); + this.ableToRun = false; + return; + } + + if (!StreamProvider.setStream(serverName, socket)) { + this.ableToRun = false; + return; + } + + InputStream inStream = socket.getInputStream(); + InputStreamReader isr = new InputStreamReader(inStream, StandardCharsets.UTF_8); //TODO set charset in options; + this.rawStreamReader = new BufferedReader(isr); + } catch(UnknownHostException e){ + this.ableToRun = false; + System.out.println("Internal issue: DataProvider->constructor caused unknown host exception:\n\t"+e); // caused by InetAddress + } catch (IOException e){ + this.ableToRun = false; + System.out.println("Internal issue: DataProvider->constructor caused I/O exception\n\t"+e); // caused by Socket + } + } + //HANDLE ALWAYS in case thread decided to die + private void close(){ + StreamProvider.delStream(serverName); + } + + public void run(){ + if (!ableToRun) { + this.close(); + return; + } + + if(!this.initConnection(rawStreamReader)) { + this.close(); + return; + } + + //Prepare logDriver for using in threads. + if(!BotDriver.setFileDriver(serverName, configFile.getLogDriver(), configFile.getLogDriverParameters())) { + this.close(); + return; + } + + /* Used for sending data into consumers objects*/ + Map channelsMap = Collections.synchronizedMap(new HashMap()); + try { + PipedOutputStream streamOut = new PipedOutputStream(); // K-K-K-KOMBO \m/ + + BufferedReader streamBufferedReader = new BufferedReader( + new InputStreamReader( + new PipedInputStream(streamOut), StandardCharsets.UTF_8) + ); + + Runnable consumer = new SystemConsumer(streamBufferedReader, userNick, channelsMap, this.configFile); + new Thread(consumer).start(); + channelsMap.put("", new PrintWriter(streamOut)); // Not sure that PrintWriter is thread-safe.. + } catch (IOException e){ + System.out.println("Internal issue: DataProvider->run() I/O exception while initialized child objects.\n\t"+e); // caused by Socket + this.close(); + return; + } + ////////////////////////////////////// Start loop ////////////////////////////////////////////////////////////// + + try { + String rawMessage; + String[] rawStrings; // prefix[0] command[1] command-parameters\r\n[2] + //if there is no prefix, you should assume the message came from your client. + String chan; + // Say 'yes, we need reconnect if connection somehow died' + ReconnectControl.register(serverName); + + while ((rawMessage = rawStreamReader.readLine()) != null) { + //System.out.println(rawMessage); + if (rawMessage.startsWith(":")) { + rawStrings = rawMessage + .substring(1) + .split(" :?", 3); // Removing ':' + + + chan = rawStrings[2].replaceAll("(\\s.?$)|(\\s.+?$)", ""); + + //System.out.println("\tChannel: "+chan+"\n\tAction: "+rawStrings[1]+"\n\tSender: "+rawStrings[0]+"\n\tMessage: "+rawStrings[2]+"\n"); + + if (rawStrings[1].equals("QUIT") || rawStrings[1].equals("NICK")) { // replace regex + for (PrintWriter value : channelsMap.values()) { + value.println(rawStrings[1] + " " + rawStrings[0] + " " + rawStrings[2]); + value.flush(); + } + } else if (channelsMap.containsKey(chan)) { + channelsMap.get(chan).println(rawStrings[1] + " " + rawStrings[0] + " " + rawStrings[2]); + channelsMap.get(chan).flush(); + } else { + channelsMap.get("").println(rawStrings[1] + " " + rawStrings[0] + " " + rawStrings[2]); + channelsMap.get("").flush(); + } + } else if (rawMessage.startsWith("PING :")) { + pingSrvResponse(rawMessage); + } else { + System.out.println("Not a valid response=" + rawMessage); + } + } + } catch (IOException e){ + System.out.println("Socket issue: I/O exception"); //Connection closed. TODO: MAYBE try reconnect + } + finally { + for (PrintWriter p :channelsMap.values()) { + p.close(); + } + this.close(); + } + } + + private void pingSrvResponse(String rawData){ + StreamProvider.writeToStream(serverName,"PONG :" + rawData.replace("PING :", "")); + } + /** + * Initiate connection before starting main routine. + * */ + private boolean initConnection(BufferedReader genericStreamReader){ + int nickTail = 0; // handle appendix to nickname + + this.userNick = configFile.getUserNick(); + + if (this.userNick == null || this.userNick.isEmpty()) { + System.out.println("Configuration issue: no nickname specified."); + return false; + } + String rawMessage; + + StreamProvider.writeToStream(serverName,"NICK "+this.userNick); + StreamProvider.writeToStream(serverName,"USER "+configFile.getUserIdent()+" 8 * :"+configFile.getUserRealName()); // TODO: Add usermode 4 rusnet + + if (!configFile.getUserNickPass().isEmpty() && (!configFile.getUserNickAuthStyle().isEmpty() && configFile.getUserNickAuthStyle().toLowerCase().equals("freenode"))) + StreamProvider.writeToStream(serverName,"PRIVMSG NickServ :IDENTIFY "+configFile.getUserNickPass()); + else if (!configFile.getUserNickPass().isEmpty() && (!configFile.getUserNickAuthStyle().isEmpty() && configFile.getUserNickAuthStyle().toLowerCase().equals("rusnet"))) + StreamProvider.writeToStream(serverName,"NickServ IDENTIFY "+configFile.getUserNickPass()); + else if (!configFile.getUserNickPass().isEmpty()) + System.out.println("Configuration issue: Unable to determinate method of user nick identification (by password): could be \"freenode\" or \"rusnet\"\nSee \"userNickAuthStyle\"."); + + try { + // 431 ERR_NONICKNAMEGIVEN how can we get this? + // 432 ERR_ERRONEUSNICKNAME covered + // 433 ERR_NICKNAMEINUSE covered + // 436 ERR_NICKCOLLISION + while ((rawMessage = genericStreamReader.readLine()) != null){ + System.out.println(rawMessage); + if (rawMessage.startsWith("PING :")) { + pingSrvResponse(rawMessage); + } + if (rawMessage.contains(" 001 ")) { + + StringBuilder message = new StringBuilder(); + + if (!configFile.getUserMode().trim().isEmpty()){ + String modes = configFile.getUserMode(); + modes = modes.replaceAll("[\t\\s]", ""); + + for(char c :modes.toCharArray()) { + message.append("MODE "); + message.append(userNick); + message.append(" +"); + message.append(c); + message.append("\n"); + } + } + StreamProvider.writeToStream(serverName,message.toString()); + + return true; + } + else if (rawMessage.contains(" 433 ")) { + if (this.userNick.substring(userNick.length()-1, userNick.length()-1).equals("|")) + this.userNick = this.userNick.substring(0,userNick.length()-1)+Integer.toString(nickTail++); + else + this.userNick = this.userNick+"|"+Integer.toString(nickTail++); + + StreamProvider.writeToStream(serverName,"NICK "+this.userNick); + } + else if (rawMessage.contains(" 432 ")) { + System.out.println("Configuration issue: Nickname contains unacceptable characters (432 ERR_ERRONEUSNICKNAME)."); + return false; + } + } + } catch (IOException e){ + System.out.println("Internal issue: DataProvider->initConnection() caused I/O exception."); + return false; + } + return false; + } +} diff --git a/src/InnaIrcBot/ProvidersConsumers/StreamProvider.java b/src/InnaIrcBot/ProvidersConsumers/StreamProvider.java new file mode 100644 index 0000000..15cc83a --- /dev/null +++ b/src/InnaIrcBot/ProvidersConsumers/StreamProvider.java @@ -0,0 +1,34 @@ +package InnaIrcBot.ProvidersConsumers; + +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.Socket; +import java.util.HashMap; + +public class StreamProvider { + + private static HashMap srvStreamMap = new HashMap<>(); + //private static OutputStreamWriter streamWriter; + + public static synchronized void writeToStream(String server, String message){ + try { + srvStreamMap.get(server).write(message+"\n"); + srvStreamMap.get(server).flush(); + } catch (java.io.IOException e){ + System.out.println("Internal issue: StreamProvider->writeToStream() caused I/O exception."); + } + } + public static synchronized boolean setStream(String server, Socket socket){ + try { + OutputStream outStream = socket.getOutputStream(); + srvStreamMap.put(server, new OutputStreamWriter(outStream)); + return true; + } catch (java.io.IOException e){ + System.out.println("Internal issue: StreamProvider->setStream() caused I/O exception."); + return false; + } + } + public static synchronized void delStream(String server){ + srvStreamMap.remove(server); + } +} diff --git a/src/InnaIrcBot/ProvidersConsumers/SystemConsumer.java b/src/InnaIrcBot/ProvidersConsumers/SystemConsumer.java new file mode 100644 index 0000000..9249b56 --- /dev/null +++ b/src/InnaIrcBot/ProvidersConsumers/SystemConsumer.java @@ -0,0 +1,206 @@ +package InnaIrcBot.ProvidersConsumers; + +import InnaIrcBot.Commanders.PrivateMsgCommander; +import InnaIrcBot.Config.StorageFile; +import InnaIrcBot.GlobalData; +import InnaIrcBot.LogDriver.BotDriver; +import InnaIrcBot.LogDriver.Worker; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.time.LocalTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Pattern; + +public class SystemConsumer implements Runnable{ + private BufferedReader reader; + private Worker writerWorker; + private String nick; + private String serverName; + private Map channelsMap; + private boolean proxyRequired; + private HashMap> proxyAList; + private StorageFile storageFile; + + private PrivateMsgCommander commander; + + SystemConsumer(BufferedReader streamReader, String userNick, Map map, StorageFile storage) { + this.writerWorker = BotDriver.getWorker(storage.getServerName(), "system"); + this.nick = userNick; + this.serverName = storage.getServerName(); + this.channelsMap = map; + this.reader = streamReader; + + this.proxyRequired = false; + this.proxyAList = new HashMap<>(); + this.storageFile = storage; + + this.commander = new PrivateMsgCommander(serverName, storageFile.getBotAdministratorPassword()); + // Start pre-set channels + StringBuilder message = new StringBuilder(); + for (String cnl : storageFile.getChannels()) { // TODO: add validation of channels. + message.append("JOIN "); + message.append(cnl); + message.append("\n"); + } + StreamProvider.writeToStream(serverName,message.toString()); + } + + @Override + public void run() { + System.out.println("["+LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"))+"] THREAD "+serverName+":[system] started"); // TODO:REMOVE DEBUG + + setMainRoutine(); + + for (PrintWriter p :channelsMap.values()) //TODO: check, code duplication. see Data provider constructor + p.close(); + + writerWorker.close(); + System.out.println("["+LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"))+"] THREAD "+serverName+":[system] ended"); // TODO:REMOVE DEBUG + } + + private void setMainRoutine(){ + String data; + String dataStrings[]; + try { + while ((data = reader.readLine()) != null) { + dataStrings = data.split(" ",3); + + if (proxyRequired) + if (getProxy(dataStrings[0], dataStrings[1], dataStrings[2])) + continue; + + if (dataStrings[0].equals("PRIVMSG") && dataStrings[2].indexOf("\u0001") < dataStrings[2].lastIndexOf("\u0001")) + replyCTCP(dataStrings[1], dataStrings[2].substring(dataStrings[2].indexOf(":")+1)); + else if (Pattern.matches("(^[0-9]{3}$)|(^NICK$)|(^JOIN$)", dataStrings[0])){ + handleNumeric(dataStrings[0], dataStrings[1], dataStrings[2]); + } + else if (dataStrings[0].equals("PRIVMSG")) { + commander.receiver(dataStrings[1], dataStrings[2].replaceAll("^.+?:", "").trim()); + writerWorker.logAdd("[system]", "PRIVMSG sent to", "commander"); + } + else + writerWorker.logAdd(dataStrings[0], dataStrings[1], dataStrings[2]); // TODO: Track users + //System.out.println("System: "+"|"+dataStrings[0]+"|"+dataStrings[1]+"|"+dataStrings[2]+"|"); + } + } catch (java.io.IOException e){ + System.out.println("Internal issue: thread SystemConsumer->run() caused I/O exception."); // TODO: reconnect + } + } + private boolean getProxy(String eventNum, String sender, String message){ //TODO: if can't join: like channel with password + if (eventNum.equals("353")) + return false; // never mind and let it flows as usual. + else { + String chan = message.replaceAll("(\\s.?$)|(\\s.+?$)", ""); + + if (eventNum.equals("QUIT") || eventNum.equals("NICK")) { + for (ArrayList key : proxyAList.values()) + key.add(eventNum + " " + sender + " " + message); + return false; + } else if (chan.equals(nick)) + return false; + else if (proxyAList.keySet().contains(chan)) { + proxyAList.get(chan).add(eventNum + " " + sender + " " + message); + return true; + } else + return false; + } + } + + private void replyCTCP(String sender, String message){ + if (message.equals("\u0001VERSION\u0001")){ + StreamProvider.writeToStream(serverName,"NOTICE "+simplifyNick(sender)+" :\u0001VERSION "+ GlobalData.getAppVersion()+"\u0001"); + writerWorker.logAdd("[system]", "catch/handled CTCP VERSION from", simplifyNick(sender)); + System.out.println(sender+" "+message); + System.out.println("NOTICE "+simplifyNick(sender)+" \u0001VERSION "+ GlobalData.getAppVersion()+"\u0001"); + } + else if (message.startsWith("\u0001PING ") && message.endsWith("\u0001")){ + StreamProvider.writeToStream(serverName,"NOTICE "+simplifyNick(sender)+" :"+message); + writerWorker.logAdd("[system]", "catch/handled CTCP PING from", simplifyNick(sender)); + //System.out.println(":"+simplifyNick(sender)+" NOTICE "+sender.substring(0,sender.indexOf("!"))+" "+message); + } + else if (message.equals("\u0001CLIENTINFO\u0001")){ + StreamProvider.writeToStream(serverName,"NOTICE "+simplifyNick(sender)+" :\u0001CLIENTINFO ACTION PING VERSION TIME CLIENTINFO\u0001"); + writerWorker.logAdd("[system]", "catch/handled CTCP CLIENTINFO from", simplifyNick(sender)); + //System.out.println(":"+simplifyNick(sender)+" NOTICE "+sender.substring(0,sender.indexOf("!"))+" \u0001CLIENTINFO ACTION PING VERSION TIME CLIENTINFO\u0001"); + } + else if (message.equals("\u0001TIME\u0001")){ + StreamProvider.writeToStream(serverName,"NOTICE "+simplifyNick(sender)+" :\u0001TIME "+ ZonedDateTime.now().format(DateTimeFormatter.RFC_1123_DATE_TIME)+"\u0001"); + writerWorker.logAdd("[system]", "catch/handled CTCP TIME from", simplifyNick(sender)); + //System.out.println(":"+simplifyNick(sender)+" NOTICE "+sender.substring(0,sender.indexOf("!"))+" \u0001TIME "+ ZonedDateTime.now().format(DateTimeFormatter.RFC_1123_DATE_TIME)+"\u0001"); + } else + writerWorker.logAdd("[system]", "catch CTCP request \""+message+"\" from ", simplifyNick(sender)); + } + private String simplifyNick(String nick){ return nick.replaceAll("!.*$",""); } + + //todo: nandle nickserv messages + + private void handleNumeric(String eventNum, String sender, String message){ + switch (eventNum){ + case "433": // TODO: try to use alternative nickname + writerWorker.logAdd("[system]", "catch/handled:", eventNum+" [nickname already in use]"); + break; + case "353": + String chan = message.substring(message.indexOf(" ")+3); + chan = chan.substring(0, chan.indexOf(" ")); + if (proxyAList.containsKey(chan)) { + String userOnChanStr = message.substring(message.indexOf(":") + 1); + userOnChanStr = userOnChanStr.replaceAll("[%@+]", "").trim(); + String[] usersOnChanArr = userOnChanStr.split(" "); + + PipedOutputStream streamOut = new PipedOutputStream(); + try { + BufferedReader streamBufferedReader = new BufferedReader( + new InputStreamReader( + new PipedInputStream(streamOut), StandardCharsets.UTF_8) + ); + + channelsMap.put(chan, new PrintWriter(streamOut)); + // % @ + + ChanConsumer consumer = new ChanConsumer(streamBufferedReader, storageFile.getServerName(), chan, nick, usersOnChanArr, storageFile.getRejoinOnKick(), channelsMap, storageFile.getChanelConfigurationsPath()); + new Thread(consumer).start(); + + + for (String msgStored : proxyAList.get(chan)) { + channelsMap.get(chan).println(msgStored); + channelsMap.get(chan).flush(); + } + + proxyAList.remove(chan); + if (proxyAList.isEmpty()) { + proxyRequired = false; + } + } catch (IOException e) { + System.out.println("Internal issue: SystemConsumer->handleNumeric() @ JOIN: I/O exception while initialized child objects."); // caused by Socket + return; //TODO: QA + } + } + else + System.out.println("Some internal shit happens that shouldn't happens never ever. Take your cat, call scientists and wait for singularity. Panic allowed."); + break; + case "NICK": + if (sender.startsWith(nick+"!")) { + String oldNick = nick; + nick = message.trim(); + + writerWorker.logAdd("[system]", "catch/handled own NICK change from:", sender+" to: "+message); + } + break; + case "JOIN": + if (sender.startsWith(nick+"!")) { + proxyAList.put(message, new ArrayList<>()); // Add new channel name to proxy watch-list + proxyAList.get(message).add(eventNum+" "+sender+" "+message); // Add message to array linked + this.proxyRequired = true; // Ask for proxy validators + } + break; + default: + writerWorker.logAdd("[system]", "catch: "+eventNum+" from: "+sender+" :",message); + break; + } + } +} \ No newline at end of file diff --git a/src/InnaIrcBot/README.md b/src/InnaIrcBot/README.md new file mode 100644 index 0000000..d54cf27 --- /dev/null +++ b/src/InnaIrcBot/README.md @@ -0,0 +1,43 @@ +# InnaIrcBot + +Another one IRC bot in deep beta. + +##Usage +` -c, --configuration [ ...] Read Config` + +` -g, --generate [name.config] Generate Config` + +` -v, --version Get application version` +####Configuration settings +"userNickAuthStyle": "rusnet" or "freenode" +* rusnet - send '/nickserv IDENTIFY mySecretPass' +* freenode - send '/msg nickserv IDENTIFY mySecretPass' + +"logDriver" could be "Files", "SQLite" or "Zero" +* Files - log everything to files using /yourPathSet/serverName/#chanelName_YYYY-MM-DD.txt format. +* SQLite - use /yourPathSet/server.db (or /yourPathSet/yourFileName.db) sqlite file. + +##License +Source code spreads under the GNU General Public License v3 or higher. Please see LICENSE file. + +Used libraries: +* GSON: https://github.com/google/gson +* sqliteJDBC: https://bitbucket.org/xerial/sqlite-jdbc + + +## TODO: +- [ ] Documentation +- [ ] Code refactoring +- [ ] QA: good regression testing +- [ ] CI/CD +- [ ] Suppress messages from server or handle them separately from selected worker +- [ ] Logs backend workers as threads (SQLite and co. are too slow) +- [ ] Logs backend worker for mongodb +- [ ] Logs backend worker for redis/redis node +- [ ] Re-implement connection routine +- [ ] Availability to run scripts @ 'ChanelCommander' +- [ ] Docker(+compose) package +- [ ] Flood tracker +- [ ] Deep configuration files validation +- [ ] Maven or Gradle build +- [ ] ncurses-like or/and GUI configuration files (server/chanel setting) editor diff --git a/src/InnaIrcBot/ReconnectControl.java b/src/InnaIrcBot/ReconnectControl.java new file mode 100644 index 0000000..b4ea5ce --- /dev/null +++ b/src/InnaIrcBot/ReconnectControl.java @@ -0,0 +1,23 @@ +package InnaIrcBot; + +import java.util.HashMap; + +public class ReconnectControl { + private static HashMap serversList = new HashMap<>(); + public static synchronized void register(String serverName){ + serversList.put(serverName, true); + } + public static synchronized void update(String serverName, boolean needReconnect){ + serversList.replace(serverName, needReconnect); + } + public static synchronized boolean get(String serverName){ // could be null and it should be considered as false + if (serversList.get(serverName) == null) + return false; + else + return serversList.get(serverName); + } + public static synchronized void delete(String serverName){ + serversList.remove(serverName); + } + +} diff --git a/src/InnaIrcBot/lib/gson-2.8.5.jar b/src/InnaIrcBot/lib/gson-2.8.5.jar new file mode 100644 index 0000000..0d5baf3 Binary files /dev/null and b/src/InnaIrcBot/lib/gson-2.8.5.jar differ diff --git a/src/InnaIrcBot/lib/sqlite-jdbc-3.23.1.jar b/src/InnaIrcBot/lib/sqlite-jdbc-3.23.1.jar new file mode 100644 index 0000000..ce81d03 Binary files /dev/null and b/src/InnaIrcBot/lib/sqlite-jdbc-3.23.1.jar differ diff --git a/src/META-INF/MANIFEST.MF b/src/META-INF/MANIFEST.MF new file mode 100644 index 0000000..fc38ee6 --- /dev/null +++ b/src/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: InnaIrcBot.BotStart + diff --git a/src/test/DriverTest.java b/src/test/DriverTest.java new file mode 100644 index 0000000..13b5dc5 --- /dev/null +++ b/src/test/DriverTest.java @@ -0,0 +1,37 @@ +import InnaIrcBot.LogDriver.BotDriver; +import InnaIrcBot.LogDriver.Worker; + +public class DriverTest { + + public static void main(String[] args){ + if (BotDriver.setFileDriver("irc.tomsk.net", "SQLiteDriver", new String[]{"/tmp/"})) + System.out.println("Successful driver initiation"); + else { + System.out.println("Failed driver initiation"); + return; + } + + Worker fw1 = BotDriver.getWorker("irc.tomsk.net","#lpr"); + Worker fw2 = BotDriver.getWorker("irc.tomsk.net","#main"); + Worker fw3 = BotDriver.getWorker("irc.tomsk.net","##loper"); + + if ((fw1 !=null) && (fw2 !=null) && (fw3 !=null)){ + System.out.println("LogFile1: "+fw1.isConsistent()); + System.out.println("LogFile2: "+fw2.isConsistent()); + System.out.println("LogFile3: "+fw3.isConsistent()); + + fw1.logAdd("JOIN", "de_su!loper@desktop.lan", "message1"); + fw1.logAdd("PART", "de_su!loper@desktop.lan", "#chan1"); + + fw2.logAdd("JOIN", "de_su!loper@desktop.lan", "message2"); + fw2.logAdd("PART", "de_su!loper@desktop.lan", "#chan2"); + + fw3.logAdd("JOIN", "de_su!loper@desktop.lan", "message3"); + fw3.logAdd("PART", "de_su!loper@desktop.lan", "#chan3"); + + fw1.close(); + fw2.close(); + fw3.close(); + } + } +} diff --git a/src/test/ReconnectControlTest.java b/src/test/ReconnectControlTest.java new file mode 100644 index 0000000..ef05cf6 --- /dev/null +++ b/src/test/ReconnectControlTest.java @@ -0,0 +1,14 @@ +import InnaIrcBot.ReconnectControl; +import com.sun.org.apache.regexp.internal.RE; + +public class ReconnectControlTest { + public static void main(String args[]){ + ReconnectControl.register("testing"); + ReconnectControl.register("testing1"); + ReconnectControl.update("testing1", false); + + System.out.println(ReconnectControl.get("testing")); + System.out.println(ReconnectControl.get("testing1")); + System.out.println(ReconnectControl.get("wrong")); + } +} diff --git a/src/test/StorageFileTest.java b/src/test/StorageFileTest.java new file mode 100644 index 0000000..89330a4 --- /dev/null +++ b/src/test/StorageFileTest.java @@ -0,0 +1,27 @@ +import InnaIrcBot.Config.StorageFile; + +public class StorageFileTest { + static public void main(String[] args){ + StorageFile config = new StorageFile( + "", + 0, + null, + "", + "", + "", + "", + "", + "", + true, + "", + new String[]{null}, + "", + "" + ); + + System.out.println(config.getLogDriver().isEmpty()); + System.out.println(config.getLogDriverParameters().length); + + + } +} diff --git a/src/test/StorageReaderTest.java b/src/test/StorageReaderTest.java new file mode 100644 index 0000000..bcc22d1 --- /dev/null +++ b/src/test/StorageReaderTest.java @@ -0,0 +1,10 @@ +import InnaIrcBot.Config.StorageFile; +import InnaIrcBot.Config.StorageReader; + +public class StorageReaderTest { + public static void main(String[] args){ + // StorageReader.readConfig("/home/loper/bot.config"); + // StorageFile storageFile = StorageReader.getConfig(); + // System.out.println(storageFile.getLogDriver() == null); + } +} diff --git a/src/test/brokenJoinFloodHandlerTest.java b/src/test/brokenJoinFloodHandlerTest.java new file mode 100644 index 0000000..78a5777 --- /dev/null +++ b/src/test/brokenJoinFloodHandlerTest.java @@ -0,0 +1,26 @@ +import InnaIrcBot.Commanders.JoinFloodHandler; + +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; + +public class brokenJoinFloodHandlerTest { + + public static void main(String[] args){ + new brokenJoinFloodHandlerTest(); + JoinFloodHandler jdh = new JoinFloodHandler(5, 5, "SRV_NAME", "#CHAN_NAME"); + System.out.println("Envents:\t5\n" + +"Time Frame:\t5 sec\n" + +"ServerName:\tSRV_NAME\n" + +"Chanel:\t#CHAN_NAME\n"); + for (int i=0; i<40; i++) { + System.out.println("Join for two users happened @"+ LocalDateTime.now()); + jdh.track("eblan"); + jdh.track("eban'ko"); + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +}