]> jfr.im git - irc/rizon/bncbot.git/commitdiff
Initial code drop.
authorN Lum <redacted>
Thu, 30 Jul 2009 22:32:53 +0000 (22:32 +0000)
committerN Lum <redacted>
Thu, 30 Jul 2009 22:32:53 +0000 (22:32 +0000)
--HG--
extra : convert_revision : svn%3Aafccd9b4-69c3-4f67-9d0d-5b27f80d3d7c/trunk%402

.classpath [new file with mode: 0644]
.project [new file with mode: 0644]
mysql-connector-java-5.0.8-bin.jar [new file with mode: 0644]
mysql-connector-java-5.0.8-doc.jar [new file with mode: 0644]
src/net/rizon/DatabaseConnection.java [new file with mode: 0644]
src/net/rizon/TrustingSSLSocketFactory.java [new file with mode: 0644]
src/net/rizon/bncbot/BncBot.java [new file with mode: 0644]
src/net/rizon/bncbot/UserDB.java [new file with mode: 0644]

diff --git a/.classpath b/.classpath
new file mode 100644 (file)
index 0000000..4a42b93
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>\r
+<classpath>\r
+       <classpathentry kind="src" path="src"/>\r
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>\r
+       <classpathentry kind="lib" path="mysql-connector-java-5.0.8-bin.jar">\r
+               <attributes>\r
+                       <attribute name="javadoc_location" value="jar:platform:/resource/RizonHelpBot/mysql-connector-java-5.0.8-doc.jar!/"/>\r
+               </attributes>\r
+       </classpathentry>\r
+       <classpathentry kind="output" path="bin"/>\r
+</classpath>\r
diff --git a/.project b/.project
new file mode 100644 (file)
index 0000000..5874c5e
--- /dev/null
+++ b/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>\r
+<projectDescription>\r
+       <name>BncBot</name>\r
+       <comment></comment>\r
+       <projects>\r
+       </projects>\r
+       <buildSpec>\r
+               <buildCommand>\r
+                       <name>org.eclipse.jdt.core.javabuilder</name>\r
+                       <arguments>\r
+                       </arguments>\r
+               </buildCommand>\r
+       </buildSpec>\r
+       <natures>\r
+               <nature>org.eclipse.jdt.core.javanature</nature>\r
+       </natures>\r
+</projectDescription>\r
diff --git a/mysql-connector-java-5.0.8-bin.jar b/mysql-connector-java-5.0.8-bin.jar
new file mode 100644 (file)
index 0000000..0170c3e
Binary files /dev/null and b/mysql-connector-java-5.0.8-bin.jar differ
diff --git a/mysql-connector-java-5.0.8-doc.jar b/mysql-connector-java-5.0.8-doc.jar
new file mode 100644 (file)
index 0000000..5fedbbf
Binary files /dev/null and b/mysql-connector-java-5.0.8-doc.jar differ
diff --git a/src/net/rizon/DatabaseConnection.java b/src/net/rizon/DatabaseConnection.java
new file mode 100644 (file)
index 0000000..9bfb9c7
--- /dev/null
@@ -0,0 +1,62 @@
+package net.rizon;\r
+\r
+import java.sql.Connection;\r
+import java.sql.DriverManager;\r
+import java.sql.SQLException;\r
+import java.util.Properties;\r
+\r
+public class DatabaseConnection {\r
+       private static Connection con = null;\r
+       private static Properties props = null;\r
+\r
+       public static Connection getConnection() {\r
+               if (props == null) throw new RuntimeException("DatabaseConnection not initialized");\r
+               \r
+               try {\r
+                       if((con != null) && con.isValid(2)) {\r
+                               return con;\r
+                       } else { // Attempt reconnect.\r
+                               if (con != null) con.close();\r
+                               \r
+                               DatabaseConnection.connect();\r
+                               \r
+                               if(con == null) {\r
+                                       throw new RuntimeException("MySQL server has gone away. Attempted reconnection failed.");\r
+                               }\r
+                               \r
+                               return con;\r
+                       }\r
+               }\r
+               catch (SQLException e) {\r
+                       e.printStackTrace();\r
+               }\r
+               return con;\r
+       }\r
+       \r
+       public static boolean isInitialized() {\r
+               return props != null;\r
+       }\r
+       \r
+       public static void setProps(Properties aProps) {\r
+               props = aProps;\r
+       }\r
+       \r
+       private static void connect() {\r
+               String driver = props.getProperty("sqlDriver");\r
+               String url = props.getProperty("sqlUrl");\r
+               String user = props.getProperty("sqlUser");\r
+               String password = props.getProperty("sqlPass");\r
+               \r
+               try {\r
+                       Class.forName(driver);\r
+               } catch (ClassNotFoundException e) {\r
+                       e.printStackTrace();\r
+               }\r
+               try {\r
+                       con = DriverManager.getConnection(url, user, password);\r
+               } catch (SQLException e) {\r
+                       e.printStackTrace();\r
+                       throw new RuntimeException("Error connecting to MySQL: " + e.getMessage());\r
+               }\r
+       }\r
+}\r
diff --git a/src/net/rizon/TrustingSSLSocketFactory.java b/src/net/rizon/TrustingSSLSocketFactory.java
new file mode 100644 (file)
index 0000000..6eef282
--- /dev/null
@@ -0,0 +1,163 @@
+package net.rizon;\r
+\r
+import java.io.IOException;\r
+import java.net.InetAddress;\r
+import java.net.Socket;\r
+import java.net.UnknownHostException;\r
+import java.security.KeyManagementException;\r
+import java.security.NoSuchAlgorithmException;\r
+import java.security.Security;\r
+import java.security.cert.CertificateException;\r
+import java.security.cert.X509Certificate;\r
+\r
+import javax.net.ssl.SSLContext;\r
+import javax.net.ssl.SSLException;\r
+import javax.net.ssl.SSLSocket;\r
+import javax.net.ssl.SSLSocketFactory;\r
+import javax.net.ssl.TrustManager;\r
+import javax.net.ssl.X509TrustManager;\r
+\r
+/**\r
+ * An SSLSocketFactory implementation that treats all certificates as valid. Use with care.\r
+ */\r
+public class TrustingSSLSocketFactory extends SSLSocketFactory {\r
+\r
+    private SSLSocketFactory factory;\r
+    private String[] ciphers;\r
+\r
+    /**\r
+     * Create a new SSLSocketFactory factory that will create Sockets regardless of what certificate\r
+     * is used.\r
+     * @throws SSLException if it cannot initialize correctly.\r
+     */\r
+    public TrustingSSLSocketFactory() throws SSLException {\r
+        Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider());\r
+        System.setProperty("java.protocol.handler.pkgs", "com.sun.net.ssl.internal.www.protocol");\r
+        try {\r
+            SSLContext sslContext;\r
+            sslContext = SSLContext.getInstance("SSLv3");\r
+            sslContext.init(null, new TrustManager[] { new TrustingX509TrustManager() }, null);\r
+            factory = sslContext.getSocketFactory();\r
+        } catch (NoSuchAlgorithmException nsae) {\r
+            throw new SSLException("Unable to initialize the SSL context:  ", nsae);\r
+        } catch (KeyManagementException kme) {\r
+            throw new SSLException("Unable to register a trust manager:  ", kme);\r
+        }\r
+        ciphers = factory.getDefaultCipherSuites();\r
+    }\r
+\r
+    /*\r
+     * (non-Javadoc)\r
+     * @see javax.net.ssl.SSLSocketFactory#getDefaultCipherSuites()\r
+     */\r
+    @Override\r
+    public String[] getDefaultCipherSuites() {\r
+        return ciphers;\r
+    }\r
+\r
+    /*\r
+     * (non-Javadoc)\r
+     * @see javax.net.ssl.SSLSocketFactory#getSupportedCipherSuites()\r
+     */\r
+    @Override\r
+    public String[] getSupportedCipherSuites() {\r
+        return ciphers;\r
+    }\r
+\r
+    /*\r
+     * (non-Javadoc)\r
+     * @see javax.net.SocketFactory#createSocket(java.lang.String, int)\r
+     */\r
+    @Override\r
+    public Socket createSocket(String host, int port) throws IOException, UnknownHostException {\r
+        return prepare((SSLSocket)factory.createSocket(host, port));\r
+    }\r
+\r
+    /*\r
+     * (non-Javadoc)\r
+     * @see javax.net.SocketFactory#createSocket(java.lang.String, int, java.net.InetAddress, int)\r
+     */\r
+    @Override\r
+    public Socket createSocket(String host, int port, InetAddress localHost, int localPort)\r
+            throws IOException, UnknownHostException {\r
+        return prepare((SSLSocket)factory.createSocket(host, port, localHost, localPort));\r
+    }\r
+\r
+    /*\r
+     * (non-Javadoc)\r
+     * @see javax.net.SocketFactory#createSocket(java.net.InetAddress, int)\r
+     */\r
+    @Override\r
+    public Socket createSocket(InetAddress address, int port) throws IOException {\r
+        return prepare((SSLSocket)factory.createSocket(address, port));\r
+    }\r
+\r
+    /*\r
+     * (non-Javadoc)\r
+     * @see javax.net.SocketFactory#createSocket(java.net.InetAddress, int, java.net.InetAddress,\r
+     *      int)\r
+     */\r
+    @Override\r
+    public Socket createSocket(InetAddress address, int port, InetAddress localAddress,\r
+            int localPort) throws IOException {\r
+        return prepare((SSLSocket)factory.createSocket(address, port, localAddress, localPort));\r
+    }\r
+\r
+    /*\r
+     * (non-Javadoc)\r
+     * @see javax.net.ssl.SSLSocketFactory#createSocket(java.net.Socket, java.lang.String, int,\r
+     *      boolean)\r
+     */\r
+    @Override\r
+    public Socket createSocket(Socket s, String host, int port, boolean autoClose)\r
+            throws IOException {\r
+        return prepare((SSLSocket)factory.createSocket(s, host, port, autoClose));\r
+    }\r
+\r
+    /**\r
+     * Setup the socket with ciphers. Add code here to do things to all created Sockets.\r
+     * @param baseSocket\r
+     * @return &lt;code&gt;baseSocket&lt;/code&gt; all set up.\r
+     */\r
+    private SSLSocket prepare(SSLSocket baseSocket) {\r
+        baseSocket.setEnabledCipherSuites(ciphers);\r
+        return baseSocket;\r
+    }\r
+\r
+    /**\r
+     * Trusts everyone. Very insecure.\r
+     */\r
+    private class TrustingX509TrustManager implements X509TrustManager {\r
+\r
+        /*\r
+         * (non-Javadoc)\r
+         * @see javax.net.ssl.X509TrustManager#checkClientTrusted(java.security.cert.X509Certificate[],\r
+         *      java.lang.String)\r
+         */\r
+        public void checkClientTrusted(X509Certificate[] arg0, String arg1)\r
+                throws CertificateException {\r
+            // no Exception implies acceptance\r
+            return;\r
+        }\r
+\r
+        /*\r
+         * (non-Javadoc)\r
+         * @see javax.net.ssl.X509TrustManager#checkServerTrusted(java.security.cert.X509Certificate[],\r
+         *      java.lang.String)\r
+         */\r
+        public void checkServerTrusted(X509Certificate[] arg0, String arg1)\r
+                throws CertificateException {\r
+            // no Exception implies acceptance\r
+            return;\r
+        }\r
+\r
+        /*\r
+         * (non-Javadoc)\r
+         * @see javax.net.ssl.X509TrustManager#getAcceptedIssuers()\r
+         */\r
+        public X509Certificate[] getAcceptedIssuers() {\r
+            return new X509Certificate[0];\r
+        }\r
+\r
+    }\r
+}\r
diff --git a/src/net/rizon/bncbot/BncBot.java b/src/net/rizon/bncbot/BncBot.java
new file mode 100644 (file)
index 0000000..e8dc865
--- /dev/null
@@ -0,0 +1,320 @@
+package net.rizon.bncbot;\r
+\r
+import java.io.FileInputStream;\r
+import java.io.FileNotFoundException;\r
+import java.io.IOException;\r
+import java.io.PrintWriter;\r
+import java.net.Socket;\r
+import java.text.DateFormat;\r
+import java.text.SimpleDateFormat;\r
+import java.util.Date;\r
+import java.util.Hashtable;\r
+import java.util.Properties;\r
+import java.util.Scanner;\r
+import java.util.regex.Matcher;\r
+import java.util.regex.Pattern;\r
+\r
+import net.rizon.TrustingSSLSocketFactory;\r
+import net.rizon.DatabaseConnection;\r
+\r
+public class BncBot {\r
+       public static PrintWriter netOut;\r
+       public static String channel;\r
+       public static String adminChannel;\r
+       public static String whyChannel;\r
+\r
+       public static UserDB database;\r
+       public static Hashtable<String, String> whyLookup = new Hashtable<String, String>();\r
+       public static Hashtable<String, Date> regTimeLookup = new Hashtable<String, Date>();\r
+\r
+       public static final boolean debug = true;\r
+       public static boolean running = true;\r
+\r
+       public final static Properties config = new Properties();\r
+\r
+       // Entry point.\r
+       public static void main(String[] args) {\r
+               try {\r
+                       FileInputStream fis = new FileInputStream("bncbot.properties");\r
+                       config.load(fis);\r
+                       fis.close();\r
+\r
+                       DatabaseConnection.setProps(config);\r
+                       botLoop();\r
+               } catch (FileNotFoundException e) {\r
+                       System.out.println("Error loading config: " + e.getMessage());\r
+                       e.printStackTrace();\r
+               } catch (IOException e) {\r
+                       System.out.println("Error loading config: " + e.getMessage());\r
+                       e.printStackTrace();\r
+               }\r
+       }\r
+\r
+       // Main bot loop.\r
+       public static void botLoop() {\r
+               try {\r
+                       BncBot.database = new UserDB();\r
+\r
+                       Socket socket = new TrustingSSLSocketFactory().createSocket(config.getProperty("server"), Integer.parseInt(config.getProperty("port")));\r
+                       netOut = new PrintWriter(socket.getOutputStream(), true);\r
+\r
+                       // Send stuff at beginning.\r
+                       if (!config.getProperty("pass").equals(""))\r
+                               BncBot.send("PASS :" + config.getProperty("pass"));\r
+                       BncBot.send("NICK " + config.getProperty("nick"));\r
+                       BncBot.send("USER " + config.getProperty("ident") + " 1 * :" + config.getProperty("gecos"));\r
+                       if (!config.getProperty("nickservPass").equals(""))\r
+                               BncBot.privmsg("NickServ", "IDENTIFY " + config.getProperty("nickservPass"));\r
+\r
+                       // Set up database save loop.\r
+                       Thread dbSaveDaemon = new DatabaseDaemon();\r
+                       dbSaveDaemon.start();\r
+\r
+                       // Set up stdin.\r
+                       new StdinLoop().start();\r
+\r
+                       // Set up main input loop.\r
+                       Scanner scan = new Scanner(socket.getInputStream());\r
+                       String line, words[], userlookup = null;\r
+                       Pattern ccStripper = Pattern.compile("[\u0002\u001F\u000F]");\r
+                       Pattern colorStripper = Pattern.compile(" \u0003[0-9]{1,2}(,[0-9]{1,2})?");\r
+                       Pattern userIs = Pattern.compile("([A-Z0-9`_^|\\[\\]]*) is .*", Pattern.CASE_INSENSITIVE);\r
+                       Pattern regPattern = Pattern.compile("Time registered: +(.*)$", Pattern.CASE_INSENSITIVE);\r
+                       DateFormat dateParser = (DateFormat) new SimpleDateFormat("MMM dd hh:mm:ss yyyy z");\r
+\r
+                       while (running && scan.hasNextLine()) {\r
+                               line = scan.nextLine();\r
+                               line = ccStripper.matcher(line).replaceAll("");\r
+                               line = colorStripper.matcher(line).replaceAll("");\r
+\r
+                               if (BncBot.debug)\r
+                                       System.out.println("<- " + line);\r
+\r
+                               words = line.split("\\s+");\r
+\r
+                               // Handle ping messages.\r
+                               if (line.startsWith("PING ") && words.length > 1)\r
+                                       BncBot.send("PONG " + words[1]);\r
+\r
+                               // Look for end of MOTD.\r
+                               if (words[1].equals("376")) {\r
+                                       Thread.sleep(500); // Sleep to allow NickServ to process our IDENT.\r
+                                       BncBot.send("JOIN " + config.getProperty("adminChannel"));\r
+                                       BncBot.send("JOIN " + config.getProperty("whyChannel"));\r
+                               }\r
+\r
+                               // Look for INFO responses (requires special treatment.)\r
+                               if (words[0].split("!")[0].equals(":NickServ")) {\r
+                                       String message = "";\r
+                                       for (int i = 3; i < words.length; i++)\r
+                                               message += ((i == 3) ? words[i].substring(1) : words[i]) + " ";\r
+                                       message = message.trim();\r
+\r
+                                       Matcher ui = userIs.matcher(message), rp = regPattern.matcher(message);\r
+                                       if (ui.matches() && ui.groupCount() == 1) {\r
+                                               userlookup = ui.group(1);\r
+                                       } else if (rp.matches() && rp.groupCount() == 1 && userlookup != null) {\r
+                                               synchronized (BncBot.regTimeLookup) {\r
+                                                       BncBot.regTimeLookup.put(userlookup, dateParser.parse(rp.group(1)));\r
+                                               }\r
+                                               userlookup = null;\r
+                                       }\r
+                               }\r
+\r
+                               if (words[1].equals("PRIVMSG") || words[1].equals("NOTICE")) {\r
+                                       new MessageProc(words).start();\r
+                               }\r
+                       }\r
+\r
+                       // Save database.\r
+                       dbSaveDaemon.interrupt();\r
+                       database.saveData(true);\r
+\r
+                       // Send quit.\r
+                       BncBot.send("QUIT :Ping timeout (9001 seconds)");\r
+                       netOut.close();\r
+                       socket.close();\r
+               } catch (Exception e) {\r
+                       e.printStackTrace();\r
+               }\r
+       }\r
+\r
+       // Split message into parts of 400 bytes.\r
+       public static void privmsg(String target, String msg) {\r
+               if (msg.length() > 400) {\r
+                       for (int i = 400; i > 0; i--) {\r
+                               if (msg.substring(i, i + 1).matches("\\s")) {\r
+                                       doPrivmsg(target, msg.substring(0, i));\r
+                                       privmsg(target, msg.substring(i));\r
+                                       return;\r
+                               }\r
+                       }\r
+               } else {\r
+                       doPrivmsg(target, msg);\r
+               }\r
+       }\r
+\r
+       private static void doPrivmsg(String target, String line) {\r
+               BncBot.send("PRIVMSG " + target + " :" + line);\r
+       }\r
+\r
+       public static void notice(String target, String msg) {\r
+               if (msg.length() > 400) {\r
+                       for (int i = 400; i > 0; i--) {\r
+                               if (msg.substring(i, i + 1).matches("\\s")) {\r
+                                       doNotice(target, msg.substring(0, i));\r
+                                       privmsg(target, msg.substring(i));\r
+                                       return;\r
+                               }\r
+                       }\r
+               } else {\r
+                       doNotice(target, msg);\r
+               }\r
+       }\r
+\r
+       private static void doNotice(String target, String line) {\r
+               BncBot.send("NOTICE " + target + " :" + line);\r
+       }\r
+\r
+       private static void send(String line) {\r
+               if (BncBot.debug)\r
+                       System.out.println("-> " + line);\r
+               netOut.println(line);\r
+       }\r
+\r
+       private static class DatabaseDaemon extends Thread {\r
+               public void run() {\r
+                       try {\r
+                               Thread.sleep(300000); // Sleep for 5 minutes.\r
+                               BncBot.database.saveData();\r
+                       } catch (InterruptedException e) {\r
+                               return; // Whatever.\r
+                       }\r
+               }\r
+       }\r
+\r
+       private static class StdinLoop extends Thread {\r
+               public void run() {\r
+                       Scanner scan = new Scanner(System.in);\r
+                       String str;\r
+                       while (((str = scan.nextLine()) != "") && running) {\r
+                               send(str);\r
+                       }\r
+               }\r
+       }\r
+}\r
+\r
+class MessageProc extends Thread {\r
+       private String[] tokens;\r
+       private String message;\r
+       private static final Pattern whyPattern = Pattern.compile("([A-Z0-9`_^|\\[\\]]*) has [A-Z]* access to #[A-Z]*. Reason: .*\\. Main nick: (.*)", Pattern.CASE_INSENSITIVE);\r
+\r
+       public MessageProc(String[] words) {\r
+               this.tokens = words.clone();\r
+\r
+               this.message = "";\r
+               for (int i = 3; i < tokens.length; i++)\r
+                       this.message += ((i == 3) ? tokens[i].substring(1) : tokens[i]) + " ";\r
+               this.message = this.message.trim();\r
+       }\r
+\r
+       public void run() {\r
+               if (tokens[2].equalsIgnoreCase(BncBot.config.getProperty("adminChannel")) && tokens.length >= 5) { // Process messages from the admin channel.\r
+                       if (tokens[3].equalsIgnoreCase(":.bncaccept")) {\r
+\r
+                       } else if (tokens[3].equalsIgnoreCase(":.bncreject")) {\r
+\r
+                       }\r
+               } else if (tokens[2].equalsIgnoreCase(BncBot.config.getProperty("adminChannel")) && tokens.length == 4) { // Process commands w/o args.\r
+                       if (tokens[3].equalsIgnoreCase(":.shutdownbot")) {\r
+                               BncBot.running = false;\r
+                       }\r
+               } else if (tokens[0].substring(1).split("!")[0].equalsIgnoreCase("ChanServ")) { // Process WHY responses.\r
+                       Matcher m = whyPattern.matcher(message);\r
+                       if (m.matches() && m.groupCount() == 2) {\r
+                               synchronized (BncBot.whyLookup) {\r
+                                       BncBot.whyLookup.put(m.group(1).trim(), m.group(2).trim());\r
+                               }\r
+                       }\r
+               } else if (tokens[1].equals("PRIVMSG") && tokens[3].equalsIgnoreCase(":request")) { // Process requests.\r
+                       String user = tokens[0].substring(1);\r
+                       String nick = user.split("!")[0];\r
+                       String hostmask = user.split("!")[1];\r
+\r
+                       String mainNick = lookupNick(nick);\r
+                       Date regTime = lookupRegTime(mainNick);\r
+                       long timeDifference = System.currentTimeMillis() - regTime.getTime();\r
+                       int daysDifference = (int) (timeDifference / 86400000); // Divide by one day in millis.\r
+\r
+                       BncBot.privmsg(nick, "Your main nick is: " + mainNick);\r
+                       BncBot.privmsg(nick, "You registered on: " + regTime.toString());\r
+                       BncBot.privmsg(nick, "You registered " + daysDifference + " ago.");\r
+                       \r
+                       if(daysDifference > 7) {\r
+                               UserDB.UserEntry ue = BncBot.database.findUser(mainNick);\r
+                               if(ue != null) {\r
+                                       if(ue.getApprovalState() == UserDB.UserEntry.ApprovalState.Approved) { \r
+                                               BncBot.privmsg(nick, String.format("You already have an approved BNC. It was approved by %s (%s) at %s.", ue.getApprovedByNick(),\r
+                                                       ue.getApprovedByHostmask(), ue.getTimeApproved()));\r
+                                       } else if (ue.getApprovalState() == UserDB.UserEntry.ApprovalState.Rejected){\r
+                                               BncBot.privmsg(nick, String.format("Your previous BNC request has been rejected. The reason given was: %s", ue.getRejectionReason()));\r
+                                               BncBot.privmsg(nick, BncBot.config.getProperty("rejectionAppeal"));\r
+                                       } else {\r
+                                               BncBot.privmsg(nick, "You already have a pending BNC request. Be patient as a staff member reviews your request. You will receive a memo when your request has been processed.");\r
+                                       }\r
+                               } else {\r
+                                       int id = BncBot.database.addUser(mainNick, hostmask);\r
+                                       if(id < 0) {\r
+                                               BncBot.privmsg(nick, "An error occurred while processing your request.");\r
+                                       } else {\r
+                                               BncBot.privmsg(nick, "Your request has been entered into the system and is now pending review by a staff member.");\r
+                                               BncBot.privmsg(nick, "Note that this process has no defined length, and may take anywhere from a couple hours to a couple days. You will receive a memo once your request has been processed.");\r
+                                       \r
+                                               BncBot.privmsg(BncBot.config.getProperty("adminChannel"), String.format("Nick '%s' (Main: %s; Hostmask: %s) has submitted a BNC request. The ID of this request is %s.", nick,\r
+                                                       mainNick, hostmask, id));\r
+                                               BncBot.privmsg(BncBot.config.getProperty("adminChannel"), "To accept the request, type '.bncaccept' followed by either the main nick or request ID. To reject, type '.bncreject' followed by the main nick or ID, then a reason.");\r
+                                               \r
+                                       }\r
+                               }\r
+                       } else {\r
+                               BncBot.privmsg(nick, "The main nick of your group must be registered for at least 7 days before requesting a BNC.");\r
+                       }\r
+               }\r
+       }\r
+\r
+       private synchronized static String lookupNick(String nick) {\r
+               try {\r
+                       BncBot.privmsg("ChanServ", "ACCESS " + BncBot.config.getProperty("whyChannel") + " ADD " + nick + " 3");\r
+                       BncBot.privmsg("ChanServ", "WHY " + BncBot.config.getProperty("whyChannel") + " " + nick);\r
+                       BncBot.privmsg("ChanServ", "ACCESS " + BncBot.config.getProperty("whyChannel") + " DEL " + nick);\r
+\r
+                       long time = System.currentTimeMillis();\r
+                       while (!BncBot.whyLookup.containsKey(nick) && ((time + 5000) > System.currentTimeMillis())) { // While we don't have a result. Time out after 5 seconds.\r
+                               Thread.sleep(100);\r
+                       }\r
+\r
+                       synchronized (BncBot.whyLookup) {\r
+                               return BncBot.whyLookup.remove(nick);\r
+                       }\r
+               } catch (Exception e) {\r
+                       return null;\r
+               }\r
+       }\r
+\r
+       private synchronized static Date lookupRegTime(String nick) {\r
+               try {\r
+                       BncBot.privmsg("NickServ", "INFO " + nick);\r
+\r
+                       long time = System.currentTimeMillis();\r
+                       while (!BncBot.regTimeLookup.containsKey(nick) && ((time + 5000) > System.currentTimeMillis())) { // While we don't have a result. Time out after 5 seconds.\r
+                               Thread.sleep(100);\r
+                       }\r
+\r
+                       synchronized (BncBot.regTimeLookup) {\r
+                               return BncBot.regTimeLookup.remove(nick);\r
+                       }\r
+               } catch (Exception e) {\r
+                       return null;\r
+               }\r
+       }\r
+}
\ No newline at end of file
diff --git a/src/net/rizon/bncbot/UserDB.java b/src/net/rizon/bncbot/UserDB.java
new file mode 100644 (file)
index 0000000..de23b10
--- /dev/null
@@ -0,0 +1,309 @@
+package net.rizon.bncbot;\r
+\r
+import java.sql.Connection;\r
+import java.sql.PreparedStatement;\r
+import java.sql.ResultSet;\r
+import java.sql.SQLException;\r
+import java.sql.Statement;\r
+import java.sql.Timestamp;\r
+import java.util.Date;\r
+import java.util.Enumeration;\r
+import java.util.Hashtable;\r
+\r
+import net.rizon.DatabaseConnection;\r
+\r
+public class UserDB {\r
+       private Hashtable<Integer, UserEntry> entries = new Hashtable<Integer, UserEntry>();\r
+\r
+       public UserDB() {\r
+               Connection conn = DatabaseConnection.getConnection();\r
+\r
+               // Try to load from the DB.\r
+               try {\r
+                       PreparedStatement ps = conn.prepareStatement("SELECT * FROM `bncbot_users`;");\r
+                       ResultSet rs = ps.executeQuery();\r
+\r
+                       while (rs.next()) {\r
+                               entries.put(rs.getInt("bnc_user_id"), UserEntry.fromResultSet(rs));\r
+                       }\r
+\r
+                       rs.close();\r
+                       ps.close();\r
+               } catch (SQLException e) {\r
+                       e.printStackTrace();\r
+               }\r
+       }\r
+\r
+       public int addUser(String nick, String hostmask) {\r
+               Connection conn = DatabaseConnection.getConnection();\r
+\r
+               try {\r
+                       PreparedStatement ps = conn.prepareStatement("INSERT INTO `bncbot_users` (`bnc_user_nick`,`bnc_user_hostmask`) VALUES (?,?);", Statement.RETURN_GENERATED_KEYS);\r
+\r
+                       ps.setString(1, nick);\r
+                       ps.setString(2, hostmask);\r
+                       ps.execute();\r
+\r
+                       ResultSet rs = ps.getGeneratedKeys();\r
+                       if (!rs.next()) {\r
+                               System.out.println("Error adding user: MySQL did not return a AUTO_INCREMENT key.");\r
+                               return -1;\r
+                       }\r
+\r
+                       synchronized (this.entries) {\r
+                               this.entries.put(rs.getInt(1), new UserEntry(rs.getInt(1), nick, hostmask));\r
+                       }\r
+\r
+                       return rs.getInt(1);\r
+               } catch (Exception e) {\r
+                       e.printStackTrace();\r
+                       return -1;\r
+               }\r
+       }\r
+\r
+       public UserEntry findUser(String nick) {\r
+               synchronized (this.entries) {\r
+                       for (Enumeration<Integer> iter = this.entries.keys(); iter.hasMoreElements();) {\r
+                               Integer key = iter.nextElement();\r
+                               UserEntry ue = this.entries.get(key);\r
+\r
+                               if (ue.getNick().equalsIgnoreCase(nick)) {\r
+                                       return ue;\r
+                               }\r
+                       }\r
+               }\r
+\r
+               return null;\r
+       }\r
+\r
+       public UserEntry findUser(int userID) {\r
+               synchronized (this.entries) {\r
+                       for (Enumeration<Integer> iter = this.entries.keys(); iter.hasMoreElements();) {\r
+                               Integer key = iter.nextElement();\r
+\r
+                               if (key == userID) {\r
+                                       return this.entries.get(key);\r
+                               }\r
+                       }\r
+               }\r
+\r
+               return null;\r
+       }\r
+\r
+       public boolean approveUser(String nick, String approvalUser) {\r
+               return approveUser(findUser(nick), approvalUser);\r
+       }\r
+\r
+       public boolean approveUser(int userID, String approvalUser) {\r
+               return approveUser(findUser(userID), approvalUser);\r
+       }\r
+\r
+       public boolean approveUser(UserEntry ue, String aUser) {\r
+               if (ue == null)\r
+                       return false;\r
+\r
+               ue.approve(aUser);\r
+               return true;\r
+       }\r
+\r
+       public boolean rejectUser(String nick, String rejectionUser, String rejectionReason) {\r
+               return rejectUser(findUser(nick), rejectionUser, rejectionReason);\r
+       }\r
+\r
+       public boolean rejectUser(int userID, String rejectionUser, String rejectionReason) {\r
+               return rejectUser(findUser(userID), rejectionUser, rejectionReason);\r
+       }\r
+\r
+       public boolean rejectUser(UserEntry ue, String rUser, String rReason) {\r
+               if (ue == null)\r
+                       return false;\r
+\r
+               ue.reject(rUser, rReason);\r
+               return true;\r
+       }\r
+\r
+       public void saveData(boolean now) {\r
+               String query = "REPLACE " + (now ? "" : "DELAYED ") + "INTO `bncbot_users` VALUES (?,?,?,?,?,?,?,?,?);";\r
+\r
+               Connection conn = DatabaseConnection.getConnection();\r
+               try {\r
+                       PreparedStatement ps = conn.prepareStatement(query);\r
+\r
+                       synchronized (this.entries) {\r
+                               for (Enumeration<Integer> iter = this.entries.keys(); iter.hasMoreElements();) {\r
+                                       Integer key = iter.nextElement();\r
+                                       UserEntry ue = this.entries.get(key);\r
+\r
+                                       if (ue.isModified()) {\r
+                                               ps.setInt(1, key);\r
+                                               ps.setString(2, ue.getNick());\r
+                                               ps.setString(3, ue.getHostmask());\r
+                                               ps.setTimestamp(4, new Timestamp(ue.getTimeRequested().getTime()));\r
+                                               ps.setInt(5, ue.getApprovalState().getAsInt());\r
+                                               ps.setString(6, ue.getApprovedByNick());\r
+                                               ps.setString(7, ue.getApprovedByHostmask());\r
+                                               ps.setTimestamp(8, (ue.getApprovalState() == UserEntry.ApprovalState.Approved) ? new Timestamp(ue.getTimeApproved().getTime()) : null);\r
+                                               ps.setString(9, ue.getRejectionReason());\r
+\r
+                                               ps.execute();\r
+                                               ue.setNotModified();\r
+                                       }\r
+                               }\r
+                       }\r
+               } catch (Exception e) {\r
+                       e.printStackTrace();\r
+               }\r
+       }\r
+\r
+       public void saveData() {\r
+               saveData(false);\r
+       }\r
+\r
+       public static class UserEntry {\r
+               private int id;\r
+               private String nick;\r
+               private String hostmask;\r
+               private Date timeRequested;\r
+\r
+               private ApprovalState approved;\r
+               private String approvedBy;\r
+               private String approvedByHostmask;\r
+               private Date approvedTime;\r
+\r
+               private String rejectedReason;\r
+\r
+               // Flag to tell if the entry is modified\r
+               // and must be flushed to DB.\r
+               private boolean modified;\r
+\r
+               public UserEntry(int id, String nick, String hostmask, Date timeRequested) {\r
+                       this.id = id;\r
+                       this.nick = nick;\r
+                       this.hostmask = hostmask;\r
+                       this.timeRequested = timeRequested;\r
+\r
+                       this.approved = ApprovalState.NotProcessed;\r
+                       this.approvedBy = null;\r
+                       this.approvedByHostmask = null;\r
+                       this.approvedTime = null;\r
+\r
+                       this.rejectedReason = null;\r
+\r
+                       this.modified = true;\r
+               }\r
+\r
+               public UserEntry(int id, String nick, String hostmask) {\r
+                       this(id, nick, hostmask, new Date());\r
+               }\r
+\r
+               public int getId() {\r
+                       return this.id;\r
+               }\r
+\r
+               public String getNick() {\r
+                       return this.nick;\r
+               }\r
+\r
+               public String getHostmask() {\r
+                       return this.hostmask;\r
+               }\r
+\r
+               public Date getTimeRequested() {\r
+                       return this.timeRequested;\r
+               }\r
+\r
+               public ApprovalState getApprovalState() {\r
+                       return this.approved;\r
+               }\r
+\r
+               public String getApprovedByNick() {\r
+                       return (this.approved == ApprovalState.Approved) ? this.approvedBy : null;\r
+               }\r
+\r
+               public String getApprovedByHostmask() {\r
+                       return (this.approved == ApprovalState.Approved) ? this.approvedByHostmask : null;\r
+               }\r
+\r
+               public Date getTimeApproved() {\r
+                       return (this.approved == ApprovalState.Approved) ? this.approvedTime : null;\r
+               }\r
+\r
+               public String getRejectionReason() {\r
+                       return (this.approved == ApprovalState.Rejected) ? this.rejectedReason : null;\r
+               }\r
+\r
+               public void approve(String user) {\r
+                       this.approved = ApprovalState.Approved;\r
+                       this.approvedBy = user.split("!")[0];\r
+                       this.approvedByHostmask = user;\r
+                       this.approvedTime = new Date();\r
+\r
+                       this.modified = true;\r
+               }\r
+\r
+               public void reject(String user, String reason) {\r
+                       this.approved = ApprovalState.Rejected;\r
+                       this.approvedBy = user.split("!")[0];\r
+                       this.approvedByHostmask = user;\r
+                       this.approvedTime = new Date();\r
+                       this.rejectedReason = reason;\r
+\r
+                       this.modified = true;\r
+               }\r
+\r
+               public boolean isModified() {\r
+                       return this.modified;\r
+               }\r
+\r
+               public void setNotModified() {\r
+                       this.modified = false;\r
+               }\r
+\r
+               public static UserEntry fromResultSet(ResultSet rs) {\r
+                       try {\r
+                               UserEntry ue = new UserEntry(rs.getInt("bnc_user_id"), rs.getString("bnc_user_nick"), rs.getString("bnc_user_hostmask"), new Date(rs.getTimestamp("bnc_time_requested").getTime()));\r
+\r
+                               ue.approved = ApprovalState.valueOf(rs.getInt("bnc_approved"));\r
+                               ue.approvedBy = rs.getString("bnc_approved_by");\r
+                               ue.approvedByHostmask = rs.getString("bnc_approved_by_hostmask");\r
+                               ue.approvedTime = new Date(rs.getTimestamp("bnc_approved_time").getTime());\r
+\r
+                               ue.rejectedReason = rs.getString("bnc_rejected_reason");\r
+\r
+                               ue.modified = false;\r
+\r
+                               return ue;\r
+                       } catch (Exception e) {\r
+                               return null;\r
+                       }\r
+               }\r
+\r
+               public enum ApprovalState {\r
+                       NotProcessed {\r
+                               public int getAsInt() {\r
+                                       return 0;\r
+                               }\r
+                       },\r
+                       Approved {\r
+                               public int getAsInt() {\r
+                                       return 1;\r
+                               }\r
+                       },\r
+                       Rejected {\r
+                               public int getAsInt() {\r
+                                       return 2;\r
+                               }\r
+                       };\r
+\r
+                       public abstract int getAsInt();\r
+\r
+                       public static ApprovalState valueOf(int value) {\r
+                               for (ApprovalState as : ApprovalState.values()) {\r
+                                       if (as.getAsInt() == value)\r
+                                               return as;\r
+                               }\r
+                               throw new AssertionError("Invalid approval state.");\r
+                       }\r
+               }\r
+       }\r
+}\r