diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9c98000
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+#IntelliJ IDEA gitignore list
+.idea/*
+*.ipr
+*.iws
+out/*
+.idea_modules/*
+atlassian-ide-plugin.xml
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+logs/*
+*.cfg
+module_otg-bot.xml
+otg-bot.properties
+otg-bot.xml
\ No newline at end of file
diff --git a/CHANGELOG b/CHANGELOG
new file mode 100644
index 0000000..925599c
--- /dev/null
+++ b/CHANGELOG
@@ -0,0 +1,2 @@
+(29.09.2015) v0.0.0 - Initial commit
+(06.10.2015) v1.0.0 - Initial commit to github
\ No newline at end of file
diff --git a/OTG-Bot.iml b/OTG-Bot.iml
new file mode 100644
index 0000000..e746d45
--- /dev/null
+++ b/OTG-Bot.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PluginSettings.example b/PluginSettings.example
new file mode 100644
index 0000000..10cc702
--- /dev/null
+++ b/PluginSettings.example
@@ -0,0 +1,4 @@
+#Cbox Bot settings file. Please don't touch unless you know specifically what to change, else you might cause a crash.
+#Tue Oct 06 10:28:36 UTC 2015
+LastSeen=
+StreamerList=zingmars\:0\\0
diff --git a/README.MD b/README.MD
new file mode 100644
index 0000000..15c08ac
--- /dev/null
+++ b/README.MD
@@ -0,0 +1,100 @@
+Cbox.ws Bot in Kotlin
+====
+
+# This is a very overengineered "simple" bot that...
+
+1) Connects to a specified cbox.ws chat box and reads/logs messages
+
+2) Allows moderator-only commands and that kind of stuff
+
+3) Provides a CLI (trough, say a telnet client) access that allows quick reconfiguring and reloading if needed (admittedly)
+
+4) Has a plugin API that lets you inject any compiled kotlin class (technically at any time too) and run it, as long as it inherits the base plugin class and functions
+
+5) Does it in a multi-threaded fashion
+
+And probably much more...
+
+-----
+# Why?
+
+When looking around the webs I found this language, Kotlin. Now, I have a project coming up that involves building an Android app, and not being on good terms with Java myself, I decided to build something simple to understand it's basics (this is why you'll find stuff like classloader, socket server etc. in this project). I've also wanted to build something that connects to a chat box that a community I frequent uses; I've tried making a desktop client before before, but I didn't finish it out of laziness. It should however in theory work with any cbox.ws based chatbox, although I haven't tested it, so I can't vouch for it.
+
+-----
+# Setup guide (using IntelliJ IDEA):
+
+1) Open File->Settings->Plugins, look for Kotlin, download and install it
+
+2) Open project's folder using the Open function
+
+3) On the Project view open the src directory and open the app.kt file
+
+4) There should now be a bar asking your to set up Project SDK. If you haven't already, point it to your jdk's directory.
+
+5) I have included kotlin's runtimes with the project, so you should now be able to make and run this project.
+
+------
+# TL;DR - Files
+
+/src:
+
+ /BotPlugins - contains plugins for the bot
+
+ /Containers - some cross communication model classes
+
+app.kt - main entry point for this application. Loads and manages every other class
+
+The rest should be obvious - ZipUtil handles file zipping (for log file archiving), CLI handles command line input, Logger Logs, HTTP handles HTTP connections etc.
+
+------
+# Plugins
+
+Just throw them into /src/BotPlugins. All plugins must follow this base example:
+
+```
+package BotPlugins
+import Containers.PluginBufferItem
+
+public class PluginName : BasePlugin()
+{
+ override fun pubInit() :Boolean
+ {
+ return true
+ }
+ override fun connector(buffer : PluginBufferItem) :Boolean
+ {
+ return true
+ }
+ override fun stop()
+ {
+ }
+}
+```
+
+There are couple of notes though:
+
+1. Plugins are using a really weird ClassLoader implementation, so it it's a bit buggy. The reason for this is that I wanted to see if dynamic class loading is possible with Java, and although it is, I don't really think it's worth the hassle I went through.
+
+2. Plugin name MUST be the same as the filename. If it's not, it won't be loaded. The files in BotPlugins folder can be empty however, but they need to have a compiled version.
+
+3. Yes, it must be a class.
+
+4. pubInit() is the class that's executed once it has been given proper context (namely - logger, a settings file, threadmanager etc). More info on that is available as a comment on BasePlugin.kt
+
+5. connector() is called for every message
+
+6. stop() is called when the plugin is turned off. This is for when you have threads or something like that running.
+
+7. Even though you can use pretty much the whole logger class, it would be saner to use plugin related log commands. The boring one's for when the loglevel is over 2 and it's there to avoid spamming the log with pointless stuff.
+
+8. Yes, you can reload your class while it's running (using CLI, or if you make it to - through an user command), but you'll need to replace the compiled version in the jar file. It works when run from an IDE, not so much when you have a portable jar file.
+
+------
+# TODO?
+
+A full refactor to make the code consistent would be nice. Plenty of TODO's scattered through the code as well. Even so, this is project is, for all intents and purposes, finished. Feel free to fork and do whatever.
+
+------
+# License
+
+Please view LICENSE (BSD 2-clause). TL;DR - Do what you want, just include the original license and don't blame me when something breaks
\ No newline at end of file
diff --git a/kotlin-reflect.jar b/kotlin-reflect.jar
new file mode 100644
index 0000000..3911770
Binary files /dev/null and b/kotlin-reflect.jar differ
diff --git a/kotlin-runtime-sources.jar b/kotlin-runtime-sources.jar
new file mode 100644
index 0000000..8b733a6
Binary files /dev/null and b/kotlin-runtime-sources.jar differ
diff --git a/kotlin-runtime.jar b/kotlin-runtime.jar
new file mode 100644
index 0000000..5f44f94
Binary files /dev/null and b/kotlin-runtime.jar differ
diff --git a/settings.example b/settings.example
new file mode 100644
index 0000000..2e2c262
--- /dev/null
+++ b/settings.example
@@ -0,0 +1,25 @@
+#OTG Bot settings file. Please don't touch unless you know specifically what to change, else you might cause a crash.
+#Sat Oct 03 18:15:13 MSK 2015
+daemonPort=9970
+boxtag=0
+fileLog=true
+server=0
+chatLogFile=chat.txt
+logChat=true
+logFile=log.txt
+username=zingmars
+boxid=0
+logFolder=logs
+password=0
+daemonEnabled=true
+isOriginal=False
+maxLogs=100
+consoleLog=true
+avatar=
+refreshRate=4000
+logLevel=1
+enablePlugins=true
+pluginLogFile=plugins.txt
+pluginDirectory=src/BotPlugins
+archiveLogs=true
+logRotate=true
\ No newline at end of file
diff --git a/src/BasePlugin.kt b/src/BasePlugin.kt
new file mode 100644
index 0000000..a816c05
--- /dev/null
+++ b/src/BasePlugin.kt
@@ -0,0 +1,48 @@
+/**
+ * Base plugin class to be inherited from
+ * Created by zingmars on 04.10.2015.
+ */
+package BotPlugins
+import Containers.PluginBufferItem
+import Settings
+import Logger
+import Plugins
+import ThreadController
+
+open public class BasePlugin() {
+ public var settings :Settings? = null
+ public var logger :Logger? = null
+ public var handher :Plugins? = null
+ public var pluginName :String? = null
+ public var controller :ThreadController? = null
+
+ init {
+ // This is the base class for all CBot plugins
+ // To make a plugin just extend this class and put it in src/BotPlugins/ (or whatever is defined in your settings file) directory
+ // Note - your filename must match your class name and it must be inside the BotPlugins package
+ // To initiate just override pubInit() (you can override this initializer too, but you won't have access to any variables at that point), and to do your logic just override connector.
+ // Note that your connector override will need to have the PluginBufferItem input for it to receive any messages.
+ // To send data back just add an element to any of the buffers available in ThreadController, (i.e. BoxBuffer will output anything send to it)
+ // Note that pubInit and connector both return a boolean value that indicates whether or not the plugin was successful
+ }
+ //non-overridable classes
+ final public fun initiate(settings :Settings, logger: Logger, pluginsHandler :Plugins, controller :ThreadController,pluginName :String) :Boolean
+ {
+ this.settings = settings
+ this.logger = logger
+ this.handher = pluginsHandler
+ this.pluginName = pluginName
+ this.controller = controller
+ if(this.pubInit()) this.logger?.LogPlugin(pluginName, "Started!")
+ else {
+ this.stop()
+ this.logger?.LogPlugin(pluginName, "failed to load!")
+ return false
+ }
+ return true
+ }
+ //overridable classes
+ open public fun pubInit() :Boolean { return true } //Initializer
+ open public fun connector(buffer :PluginBufferItem) :Boolean { return true } //Receives data from Plugin controller
+ open public fun stop() {} //This is run when the plugin is unloaded
+}
\ No newline at end of file
diff --git a/src/BotPlugins/AdminCommands.kt b/src/BotPlugins/AdminCommands.kt
new file mode 100644
index 0000000..d81347e
--- /dev/null
+++ b/src/BotPlugins/AdminCommands.kt
@@ -0,0 +1,96 @@
+/**
+ * Checks chat for user written messages and responds to specific queries
+ * Created by zingmars on 04.10.2015.
+ */
+package BotPlugins
+import Containers.PluginBufferItem
+import java.util.*
+
+public class AdminCommands : BasePlugin()
+{
+ private var DB : HashMap = HashMap()
+ override fun pubInit() :Boolean
+ {
+ try {
+ if(handher?.isAdmin() == false) {
+ throw Exception("User does not have mod rights to the given box. Exiting.")
+ }
+
+ settings?.checkSetting("IPDB", true)
+ var savedDB = settings?.GetSetting("LastSeen").toString()
+
+ if(savedDB != "") {
+ var data = savedDB.split(",")
+ for(user in data) {
+ var IP = savedDB.split(":")[0]
+ var Usernames = savedDB.split(":")[1]
+ DB.put(IP, Usernames)
+ }
+ }
+ return true
+ } catch (e: Exception) {
+ logger?.LogPlugin(pluginName.toString(), "Error: " + e.toString())
+ return false
+ }
+ }
+ override fun connector(buffer : PluginBufferItem) :Boolean
+ {
+ var message = buffer.message.split(" ")
+ var changed :Boolean
+ //TODO: Rewrite, this is a horrible 2AM energy drink powered way to do this.
+ if(DB.containsKey(buffer.extradata)) {
+ //Remove from the old entry
+ var keys = DB.keySet().iterator()
+ while(keys.hasNext())
+ {
+ var key = keys.next()
+ var userlist = DB.get(key)
+ if(userlist != null) {
+ var replaceableString = buffer.userName
+ if(userlist.indexOf(";"+buffer.userName) > 0) replaceableString = ";" + replaceableString
+ if (userlist.contains(buffer.userName)) {
+ userlist.replace(replaceableString, "")
+ }
+ }
+ }
+ //Add to DB
+ var entry = DB.get(buffer.extradata)
+ if (entry != null && !entry.contains(buffer.userName)) DB.set(buffer.userName, ";"+buffer.extradata)
+ changed = true
+ } else {
+ logger?.LogPlugin(this.pluginName.toString(), "New user encountered: " + buffer.userName)
+ DB.put(buffer.userName, buffer.extradata)
+ changed = true
+ }
+
+ if(changed) {
+ var data = ""
+ var keys = DB.keySet().iterator()
+ while(keys.hasNext()) {
+ var key = keys.next()
+ var user = DB.get(key)
+ data += key + ":" + user
+ }
+ settings?.SetSetting("IPDB", data)
+ }
+
+ if(message[0].toLowerCase() == "@alias") {
+ var keys = DB.keySet().iterator()
+ while(keys.hasNext())
+ {
+ var key = keys.next()
+ var userlist = DB.get(key)
+ if(userlist != null) {
+ if(userlist.contains(message[1])) {
+ controller?.AddToBoxBuffer("User aliases: " + userlist)
+ break
+ } else {
+ controller?.AddToBoxBuffer("No data")
+ logger?.LogPlugin(this.pluginName.toString(), "Error: Could not find data for " + message[1])
+ }
+ }
+ }
+ }
+ return true
+ }
+}
\ No newline at end of file
diff --git a/src/BotPlugins/DeadboxCheck.kt b/src/BotPlugins/DeadboxCheck.kt
new file mode 100644
index 0000000..1972b78
--- /dev/null
+++ b/src/BotPlugins/DeadboxCheck.kt
@@ -0,0 +1,49 @@
+/**
+ * Stores last cbox activity and posts about it if records are broken
+ * Created by zingmars on 04.10.2015.
+ */
+package BotPlugins
+import Containers.PluginBufferItem
+
+public class DeadboxCheck : BasePlugin()
+{
+ private var RecordUsername = ""
+ private var RecordTime = 0L
+ private var lastMessage = 0L
+ override fun pubInit() :Boolean
+ {
+ try {
+ settings?.checkSetting("DeadBox", true)
+ var savedData = settings?.GetSetting("DeadBox").toString()
+
+ if(savedData != "") {
+ var data = savedData.split(",")
+ RecordUsername = data[0]
+ RecordTime = data[1].toLong()
+ }
+ return true
+ } catch (e: Exception) {
+ logger?.LogPlugin(this.pluginName.toString(), "Error: " + e.toString())
+ return false
+ }
+ }
+ override fun connector(buffer : PluginBufferItem) :Boolean
+ {
+ var timeSinceLast = buffer.time.toLong() - lastMessage
+ if(lastMessage != 0L && timeSinceLast >= 3600L) {
+ if(timeSinceLast > RecordTime) {
+ RecordTime = timeSinceLast
+ RecordUsername = buffer.userName
+ controller?.AddToBoxBuffer("Congratz " + buffer.userName + "! You just revived the box and set a new record doing so! This deadbox lasted " + (timeSinceLast.toDouble()/60).toString() + " minutes.")
+ saveData()
+ } else {
+ controller?.AddToBoxBuffer("Congratz " + buffer.userName + "! You just revived the box. This deadbox lasted " + (timeSinceLast.toDouble()/60).toString() + " minutes. The longest recorded deadbox was " + (RecordTime.toDouble()/60).toString() + " minutes long and it was broken by " + RecordUsername)
+ }
+ }
+ return true
+ }
+ private fun saveData()
+ {
+ settings?.SetSetting("DeadBox", RecordUsername+","+RecordTime.toString())
+ }
+}
diff --git a/src/BotPlugins/LastSeen.kt b/src/BotPlugins/LastSeen.kt
new file mode 100644
index 0000000..16839b2
--- /dev/null
+++ b/src/BotPlugins/LastSeen.kt
@@ -0,0 +1,121 @@
+/**
+ * Stores user identifieable data and responds to queries about specific users from mods
+ * Created by zingmars on 04.10.2015.
+ */
+package BotPlugins
+import Containers.PluginBufferItem
+import java.util.*
+
+public class LastSeen : BasePlugin() {
+ var users :TreeMap> = TreeMap(String.CASE_INSENSITIVE_ORDER)
+ var HonourableUsers = ArrayList()
+ //TODO: Convert the data into a JSON string or something, because if someone uses these characters in their username the loading will be broken
+ var dbUserNameSeparator = "="
+ var dbUserDataSeparator = "+"
+ var dbRecordSeparator = "<>"
+ var ipSeparatorList = "?"
+ var saverThread = Thread()
+ var changed = false
+
+ private fun databaseSaver()
+ {
+ while (!saverThread.isInterrupted) {
+ try {
+ Thread.sleep(30000) //Save to disk every 30 seconds
+ if(changed) {
+ var data = ""
+ var keys = users.keySet().iterator()
+ var first = true
+
+ while(keys.hasNext()) {
+ var key = keys.next()
+ var user = users.get(key)
+ if (!first) {
+ data += dbRecordSeparator
+ }
+ data += key + dbUserNameSeparator + user?.join(dbUserDataSeparator)
+ first = false
+ }
+
+ settings?.SetSetting("LastSeen", data)
+ logger?.LogBoringPluginData(this.pluginName.toString(), "Saved user data to disk")
+ }
+ } catch (e: Exception) {
+ settings?.SaveSettings() //Will happen if the program is shutting down
+ }
+ }
+ }
+ override fun pubInit() :Boolean
+ {
+ //Load data saved in the database
+ try {
+ settings?.checkSetting("LastSeen", true) //Create the settings variable if it doesn't exist, otherwise it will return a fail
+ settings?.checkSetting("LastSeenHonourable", true)
+
+ var savedHonours = settings?.GetSetting("LastSeenHonourable")
+ var savedDB = settings?.GetSetting("LastSeen").toString()
+ if(savedDB != "") {
+ var data = savedDB.split(dbRecordSeparator)
+ for(user in data) {
+ var username = user.split(dbUserNameSeparator)[0]
+ var userdata = user.split(dbUserNameSeparator)[1].split(dbUserDataSeparator).toArrayList()
+ users.put(username, userdata)
+ }
+ }
+ if(savedHonours != "" && savedHonours != null) {
+ HonourableUsers = savedHonours.split(",").toArrayList()
+ }
+ //Run a separate thread to save users in the background
+ saverThread = Thread(Runnable { this.databaseSaver() })
+ saverThread.isDaemon = true
+ saverThread.start()
+ return true
+ } catch (e: Exception) {
+ logger?.LogPlugin(pluginName.toString(), "Error: " + e.toString())
+ return false
+ }
+ }
+ override fun connector(buffer :PluginBufferItem) :Boolean
+ {
+ var message = buffer.message.split(" ")
+
+ // Respond to @lastseen
+ if(message[0].toLowerCase() == "@lastseen" && (buffer.privilvl == "mod" || buffer.privilvl == "user")) {
+ var user = users.get(message[1])
+ if(user != null) {
+ if(message[1].toLowerCase() != handher?.username?.toLowerCase()) {
+ controller?.AddToBoxBuffer(buffer.userName+": User \"" + message[1] + "\" last seen " + Date(user[2].toLong()*1000).toString())
+ } else {
+ controller?.AddToBoxBuffer(buffer.userName+": I'm here!")
+ }
+ } else {
+ controller?.AddToBoxBuffer(buffer.userName+": User \"" + message[1] + "\" not in database")
+ }
+ }
+
+ // Add user to database
+ var user = users.get(buffer.userName)
+ if(user != null) {
+ // Greet Honourable users
+ if(HonourableUsers.contains(buffer.userName) && buffer.time.toLong() - user[2].toLong() > 86400) {
+ controller?.AddToBoxBuffer("Welcome back " + buffer.userName + "!")
+ }
+
+ if(!user[1].contains(buffer.extradata) && buffer.extradata != "") user[1] += buffer.extradata+ipSeparatorList //new IP to the list
+ user[2] = buffer.time
+ } else {
+ var userData :ArrayList = ArrayList()
+ userData.add(0, buffer.userID)
+ userData.add(1, buffer.extradata)
+ userData.add(2, buffer.time)
+ users.set(buffer.userName, userData)
+ logger?.LogPlugin(pluginName.toString(), "New user encountered: " + buffer.userName)
+ }
+ changed = true
+ return true
+ }
+ override fun stop()
+ {
+ saverThread.interrupt()
+ }
+}
diff --git a/src/BotPlugins/TwitchCheck.kt b/src/BotPlugins/TwitchCheck.kt
new file mode 100644
index 0000000..9feb892
--- /dev/null
+++ b/src/BotPlugins/TwitchCheck.kt
@@ -0,0 +1,162 @@
+/**
+ * Twitch checker plugin. Checks if the given user is streaming or not.
+ * Configuration note: For this to work you need to add a key in Plugin settings named Streamer list
+ * and it's value looks like :0\\0 (these zeroes represent last stream on/off times, and the plugin will change them as people go on/off).
+ * Created by zingmars on 04.10.2015.
+ */
+package BotPlugins
+import Containers.PluginBufferItem
+import java.util.*
+import HTTP
+
+public class TwitchCheck : BasePlugin()
+{
+ private var Daemon = Thread()
+ private var stalkList :TreeMap> = TreeMap(String.CASE_INSENSITIVE_ORDER)
+ private var stalkState :TreeMap = TreeMap(String.CASE_INSENSITIVE_ORDER)
+ private var HTTPClient = HTTP()
+ private val usernameDivisor = ":"
+ private val dataDivisor = "\\"
+ private val entryDivisor = ","
+
+ override fun pubInit() :Boolean
+ {
+ try {
+ HTTPClient.PassLogger(this.logger)
+ settings?.checkSetting("StreamerList", true) //Create the settings variable if it doesn't exist, otherwise it will return a fail
+ var savedDB = settings?.GetSetting("StreamerList").toString()
+ if(savedDB != "") {
+ var users = savedDB.split(entryDivisor)
+ for(user in users) {
+ var data = user.split(usernameDivisor)
+ var userdata = data[1].split(dataDivisor)
+ stalkList.put(data[0], userdata.toArrayList())
+ stalkState.put(data[0], "false")
+ }
+ } else {
+ throw Exception("Plugin not configured. Please add a list of usernames to plugin config files seperated by a comma in a :\\ notation (you can put 0 if you want to, it uses UNIX time)")
+ }
+
+ Daemon = Thread(Runnable { this.Demon() })
+ Daemon.isDaemon = true
+ Daemon.start()
+ return true
+
+ } catch (e: Exception) {
+ logger?.LogPlugin(pluginName.toString(), "Error: " + e.toString())
+ return false
+ }
+ }
+ override fun connector(buffer : PluginBufferItem) :Boolean
+ {
+ var message = buffer.message.split(" ")
+ if(message[0].toLowerCase() == "@laststream" && (buffer.privilvl == "mod" || buffer.privilvl == "user")) {
+ if(message[1] != "") {
+ if(stalkList.contains(message[1])) {
+ var user = stalkList.get(message[1])
+ var state = stalkState.get(message[1])
+ if(user != null && state != null) {
+ if(state == "true") {
+ controller?.AddToBoxBuffer("User is currently streaming.")
+ } else {
+ var lastStreamDate = user.get(1).toLong()
+ if(lastStreamDate != 0L) {
+ //var timeString = generateTimeString((System.currentTimeMillis() / 1000L), user.get(0).toLong(), " ago")
+ var stopTimeString = generateTimeString((System.currentTimeMillis() / 1000L), lastStreamDate, " ago")
+ var timeString = Date((user.get(0).toLong()*1000))
+ controller?.AddToBoxBuffer("User last streamed " + timeString + " (stream ended " + stopTimeString + ")")
+ } else {
+ controller?.AddToBoxBuffer("User " + message[1] + " hasn't streamed yet.")
+ }
+ }
+ }
+ } else {
+ controller?.AddToBoxBuffer("User is not monitored. (Currently monitoring: " + stalkList.keySet().join(",") + ")")
+ }
+ } else {
+ controller?.AddToBoxBuffer("Please specify which streamer you want to know about. (Currently monitoring: " + stalkList.keySet().join(",") + ")")
+ }
+ }
+ return true
+ }
+ override fun stop()
+ {
+ Daemon.interrupt()
+ }
+
+ private fun Demon()
+ {
+ var changes = false
+ while(!Daemon.isInterrupted){
+ try {
+ Thread.sleep(30000) //Check every 30 seconds
+ //TODO: Implement an actual JSON parser
+ //Check for online state
+ var keys = stalkList.keySet().iterator()
+ while(keys.hasNext()) {
+ var key = keys.next()
+ var data = stalkList.get(key)
+ var state = stalkState.get(key)
+ if(data != null && state != null) {
+ var response = HTTPClient.GET("https://api.twitch.tv/kraken/streams/"+key+".json")
+
+ if(!response.contains("\"stream\":null") && state == "false") {
+ changes = true
+
+ data.set(0, (System.currentTimeMillis() / 1000L).toString())
+ stalkList.set(key, data)
+ stalkState.set(key, "true")
+
+ // Find game name
+ var gamename = ""
+ try {
+ gamename = response.substring(response.indexOf("\"game\":")+7,response.indexOf(",\"viewers"))
+ }
+ catch (e: Exception) {} //API Changed?
+ logger?.LogPlugin(this.pluginName.toString(), "User " + key + " has started streaming!")
+ controller?.AddToBoxBuffer("User " + key + " has started streaming " + gamename + ". Click here to view: http://www.twitch.tv/" + key)
+ } else if (response.contains("\"stream\":null") && state == "true") {
+ if(stalkState.get(key) == "true") {
+ changes = true
+
+ data.set(1, (System.currentTimeMillis() / 1000L).toString())
+ stalkList.set(key, data)
+ stalkState.set(key, "false")
+ logger?.LogPlugin(this.pluginName.toString(), "User " + key + " has stopped streaming!")
+ controller?.AddToBoxBuffer("User " + key + " has stopped streaming. Stream length was " + generateTimeString(data.get(1).toLong(), data.get(0).toLong()) + " minutes")
+ }
+ }
+ } else {
+ logger?.LogPlugin(this.pluginName.toString(), "Error while trying to modify database")
+ }
+ }
+ //If stuff happened, save it
+ if(changes) {
+ keys = stalkList.keySet().iterator()
+ var dbdata = ""
+ var first = true
+ while(keys.hasNext()) {
+ var key = keys.next()
+ var data = stalkList.get(key)
+ if(data != null) {
+ if(!first) dbdata+=","
+ dbdata += key+usernameDivisor+data.get(0)+dataDivisor+data.get(1)
+ first = false
+ }
+ }
+ settings?.SetSetting("StreamerList", dbdata)
+ changes = false
+ }
+ } catch (e: Exception) {
+ logger?.LogPlugin(this.pluginName.toString(), "Error: " + e.toString())
+ }
+ }
+ }
+ private fun generateTimeString(current :Long, past :Long, endString :String = "") :String
+ {
+ var streamMinutesAgo = (current-past).toDouble()/60
+ var streamHoursAgo = if(streamMinutesAgo/60.0 >= 1.0) {streamMinutesAgo/60.0} else 0.0
+ var streamDaysAgo = if(streamHoursAgo/24.0 >= 1.0) {streamHoursAgo/24.0} else 0.0
+ return if(streamDaysAgo != 0.0) {streamDaysAgo.toString() + " days"+ endString} else if (streamHoursAgo != 0.0) {streamHoursAgo.toString() + " hours" + endString} else {streamMinutesAgo.toString() + " minutes" + endString}
+ }
+}
diff --git a/src/BotPlugins/UserCommands.kt b/src/BotPlugins/UserCommands.kt
new file mode 100644
index 0000000..2027134
--- /dev/null
+++ b/src/BotPlugins/UserCommands.kt
@@ -0,0 +1,33 @@
+/**
+ * Checks chat for user written messages and responds to specific queries
+ * Created by zingmars on 04.10.2015.
+ */
+package BotPlugins
+import Containers.PluginBufferItem
+
+public class UserCommands : BasePlugin()
+{
+ override fun pubInit() :Boolean
+ {
+ return true
+ }
+ override fun connector(buffer : PluginBufferItem) :Boolean
+ {
+ var message = buffer.message.split(" ")
+ when(message[0].toLowerCase()) {
+ "@ping" -> controller?.AddToBoxBuffer(buffer.userName + ": PONG!")
+ "@nextstream" -> controller?.AddToBoxBuffer(buffer.userName + ": Soon™")
+ //TODO: have some sort of database where plugins can register their commands so this command can be automated.
+ "@help" -> {
+ controller?.AddToBoxBuffer("For more information please see https://github.com/zingmars/Cbox-bot. For feature requests ask zingmars.")
+ controller?.AddToBoxBuffer("@about - About this bot; @lastseen - Output the date of user's last message; @ping - check if I'm alive; @help - display this")
+ controller?.AddToBoxBuffer("@laststream - Get when an user has last streamed (try @laststream zingmars); @nextstream - OTG's next stream time")
+ controller?.AddToBoxBuffer("Available commands:")
+ }
+ "@about" -> {
+ controller?.AddToBoxBuffer("Hi! My name is " + handher?.username + " and I'm a bot for cbox.ws written in Kotlin. Check me out at: https://github.com/zingmars/Cbox-bot")
+ }
+ }
+ return true
+ }
+}
\ No newline at end of file
diff --git a/src/Box.kt b/src/Box.kt
new file mode 100644
index 0000000..b086408
--- /dev/null
+++ b/src/Box.kt
@@ -0,0 +1,298 @@
+/**
+ * cbox.ws chat manager class
+ * Created by zingmars on 03.10.2015.
+ */
+import Containers.Message
+import Containers.PluginBufferItem
+import Containers.User
+import java.util.*
+
+public class Box(private val Settings :Settings, private val Logger :Logger, private val ThreadController :ThreadController)
+{
+ private var active = false
+ private var lastMessageID = "0"
+ private var server = Settings.GetSetting("server")
+ private var id = Settings.GetSetting("boxid")
+ private var tag = Settings.GetSetting("boxtag")
+ private var loggedIn = false
+ private var isAdmin = false
+ private var user :User = User()
+ private var HTTPUtility = HTTP(Logger)
+ private var Daemon :Thread = Thread()
+ private var refreshRate = Settings.GetSetting("refreshRate").toLong()
+ private var adminFailRate = 0
+ private var pingRate = Math.floor((30000.toLong()/refreshRate).toDouble()).toInt()
+ public var toCLI = false
+
+ init
+ {
+ // Get the latest messages
+ Logger.LogMessage(31)
+ var messages = GetMessages(lastMessageID)
+
+ if(messages == null) {
+ Logger.LogMessage(32)
+ } else {
+ active = true
+
+ // Log in
+ if(Settings.GetSetting("username") != "") {
+ user.SetCredentials(Settings.GetSetting("username"), SetPassword(), Settings.GetSetting("avatar"))
+ this.LogIn()
+ }
+
+ // Start a thread that monitors the box
+ Logger.LogMessage(30)
+ this.start()
+ }
+ }
+
+ //Thread functions
+ public fun isActive() :Boolean
+ {
+ return active
+ }
+ public fun start(restarted :Boolean = false)
+ {
+ active = true
+ Daemon = Thread(Runnable () { this.Demon() })
+ Daemon.isDaemon = true
+ Daemon.start()
+ Logger.LogMessage(45)
+ ThreadController.ConnectBox(Daemon)
+ if(restarted) ThreadController.AddToCLIBuffer("Box restarted successfully")
+ }
+ public fun stop()
+ {
+ this.active = false;
+ ThreadController.DisconnectBox()
+ Daemon.stop()
+ Logger.LogMessage(61)
+ }
+ private fun Demon()
+ {
+ var pingcounter = 0
+ while(active) {
+ try {
+ Thread.sleep(this.refreshRate)
+ var boxMessages = this.GetMessages()
+ //Send to plugin controller
+ if(boxMessages != null) {
+ for(message in boxMessages) {
+ // If User is a guest: userLevel = 1, userLevel2 = 0, user: userLevel = 2, userLevel2 = 1, mod: userLevel = 3, UserLevel2 = 2
+ var privlvl = if(message.userLevel == "3" && message.privLevel2 == "2") "mod" else if (message.userLevel == "2" && message.privLevel2 == "1") "user" else "guest"
+ var extra = if(isAdmin) {getIP(message.id) as String} else ""
+ ThreadController.AddToPluginBuffer(PluginBufferItem(message.id, message.userID, message.postDate,message.username, message.message, privlvl, extra))
+ if(toCLI) ThreadController.AddToCLIBuffer("{"+extra+":"+message.id+"}["+message.humanReadableDate+"]<"+message.userID+":"+message.username+">("+privlvl+") "+message.message)
+ }
+ }
+
+ //Load plugin response
+ var pluginMessages = ThreadController.GetBoxBuffer()
+ if(pluginMessages.count() > 0) {
+ for(message in pluginMessages) {
+ SendMessage(message)
+ }
+ } else {
+ pingcounter++
+ if(pingcounter == pingRate) {
+ KeepAlive()
+ pingcounter = 0
+ }
+ }
+ } catch (e: Exception) {
+ ThreadController.AddToCLIBuffer("Box module failed: " + e.toString())
+ Logger.LogMessage(999, "Unknown box loop error")
+ }
+ }
+ }
+ public fun Reload() :Boolean
+ {
+ this.stop()
+ if(Settings.GetSetting("username") != "" && user.Username != "") {
+ Logger.LogMessage(41)
+ this.LogOut()
+ this.ChangeCredentials(user.Username, user.Password, user.AvatarURL)
+ this.LogIn()
+ }
+ refreshRate = Settings.GetSetting("refreshRate").toLong()
+ pingRate = Math.floor((30000.toLong()/refreshRate).toDouble()).toInt()
+ this.start(true)
+ return true;
+ }
+ public fun changeRefreshRate(newRate :Long)
+ {
+ this.refreshRate = newRate
+ this.pingRate = Math.floor((30000.toLong()/refreshRate).toDouble()).toInt()
+ }
+
+ //Message sending and receiving API
+ public fun GetMessages(lastID :String = lastMessageID) :ArrayList?
+ {
+ try {
+ var messages = HTTPUtility.GET("http://www"+server+".cbox.ws/box/?sec=ar&boxid="+id+"&boxtag="+tag+"&_v=857&p="+lastID+"&c="+(System.currentTimeMillis() / 1000L).toString()).split("\n").reversed()
+ var sortedMessages = ArrayList()
+ if(messages.size() > 0) {
+ for (i in 0..messages.size()-2) {
+ var splitMessage = messages[i].split("\t")
+ lastMessageID = splitMessage[0]
+
+ var tmpMessage = parseMessage(splitMessage)
+ sortedMessages.add(tmpMessage as Message)
+
+ // Log the message
+ Logger.LogChat(tmpMessage)
+ }
+ }
+
+ return sortedMessages
+ } catch ( e: Exception) {
+ Logger.LogMessage(32, e.toString())
+ return null
+ }
+ }
+ public fun SendMessage(message :String) :Boolean
+ {
+ //TODO: for some reason during POST the '+' character will go missing.
+ Logger.LogMessage(42, message)
+ if(loggedIn) {
+ var POST = HTTPUtility.POST("http://www"+server+".cbox.ws/box/?sec=submit&boxid="+id+"&boxtag="+tag+"&_v=857","aj=857&lp="+lastMessageID+"&pst="+message+"&nme="+user.Username+"&eml="+user.AvatarURL+"&key="+user.Key)
+ var response = POST.Content.split("\n")
+ try {
+ if (parseMessage(response[2].split("\t")) == null) {
+ Logger.LogMessage(44)
+ return false
+ } else {
+ return true
+ }
+ } catch (e: Exception) {
+ Logger.LogMessage(44)
+ return false
+ }
+ } else {
+ return false
+ }
+ }
+ private fun parseMessage(message :List) :Message?
+ {
+ try {
+ var post :String
+
+ // Turn links back to normal text
+ post = message[6].replace("[link]", "")
+ //TODO: Turn HTML entities into their respective characters.
+ // I remember that there was a prettier way to turn an array into arguments, yet I can't remember it right now.
+ return Message(message[0], message[1], message[2], message[3], message[4], message[5], post, message[7], message[8], message[9])
+ } catch (e: Exception) {
+ return null
+ }
+ }
+
+ //Credential related functions
+ public fun ChangeCredentials(Username :String, Password :String, Avatar :String = "")
+ {
+ user.SetCredentials(Username, Password, Avatar)
+ }
+ private fun checkAdmin()
+ {
+ var test = getIP()
+ if(test != null) {
+ isAdmin = true
+ Settings.SetSetting("isAdmin", "true")
+ Logger.LogMessage(50)
+ }
+ }
+ private fun LogIn() :Boolean
+ {
+ if(loggedIn) LogOut()
+ Logger.LogMessage(36, user.Username)
+ try {
+ var POST = HTTPUtility.POST("http://www"+server+".cbox.ws/box/?boxid="+id+"&boxtag="+tag+"&sec=profile&n=" + user.Username + "&k=","pword="+user.Password+"&sublog=+Log+in+")
+ if(POST.Content != "null") {
+ var newCookies = POST.Headers.get("Set-Cookie")
+ var newCookiesLength = if(newCookies != null) newCookies.size() else 0
+ if(newCookiesLength != 0) {
+ for (i in 0..newCookiesLength-1) {
+ val cookie = newCookies?.get(i).toString();
+ if (cookie.startsWith("key_"+id)) {
+ user.Key = cookie.substring(cookie.indexOf("=")+1, cookie.indexOf(";")-1)
+ if(user.Key != "delete") {
+ loggedIn = true
+ checkAdmin()
+ Logger.LogMessage(43)
+ }
+ return true
+ }
+ }
+ return false
+ } else {
+ loggedIn = false
+ return false
+ }
+ }
+ else {
+ return false
+ }
+ } catch (e: Exception) {
+ Logger.LogMessage(33)
+ loggedIn = false
+ return false
+ }
+ }
+ private fun LogOut() :Boolean
+ {
+ Logger.LogMessage(37)
+ loggedIn = false
+ isAdmin = false
+ Settings.SetSetting("isAdmin", "false")
+ user.Key = ""
+ try {
+ var GET = HTTPUtility.GET("http://www"+server+".cbox.ws/box/?boxid="+id+"&boxtag="+tag+"&sec=profile&n="+user.Username+"&l=1&k="+user.Key) //fire and forget
+ } catch (e: Exception) {
+ }
+ return true
+ }
+ private fun SetPassword() :String
+ {
+ var password = Settings.GetSetting("password")
+ if(password == "") {
+ println("Password not defined, please enter user's password")
+ password = readLine().toString()
+ }
+ return password
+ }
+
+ //Miscellaneous functions
+ public fun getIP(ID :String = lastMessageID) :String?
+ {
+ var GET = HTTPUtility.GET("http://www"+server+".cbox.ws/box/?sec=getip&boxid="+id+"&boxtag="+tag+"&_v=857&n="+user.Username+"&k="+user.Key+"&i="+ID)
+ var state = GET[0]
+ if(state == '0') {
+ adminFailRate++
+ if(adminFailRate == 20) {
+ isAdmin = false
+ Settings.SetSetting("isAdmin", "false")
+ }
+ return null
+ } else return GET.substring(1)
+ }
+ private fun KeepAlive() :Boolean
+ {
+ Logger.LogConnection(0)
+ try {
+ var POST = HTTPUtility.POST("http://www"+server+".cbox.ws/box/?sec=users&boxid="+id+"&boxtag="+tag+"&_v=857 ", "state=online&k="+user.Key+"&rcid=9999_99999999") //WTF is RCID?
+ if(POST.Content == "1\n") {
+ Logger.LogConnection(1)
+ return true
+ }
+ else {
+ Logger.LogConnection(2)
+ return false
+ }
+ } catch (e: Exception) {
+ return false
+ }
+ }
+
+}
diff --git a/src/CLI.kt b/src/CLI.kt
new file mode 100644
index 0000000..c43ba6f
--- /dev/null
+++ b/src/CLI.kt
@@ -0,0 +1,427 @@
+/**
+ * CLI manager daemon
+ * Created by zingmars on 03.10.2015.
+ */
+import java.io.BufferedReader
+import java.io.InputStreamReader
+import java.io.PrintWriter
+import java.net.ServerSocket
+
+public class CLI (private val Settings :Settings, private val Logger :Logger, private val Box :Box, private val Plugins :Plugins, private val ThreadController :ThreadController)
+{
+ private var port :Int = Settings.GetSetting("daemonPort").toInt()
+ private var Daemon = Thread()
+ private var enabled = false
+ private var active = false
+
+ init
+ {
+ enabled = Settings.GetSetting("daemonEnabled").toBoolean()
+ if(enabled) {
+ this.start()
+ } else {
+ Logger.LogMessage(13)
+ }
+
+ }
+
+ //Daemon functions
+ public fun isActive() :Boolean
+ {
+ return active
+ }
+ public fun start()
+ {
+ if(enabled) {
+ this.active = true
+ Daemon = Thread(Runnable () { this.Demon() })
+ Daemon.isDaemon = true
+ Logger.LogMessage(14, port.toString())
+ Daemon.start()
+ ThreadController.ConnectCLI(Daemon)
+ }
+ }
+ public fun stop()
+ {
+ this.active = false;
+ ThreadController.DisconnectCLI()
+ Daemon.stop()
+ Logger.LogMessage(60)
+ }
+ public fun Reload() :Boolean
+ {
+ try {
+ Logger.LogMessage(59)
+ this.stop()
+ this.start()
+ return true
+ } catch (e: Exception) {
+ Logger.LogMessage(65)
+ this.active = false
+ return false
+ }
+ }
+ private fun Demon() //Yes, the spelling's on purpose
+ {
+ // Create a simple daemon listening on a specified port
+ val socket = ServerSocket(port)
+
+ while(active) {
+ val client = socket.accept() // Only allow one at a time since it's not supposed to be a public interface
+ if(client.isConnected) {
+ this.ActiveConnectionHandler(client)
+ }
+ }
+ }
+ private fun ActiveConnectionHandler(client :java.net.Socket)
+ {
+ val SessionStartTime = System.currentTimeMillis() / 1000L;
+ val out = PrintWriter(client.outputStream, true)
+ val _in = BufferedReader(InputStreamReader(client.inputStream))
+ var connectionActive = true
+
+ Logger.LogMessage(15, client.remoteSocketAddress.toString())
+ out.println("Cbox.ws bot CLI interface - Welcome! Type :help for help. Please note that commands are not checked for correctness, and will accept any input. Use this interface at your own risk.") //Welcome message
+ // Await input and react to it
+ var input :String
+
+ while(true) {
+ for(msg in ThreadController.GetCLIBuffer()) {
+ out.println(msg)
+ }
+ out.printf(">> ")
+ try {
+ input = _in.readLine() //My major gripe with kotlin - you can't have expression within if, while etc. Sort of annoying.
+ } catch(e: Exception) {
+ break; //Client has disconnected, close socket.
+ }
+
+ // Separate command from parameters and log it
+ var command :List = input.split(" ")
+ if(command.size() > 1) {
+ Logger.LogCommands(command[0], input.substring(input.indexOf(" ")+1))
+ } else {
+ Logger.LogCommands(input)
+ }
+
+ try {
+ // Execute commands
+ when(command[0].toLowerCase()) {
+ //TODO: have all of the functions return some sort of indication of their actions
+ ":quit" -> { // Disconnect
+ if(ConfirmAction(out, _in)) {
+ Logger.LogMessage(16, "Session lasted " + (System.currentTimeMillis() / 1000L - SessionStartTime).toString() + " seconds")
+ client.close()
+ connectionActive = false
+ }
+ else {
+ Logger.LogMessage(17)
+ }
+ }
+ ":shutdowncli" -> { // Shut down the CLI interface. Warn - can't bring it up without restarting the server.
+ if(ConfirmAction(out, _in)) {
+ Logger.LogMessage(16, "Session lasted " + (System.currentTimeMillis() / 1000L - SessionStartTime).toString() + " seconds")
+ Logger.LogMessage(29)
+ client.close()
+ this.stop()
+ this.enabled = false
+ connectionActive = false
+ } else {
+ Logger.LogMessage(17)
+ }
+ }
+ ":ping" -> {
+ out.println("Pong!")
+ }
+ ":shutdown" -> {
+ if(ConfirmAction(out, _in)) {
+ Logger.LogMessage(16, "Session lasted " + (System.currentTimeMillis() / 1000L - SessionStartTime).toString() + " seconds")
+ Logger.LogMessage(55)
+ out.println("Shutdown initiated. Good bye!")
+ connectionActive = false
+
+ Box.stop()
+ Plugins.stop()
+ this.stop()
+ }
+ }
+ ":reload" -> {
+ if(ConfirmAction(out, _in)) {
+ this.Reload()
+ }
+ }
+ ":help" -> {
+ out.println("Please read CLI.kt for full command list.")
+ }
+ "settings.savesettings" -> {
+ Settings.SaveSettings()
+ out.println("Changes saved successfully.")
+ }
+ "settings.loadsettings" -> {
+ if(ConfirmAction(out, _in)) Settings.LoadSettings()
+ out.println("Settings reloaded successfully. Please reload modules for the changes to have an effect.")
+ }
+ "settings.getsetting" -> {
+ out.println(command[1] + ":" + Settings.GetSetting(command[1]))
+ }
+ "settings.setsetting" -> {
+ Settings.SetSetting(command[1], command[2])
+ out.println(command[1] + " set to " + command[2])
+ }
+ "settings.changesettingsfile" -> {
+ if(ConfirmAction(out, _in)) Settings.ChangeSettingsFile(command[1])
+ out.println("Settings reloaded successfully. Please reload modules for the changes to have an effect.")
+ }
+ "settings.getSettings" -> {
+ out.println(Settings.GetAllSettings())
+ }
+ "logger.disable" -> {
+ if(ConfirmAction(out, _in)) {
+ var pam1 = true
+ var pam2 = true
+ try{
+ pam1 = command[1].toBoolean()
+ pam2 = command[2].toBoolean()
+
+ } catch (ex: Exception) {
+ }
+ Logger.Disable(pam1, pam2)
+ out.println("Logging disabled")
+ }
+ }
+ "logger.enable" -> {
+ var pam1 = true
+ var pam2 = true
+ try{
+ pam1 = command[1].toBoolean()
+ pam2 = command[2].toBoolean()
+
+ } catch (ex: Exception) {
+ }
+ Logger.Enable(pam1, pam2)
+ out.println("Logging enabled")
+ }
+ "logger.archive" -> {
+ if(ConfirmAction(out,_in)) {
+ Logger.ArchiveOldLogs()
+ out.println("Logs archived successfully")
+ }
+ }
+ "logger.toconsole" -> {
+ Logger.ChangeConsoleLoggingState(command[1].toBoolean())
+ out.println("Changed console logging behaviour")
+ }
+ "logger.tofile" -> {
+ Logger.ChangeFileLoggingState(command[1].toBoolean())
+ out.println("Changed file logging behaviour")
+ }
+ "logger.chattofile" -> {
+ Logger.ChangeChatLoggingState(command[1].toBoolean())
+ out.println("Changed chat logging behaviour")
+ }
+ "logger.changelogfile" -> {
+ if(ConfirmAction(out, _in)) {
+ var pam1 = if(command[1] == "") Settings.GetSetting("logFile") else command[1]
+ var pam2 = if(command[2] == "") Settings.GetSetting("logFolder") else command[2]
+
+ Logger.ChangeLogFile(pam1, pam2)
+ out.println("Changed currently active log file")
+ }
+ }
+ "logger.changechatlogfile" -> {
+ if(ConfirmAction(out, _in)) {
+ var pam1 = if(command[1] == "") Settings.GetSetting("chatLogFile") else command[1]
+ var pam2 = if(command[2] == "") Settings.GetSetting("logFolder") else command[2]
+
+ Logger.ChangeChatLogFile(pam1, pam2)
+ out.println("Changed currently active chat log file")
+ }
+ }
+ "logger.changepluginlogfile" -> {
+ if(ConfirmAction(out, _in)) {
+ var pam1 = if(command[1] == "") Settings.GetSetting("chatLogFile") else command[1]
+ var pam2 = if(command[2] == "") Settings.GetSetting("logFolder") else command[2]
+
+ Logger.ChangePluginLogFile(pam1, pam2)
+ out.println("Changed currently active chat log file")
+ }
+ }
+ "logger.reload" -> {
+ ThreadController.AddToMainBuffer("Restart,Logger")
+ }
+ "global.reload" -> {
+ out.println("This will initiate a global reload. You might be disconnected and the app might not reload successfully.")
+ if(ConfirmAction(out, _in)) {
+ Logger.LogMessage(54)
+ ThreadController.AddToMainBuffer("Restart,Logger")
+ ThreadController.AddToMainBuffer("Restart,Box")
+ ThreadController.AddToMainBuffer("Restart,Plugins")
+ out.println("Please wait...")
+ Thread.sleep(Settings.GetSetting("refreshRate").toLong()*3) //wait for the main process to sync and the other processes to do their thing
+ out.println("Modules reloaded")
+ }
+ }
+ "box.reload" -> {
+ ThreadController.AddToMainBuffer("Restart.Box")
+ out.println("Please wait...")
+ Thread.sleep(Settings.GetSetting("refreshRate").toLong()*3)
+ }
+ "box.tocli" -> {
+ this.CLIChat(out, _in, client)
+ }
+ "box.relog" -> {
+ out.println("Relogging with given credentials (username, password, avatarURL) and reloading the box")
+ try {
+ Box.ChangeCredentials(command[1], command[2], command[3])
+ } catch (e: Exception) {
+ Box.ChangeCredentials(command[1], command[2], "")
+ }
+ ThreadController.AddToMainBuffer("Restart.Box")
+ out.println("Please wait...")
+ Thread.sleep(Settings.GetSetting("refreshRate").toLong()*3)
+ }
+ "box.refreshrate" -> {
+ Box.changeRefreshRate(command[1].toLong())
+ out.println("Box refresh rate changed")
+ }
+ "box.getip" -> {
+ out.println("Response: " + Box.getIP(command[1]))
+ }
+ "box.send" -> {
+ Box.SendMessage(command.join(" ").replace("send", ""))
+ }
+ "plugins.reload" -> {
+ ThreadController.AddToMainBuffer("Restart.Plugins")
+ }
+ "plugins.disable" -> {
+ if(ConfirmAction(out, _in)) {
+ Plugins.Disable()
+ }
+ }
+ "plugins.enable" -> {
+ if(ConfirmAction(out, _in)) {
+ Plugins.Enable()
+ }
+ }
+ "plugins.refreshrate" -> {
+ Plugins.changeRefreshRate(command[1].toLong())
+ }
+ "plugins.unload" -> {
+ if(ConfirmAction(out, _in)) {
+ out.println(Plugins.unloadPlugin(command[1]))
+ }
+ }
+ "plugins.load" -> {
+ //TODO: Bug - the plugin needs to be correctly capitalised or it will crash the thread
+ out.println("Warning: There's currently a bug that will unrecoverably crash the CLI module if you misspell the plugin's name or write it in the wrong CaSe.")
+ if(ConfirmAction(out, _in)) {
+ Plugins.LoadPlugin(command[1]+".kt")
+ }
+ }
+ "plugins.reloadplugin" -> {
+ out.println("Warning: There's currently a bug that will unrecoverably crash the CLI module if you misspell the plugin's name or write it in the wrong CaSe.")
+ if(ConfirmAction(out, _in)) {
+ Plugins.reloadPlugin(command[1])
+ }
+ }
+ "plugins.savesettings" -> {
+ Plugins.SavePluginSettings()
+ out.println("Saved")
+ }
+ "plugins.getSettings" -> {
+ out.println(Plugins.GetAllSettings())
+ }
+ "plugins.setSetting" -> {
+ Plugins.SetSetting(command[1], command[2])
+ out.println(command[1] + " set to " + command[2])
+ }
+ //TODO: Create a way to talk directly to plugins from CLI
+ else -> {
+ out.println("Unrecognised command")
+ }
+ }
+ } catch (e: Exception) {
+ out.println("An error occurred while executing your command")
+ Logger.LogMessage(999, "CLI command execution failure: " + e.toString())
+ }
+
+
+ //Special case : Break out of the while loop if the client has quit
+ if(!connectionActive) {
+ break;
+ }
+ }
+ }
+
+ //CLI misc functions
+ private fun ConfirmAction(output :PrintWriter, input :BufferedReader) :Boolean
+ {
+ output.println("Are you sure? (no)")
+
+ try {
+ output.printf("> ")
+ var userInput = input.readLine() //My major gripe with kotlin - you can't have expression within if, while etc. Sort of annoying.
+ if(userInput == "yes") {
+ return true
+ }
+ else {
+ output.println("Command cancelled")
+ return false
+ }
+ } catch(e: Exception) {
+ //Client has DCed
+ }
+ return false
+ }
+ private fun CLIChat(output :PrintWriter, input :BufferedReader, client :java.net.Socket)
+ {
+ // Very hacky CLI chat. It basically relies on timeout interruptions to receive messages and it isn't the most stable thing around.
+ //Initialise environment
+ val SessionStartTime = System.currentTimeMillis() / 1000L;
+ Logger.LogMessage(57)
+ ThreadController.GetCLIBuffer() //Clear buffer
+ Box.toCLI = true
+ client.soTimeout = Settings.GetSetting("refreshRate").toInt()
+
+ //Welcome message
+ output.print("\u001B[2J")
+ output.flush()
+ output.println("CLI Chat 1.0. Enjoy your stay. Please note that this will only show messages written since chat was started. To see logs, please refer to your logs directory.")
+ output.println("Write really short messages to the char just write anything and press enter, :quit to quit, :write .> enter -> message to type longer messages without being interrupted. (this will pause receiving of messages though)")
+
+ //Print messages and allow input
+ var CLIActive = true
+ while(CLIActive) {
+ for(msg in ThreadController.GetCLIBuffer()) {
+ output.println(msg)
+ }
+
+ try {
+ var userInput = input.readLine()
+ when (userInput) {
+ ":quit" -> {
+ CLIActive = false
+ client.soTimeout = 0
+ Box.toCLI = false
+ Logger.LogMessage(58, "Session lasted " + (System.currentTimeMillis() / 1000L - SessionStartTime).toString() + " seconds")
+ ThreadController.GetCLIBuffer()
+ output.print("\u001B[2J")
+ }
+ ":write" -> {
+ output.printf(":")
+ client.soTimeout = 0
+ userInput = input.readLine()
+ Box.SendMessage(userInput)
+ client.soTimeout = Settings.GetSetting("refreshRate").toInt()
+ }
+ else -> {
+ Box.SendMessage(userInput)
+ }
+ }
+ } catch (e: Exception) {
+ }
+ }
+
+ return
+ }
+}
\ No newline at end of file
diff --git a/src/Containers/Message.kt b/src/Containers/Message.kt
new file mode 100644
index 0000000..03fb4a7
--- /dev/null
+++ b/src/Containers/Message.kt
@@ -0,0 +1,15 @@
+/**
+ * Class that holds all the cbox.ws messages
+ * Created by zingmars on 03.10.2015.
+ */
+package Containers
+
+public class Message(public val id :String, public val postDate :String, public val humanReadableDate :String, public val username :String, public val userLevel :String, public val avatarURL :String, public val message :String, public val unknownField :String, public val privLevel2 :String, public val userID :String) {
+ // A couple of notes on this class:
+ // The fields come from me reading the output of the requests to cbox's server
+ // The format isn't actually documented anywhere and I'm too lazy to read the scripts file
+ // Anything after message is just a guess from my observations. No, I have no idea why there are two userLevel strings.
+ // As for the unknown field, it appears to be a something you can toggle in admin panel, something OverTheGun's cbox has disabled.
+ // Because of this, it will pretty much always be empty. I will however leave the field, should someone use this for another cbox need it.
+ // If User is a guest: userLevel = 1, userLevel2 = 0, user: userLevel = 2, userLevel2 = 1, mod: userLevel = 3, UserLevel2 = 2
+}
\ No newline at end of file
diff --git a/src/Containers/POST.kt b/src/Containers/POST.kt
new file mode 100644
index 0000000..d68317d
--- /dev/null
+++ b/src/Containers/POST.kt
@@ -0,0 +1,6 @@
+package Containers
+/**
+ * Holds POST response data
+ * Created by zingmars on 04.10.2015.
+ */
+public class POST(public val Headers :MutableMap>, public val Content :String) {}
\ No newline at end of file
diff --git a/src/Containers/PluginBufferItem.kt b/src/Containers/PluginBufferItem.kt
new file mode 100644
index 0000000..affb370
--- /dev/null
+++ b/src/Containers/PluginBufferItem.kt
@@ -0,0 +1,7 @@
+/**
+ * Plugin thread's buffer item
+ * Created by zingmars on 04.10.2015.
+ */
+package Containers
+
+public class PluginBufferItem(public val id :String, public val userID :String, public val time :String, public val userName :String, public val message :String, public val privilvl :String = "user", public val extradata :String = "") {}
\ No newline at end of file
diff --git a/src/Containers/User.kt b/src/Containers/User.kt
new file mode 100644
index 0000000..dfe8c52
--- /dev/null
+++ b/src/Containers/User.kt
@@ -0,0 +1,29 @@
+/**
+ * User credentials holder
+ * Created by zingmars on 03.10.2015.
+ */
+package Containers
+
+public class User(public var Username :String = "", public var Password :String = "", public var AvatarURL :String = "")
+{
+ public var Key = ""
+ public fun SetCredentials(newUsername: String, newPassword: String, newAvatarURL: String = "")
+ {
+ this.SetUsername(newUsername)
+ this.SetPassword(newPassword)
+ this.SetAvatarURL(newAvatarURL)
+ }
+ //API Functions
+ public fun SetUsername(newUsername :String)
+ {
+ Username = newUsername
+ }
+ public fun SetPassword(newPassword :String)
+ {
+ Password = newPassword
+ }
+ public fun SetAvatarURL(newAvatarURL :String)
+ {
+ AvatarURL = newAvatarURL
+ }
+}
diff --git a/src/HTTP.kt b/src/HTTP.kt
new file mode 100644
index 0000000..06121f3
--- /dev/null
+++ b/src/HTTP.kt
@@ -0,0 +1,72 @@
+/**
+ * Helper class to help with HTTP requests
+ * Created by zingmars on 03.10.2015.
+ */
+import Containers.POST
+import java.io.BufferedReader
+import java.io.DataOutputStream
+import java.io.InputStreamReader
+import java.net.HttpURLConnection
+import java.net.URL
+import java.nio.charset.StandardCharsets
+
+public class HTTP(private var Logger :Logger? = null)
+{
+ public fun GET(URL :String) :String
+ {
+ Logger?.LogConnection(3, URL)
+ var site = URL(URL)
+ return site.readText(StandardCharsets.UTF_8)
+ }
+ public fun POST(URL :String, Data :String) :POST
+ {
+ //I swear to god, this might just be the only place on the internets that has HTTP POST code adapted for use with Kotlin.
+ Logger?.LogConnection(4, URL)
+ var userAgent = "cbox bot (https://github.com/zingmars/Cbox-bot) POST"
+
+ var site = URL(URL)
+ var postData = Data.toByteArray(StandardCharsets.UTF_8)
+ var postDataLength = postData.size()
+
+ var connection= site.openConnection() as HttpURLConnection
+ connection.doOutput = true
+ connection.instanceFollowRedirects = false
+ connection.requestMethod = "POST"
+ connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
+ connection.setRequestProperty("charset", "utf-8")
+ connection.setRequestProperty("User-Agent", userAgent)
+ connection.setRequestProperty("Content-Length", postDataLength.toString())
+ connection.useCaches = false
+
+ try {
+ //Send the request
+ var outputStream = DataOutputStream(connection.outputStream)
+ outputStream.write(postData)
+
+ //Get the response
+ var input = connection.inputStream
+ var inputBuffer = BufferedReader(InputStreamReader(input))
+ var line :String
+ var response = StringBuffer()
+ while(true) {
+ try {
+ line = inputBuffer.readLine()
+ } catch (e: Exception) {
+ break;
+ }
+ response.append(line+"\n")
+ }
+ inputBuffer.close()
+ connection.disconnect()
+
+ return Containers.POST(connection.headerFields, response.toString())
+ }
+ catch (e: Exception) {
+ return Containers.POST(connection.headerFields, "null")
+ }
+ }
+ public fun PassLogger(logger :Logger?)
+ {
+ this.Logger = logger
+ }
+}
\ No newline at end of file
diff --git a/src/Logger.kt b/src/Logger.kt
new file mode 100644
index 0000000..c827cca
--- /dev/null
+++ b/src/Logger.kt
@@ -0,0 +1,384 @@
+/**
+ * Overengineered logger class
+ * Created by zingmars on 02.10.2015.
+ */
+import Containers.Message
+import java.io.File
+import java.nio.charset.StandardCharsets
+import java.text.SimpleDateFormat
+import java.util.*
+
+public class Logger(private val Settings :Settings) {
+ private var DateFormat = SimpleDateFormat("dd/MM/yyyy HH:mm:ss:SSS")
+ private var log = File("logs/log.txt")
+ private var chatLog = File("logs/chat.txt")
+ private var pluginLog = File("logs/plugins.txt")
+ private var enableStatus = true
+ private var loadedFile = false
+ private var logLevel :Int = Settings.GetSetting("logLevel").toInt()
+ private val maxLogs :Int = Settings.GetSetting("maxLogs").toInt()
+ private var logFolder :String = Settings.GetSetting("logFolder")
+ private var logFileName :String = Settings.GetSetting("logFile")
+ private var chatLogFile :String = Settings.GetSetting("chatLogFile")
+ private var pluginLogFile :String = Settings.GetSetting("chatLogFile")
+ private var consoleLog :Boolean = Settings.GetSetting("consoleLog").toBoolean()
+ private var fileLog :Boolean = Settings.GetSetting("fileLog").toBoolean()
+ private var logChat :Boolean = Settings.GetSetting("logChat").toBoolean()
+ private var logPlugins :Boolean = Settings.GetSetting("enablePlugins").toBoolean() //plugins are always logged if there's file/console output. It's up to the plugins themselves to use this function.
+ private var logArchiving :Boolean = Settings.GetSetting("archiveLogs").toBoolean()
+ private var logRotate :Boolean = Settings.GetSetting("logRotate").toBoolean()
+
+ init
+ {
+ if (fileLog) {
+ log = File(logFolder+"/"+logFileName)
+ if(this.loginit()) {
+ log.writeText("Logging started at " + Date())
+ if (consoleLog) println("Logger initiated at " + Date())
+ loadedFile = true
+ } else {
+ println("Error while initialising the logging module.")
+ }
+ }
+ else {
+ if(consoleLog) println("Logger disabled, but the class was loaded. Please use the .enable() function and then re-initialise")
+ }
+ Settings.SetLogger(this)
+ }
+
+ //Various differ kinds lof loggers
+ public fun LogMessage(logMessage :Int, logData :String = "")
+ {
+ var loggedString = "["+DateFormat.format(Date())+"] "
+ when(logMessage) {
+ 1-> loggedString += "MISC: Test message, please ignore"
+ 2-> loggedString += "Cbox: Error while connecting to cbox.ws"
+ 3-> loggedString += "Logger: Could not re-enable the logger class - logging not initiated. Please reload with Logger.Reload"
+ 4-> loggedString += "SettingsManager: Deleting file"
+ 5-> loggedString += "SettingsManager: Folder successfully cleaned out"
+ 6-> loggedString += "SettingsManager: Compressing logs to Zip file"
+ 7-> loggedString += "SettingsManager: File Added"
+ 8-> loggedString += "SettingsManager: Folder successfully compressed"
+ 9-> loggedString += "SettingsManager: New settings loaded, please reload all services manually by running Global.Reload"
+ 10-> loggedString += "SettingsManager: Loaded and ready"
+ 11-> loggedString += "Unused error code"
+ 12-> loggedString += "Unused error code"
+ 13-> loggedString += "CLI: CLI is disabled, moving on"
+ 14-> loggedString += "CLI: Daemon started on port"
+ 15-> loggedString += "CLI: Client connected"
+ 16-> loggedString += "CLI: Client disconnected"
+ 17-> loggedString += "CLI: Command cancelled"
+ 18-> loggedString += "Logger: Disabled"
+ 19-> loggedString += "Logger: Enabled"
+ 20-> loggedString += "Logger: Archivation successful"
+ 21-> loggedString += "Logger: Log file location changed"
+ 22-> loggedString += "Logger: Disabled console logging"
+ 23-> loggedString += "Logger: Enabled console logging"
+ 24-> loggedString += "Logger: Disabled console logging"
+ 25-> loggedString += "Logger: Enabled console logging"
+ 26-> loggedString += "Logger: Restarting logger module"
+ 27-> loggedString += "Logger: Error while restarting, but still alive."
+ 28-> loggedString += "Logger: Restarted successfully"
+ 29-> loggedString += "CLI: CLI turned off through CLI mode."
+ 30-> loggedString += "Box: Loaded and ready"
+ 31-> loggedString += "Box: Initialising box module"
+ 32-> loggedString += "Box: Error while trying to get messages"
+ 33-> loggedString += "Box: Could not log in, going on in read-only mode. Use CLI or edit the config file to re-configure and try again."
+ 34-> loggedString += "Plugins: System started up"
+ 35-> loggedString += "Plugins: System disabled, moving on"
+ 36-> loggedString += "Box: Attempting to log in"
+ 37-> loggedString += "Box: Logging out"
+ 38-> loggedString += "ThreadController: CLI thread disconnected"
+ 39-> loggedString += "ThreadController: Box thread disconnected"
+ 40-> loggedString += "ThreadController: Plugin thread disconnected"
+ 41-> loggedString += "Box: Reloading module"
+ 42-> loggedString += "Box: Sending message"
+ 43-> loggedString += "Box: Logged in successfully"
+ 44-> loggedString += "Box: Something happened and now the user is no longer logged in. Please re-log."
+ 45-> loggedString += "Box: Daemon started"
+ 46-> loggedString += "ThreadController: Listening and waiting for commands"
+ 47-> loggedString += "ThreadController: CLI thread connected"
+ 48-> loggedString += "ThreadController: Plugin thread connected"
+ 49-> loggedString += "ThreadController: Box thread connected"
+ 50-> loggedString += "Box: Admin mode enabled"
+ 51-> loggedString += "Logger: Disabled chat logging"
+ 52-> loggedString += "Logger: Enabled chat logging"
+ 53-> loggedString += "Logger: Chat log file location changed"
+ 54-> loggedString += "CLI: Global restart initiated"
+ 55-> loggedString += "CLI: shutdown initiated"
+ 56-> loggedString += "MISC: Restarting module"
+ 57-> loggedString += "CLI: User entering CLI Chat mode"
+ 58-> loggedString += "CLI: User left CLI Chat mode"
+ 59-> loggedString += "CLI: Attempting a reload of the whole module"
+ 60-> loggedString += "CLI: Module stopped"
+ 61-> loggedString += "BOX: Module stopped"
+ 62-> loggedString += "Plugins: Module stopped"
+ 63-> loggedString += "Plugins: All plugins loaded"
+ 64-> loggedString += "Plugins: Attempting to reload the subsystem"
+ 65-> loggedString += "CLI: Failed to reload. CLI Disabled."
+ 66-> loggedString += "Plugins: Failed to reload. Plugins disabled."
+ 67-> loggedString += "Plugins: Disabling subsystem"
+ 68-> loggedString += "Plugins: Enabling subsystem"
+ 69-> loggedString += "Logger: plugins log file location changed"
+ 70-> loggedString += "Plugins: Failure in plugin loop"
+ else -> {
+ loggedString += "Error: Unspecified error ("+logMessage.toString()+")"
+ }
+ }
+ if (logData != "") {
+ loggedString += " ("+logData+")"
+ }
+ if(consoleLog) println(loggedString)
+ if(fileLog) log.appendText("\n"+loggedString, StandardCharsets.UTF_8)
+ }
+ public fun LogBoringMessage(logMessage :Int, logData :String = "")
+ {
+ if(logLevel > 1) {
+ var loggedString = "["+DateFormat.format(Date())+"] "
+ when(logMessage) {
+ 1-> loggedString += "SettingsManager: Updated settings and saved them to file"
+ 2-> loggedString += "SettingsManager: Changed setting"
+ 3-> loggedString += "SettingsManager: Settings not changed - no changes found"
+ else -> {
+ loggedString += "Error: Unspecified message ("+logMessage.toString()+")"
+ }
+ }
+ if (logData != "") {
+ loggedString += " ("+logData+")"
+ }
+ if(consoleLog) println(loggedString)
+ if(fileLog) log.appendText("\n"+loggedString, StandardCharsets.UTF_8)
+ }
+ }
+ public fun LogCommands(command :String, params :String = "")
+ {
+ var loggedString = "["+DateFormat.format(Date())+"] "
+ loggedString += "CLI: Received Command '"+command+"'"
+
+ if(params != "")loggedString += " with parameters ("+params+")"
+ if(consoleLog) println(loggedString)
+ if(fileLog) log.appendText("\n"+loggedString)
+ }
+ public fun LogChat(message: Message)
+ {
+ var loggedString = "{"+message.id+"} "
+ loggedString += "["+message.humanReadableDate+":("+message.postDate+")]"
+ loggedString += " <"+message.username+":"+message.userID+":"+message.avatarURL+">"
+ loggedString += " "+message.message
+ loggedString += " :("+message.userLevel+","+message.privLevel2+")(" + (if(message.userLevel == "1" && message.privLevel2 == "0") "guest" else if (message.userLevel == "2" && message.privLevel2 == "1") "user" else if (message.userLevel == "3" && message.privLevel2 == "2") "mod" else "unknown") + ")"
+
+ if(consoleLog) println(loggedString)
+ if(logChat) chatLog.appendText("\n"+loggedString)
+ }
+ public fun LogConnection(logMessage :Int, logData :String = "")
+ {
+ if(logLevel > 1)
+ {
+ var loggedString = "["+DateFormat.format(Date())+"] "
+ when(logMessage) {
+ 0-> loggedString += "Box: PING"
+ 1-> loggedString += "CBOX.WS: PONG"
+ 2-> loggedString += "CBOX.WS: NOPE"
+ 3-> loggedString += "WebManager: Doing a POST request"
+ 4-> loggedString += "WebManager: Doing a GET request"
+ else -> {
+ loggedString += "Unknown connection type"
+ }
+ }
+ if (logData != "") {
+ loggedString += " ("+logData+")"
+ }
+ if(consoleLog) println(loggedString)
+ if(fileLog) log.appendText("\n"+loggedString, StandardCharsets.UTF_8)
+ }
+ }
+ public fun LogPlugin(pluginName :String, pluginMessage :String)
+ {
+ if(this.logPlugins) {
+ var loggedString = "["+DateFormat.format(Date())+"] {" + pluginName + "}: " + pluginMessage
+ if(consoleLog) println(loggedString)
+ if(fileLog) pluginLog.appendText("\n"+loggedString, StandardCharsets.UTF_8)
+ }
+
+ }
+ public fun LogBoringPluginData(pluginName: String, pluginMessage: String)
+ {
+ if(logLevel > 1) this.LogPlugin(pluginName, pluginMessage)
+ }
+
+ //Setting changing API
+ public fun ChangeConsoleLoggingState(state :Boolean)
+ {
+ if(state == false) this.LogMessage(22)
+ this.consoleLog = state
+ if(state == true) this.LogMessage(23)
+ }
+ public fun ChangeFileLoggingState(state :Boolean)
+ {
+ if(state == false) this.LogMessage(24)
+ this.fileLog = state
+ if(state == true) this.LogMessage(25)
+ }
+ public fun ChangeChatLoggingState(state :Boolean)
+ {
+ if(state == false) this.LogMessage(51)
+ this.logChat = state
+ if(state == true) this.LogMessage(52)
+ }
+ public fun ChangeLogFile(fileName :String = "log.txt", folder :String = "logs")
+ {
+ loadedFile = true
+ logFolder = folder
+ logFileName = fileName
+ log = File(logFolder+"/"+logFileName)
+
+ this.LogMessage(21, folder+"/"+fileName)
+ }
+ public fun ChangeChatLogFile(fileName: String = "chat.txt", folder :String = "logs")
+ {
+ logFolder = folder
+ chatLogFile = fileName
+ chatLog = File(logFolder+"/"+logFileName)
+ this.LogMessage(53, folder+"/"+fileName)
+
+ }
+ public fun ChangePluginLogFile(fileName: String = "plugins.txt", folder :String = "logs")
+ {
+ logFolder = folder
+ pluginLogFile = fileName
+ pluginLog = File(logFolder+"/"+pluginLogFile)
+ this.LogMessage(69, folder+"/"+fileName)
+
+ }
+ public fun Disable(file :Boolean = true, console: Boolean = true, chat :Boolean = true)
+ {
+ this.LogMessage(18)
+ enableStatus = false
+ if(file)fileLog = false;
+ if(console)consoleLog = false;
+ if(chat)logChat = false;
+ }
+ public fun Enable(File :Boolean = true, Console: Boolean = true, chat :Boolean = true)
+ {
+ enableStatus = true
+ if(File && loadedFile)fileLog = true;
+ else if (!loadedFile && Console) this.LogMessage(3)
+ if(Console)consoleLog = true;
+ if(chat)logChat = true;
+ this.LogMessage(19)
+ }
+ public fun ArchiveOldLogs(log :Boolean = true)
+ {
+ val appZip = ZipUtil(logFolder, (System.currentTimeMillis() / 1000L).toString()+"_logs.zip", Settings.GetSetting("logFile"), Settings.GetSetting("chatLogFile"), Settings.GetSetting("pluginLogFile"), if (log) this else null)
+ appZip.generateFileList()
+ appZip.zipIt()
+ //Wipe the folder
+ appZip.clearOriginalFolder()
+ this.LogMessage(20)
+ }
+ public fun Reload() :Boolean
+ {
+ this.LogMessage(26)
+ this.logFolder :String = Settings.GetSetting("logFolder")
+ this.logFileName :String = Settings.GetSetting("logFile")
+ this.chatLogFile :String = Settings.GetSetting("chatLogFile")
+ this.pluginLogFile :String = Settings.GetSetting("pluginLogFile")
+ this.consoleLog :Boolean = Settings.GetSetting("consoleLog").toBoolean()
+ this.fileLog :Boolean = Settings.GetSetting("fileLog").toBoolean()
+ this.logChat :Boolean = Settings.GetSetting("logChat").toBoolean()
+ this.logPlugins :Boolean = Settings.GetSetting("enablePlugins").toBoolean()
+
+ this.log = File(logFolder+"/"+logFileName)
+ this.chatLog = File(logFolder+"/"+chatLogFile)
+ this.pluginLog = File(logFolder+"/"+pluginLogFile)
+ if(this.loginit()) {
+ this.loadedFile = true
+ this.LogMessage(28)
+ return true
+ } else {
+ this.LogMessage(27)
+ return false
+ }
+ }
+
+ //Initializer function, seperated so that it can be properly called from Reload()
+ private fun loginit() :Boolean
+ {
+ try {
+ if(logRotate) {
+ var shouldArchive = false
+ if(log.exists()) {
+ //copy old logs
+ var tmpLogLocation = "";
+ for(i in 1..maxLogs) {
+ val tmpLog = File(logFolder+"/log."+i.toString())
+ if(!tmpLog.exists()) {
+ tmpLogLocation = tmpLog.toString()
+ break;
+ }
+ }
+
+ if (tmpLogLocation == "") {
+ //Archive old logs
+ log.copyTo(File(logFolder+"/log."+maxLogs.toString()))
+ shouldArchive = true
+ } else {
+ //Copy old log to a new place and delete it afterwards
+ log.copyTo(File(tmpLogLocation))
+ }
+ log.delete()
+ } else {
+ File(logFolder).mkdirs()
+ }
+
+ if(chatLog.exists()) {
+ var tmpLogLocation = ""
+ for (i in 1..maxLogs) {
+ val tmpLog = File(logFolder+"/chat."+i.toString())
+ if(!tmpLog.exists()) {
+ tmpLogLocation = tmpLog.toString()
+ break;
+ }
+ }
+
+ if (tmpLogLocation == "") {
+ //Archive old logs
+ chatLog.copyTo(File(logFolder+"/chat."+maxLogs.toString()))
+ shouldArchive = true
+ } else {
+ //Copy old log to a new place and delete it afterwards
+ chatLog.copyTo(File(tmpLogLocation))
+ }
+ chatLog.delete()
+ }
+
+ if(pluginLog.exists()) {
+ var tmpLogLocation = ""
+ for (i in 1..maxLogs) {
+ val tmpLog = File(logFolder+"/plugins."+i.toString())
+ if(!tmpLog.exists()) {
+ tmpLogLocation = tmpLog.toString()
+ break;
+ }
+ }
+
+ if (tmpLogLocation == "") {
+ //Archive old logs
+ chatLog.copyTo(File(logFolder+"/plugins."+maxLogs.toString()))
+ shouldArchive = true
+ } else {
+ //Copy old log to a new place and delete it afterwards
+ chatLog.copyTo(File(tmpLogLocation))
+ }
+ pluginLog.delete()
+ }
+
+ if(shouldArchive && logArchiving) this.ArchiveOldLogs(false)
+ }
+ } catch (e: Exception) {
+ return false
+ }
+ return true
+ }
+}
+
diff --git a/src/META-INF/MANIFEST.MF b/src/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..2c1e82a
--- /dev/null
+++ b/src/META-INF/MANIFEST.MF
@@ -0,0 +1,3 @@
+Manifest-Version: 1.0
+Main-Class: AppKt
+
diff --git a/src/Plugins.kt b/src/Plugins.kt
new file mode 100644
index 0000000..c95cb03
--- /dev/null
+++ b/src/Plugins.kt
@@ -0,0 +1,211 @@
+/**
+ * Class that manages anything not directly related to cbox.ws
+ * Created by zingmars on 03.10.2015.
+ */
+import BotPlugins.BasePlugin
+import java.io.File
+import java.net.URL
+import java.net.URLClassLoader
+import java.util.*
+
+public class Plugins(private val Settings :Settings, private val Logger :Logger, private val ThreadController :ThreadController)
+{
+ private var active = false
+ private var enabled = Settings.GetSetting("enablePlugins").toBoolean()
+ private var pluginDirectory = Settings.GetSetting("pluginDirectory")
+ private var Daemon = Thread()
+ private var pluginSettings = Settings("PluginSettings.cfg", true)
+ private val pluginList :MutableList = ArrayList()
+ private val plugins = ArrayList()
+ private var refreshRate = Settings.GetSetting("refreshRate").toLong()
+ public var username = ""
+
+ init
+ {
+ if(enabled) {
+ if(Settings.checkSetting("username")) username = Settings.GetSetting("username")
+ if(!Settings.checkSetting("isAdmin")) Settings.SetSetting("isAdmin", "false")
+
+ pluginSettings.SetLogger(Logger)
+ this.start()
+ Logger.LogMessage(34)
+ } else {
+ Logger.LogMessage(35)
+ }
+ }
+
+ //Daemon functions
+ public fun isActive() :Boolean
+ {
+ return active
+ }
+ public fun start(restarted :Boolean = false)
+ {
+ if(enabled) {
+ this.active = true
+ Daemon = Thread(Runnable () { this.Demon() })
+ Daemon.isDaemon = true
+ reloadAllPlugins()
+ Daemon.start()
+ ThreadController.ConnectPlugins(Daemon)
+ if(restarted) ThreadController.AddToCLIBuffer("Plugin container restarted successfully")
+ Logger.LogMessage(63)
+ }
+ }
+ public fun stop()
+ {
+ this.active = false
+ for(plugin in plugins) {
+ unloadPlugin(plugin.pluginName.toString())
+ }
+ ThreadController.DisconnectPlugins()
+ Daemon.stop()
+ Logger.LogMessage(62)
+ }
+ public fun Reload() :Boolean
+ {
+ try {
+ Logger.LogMessage(64)
+ this.stop()
+
+ reloadAllPlugins()
+ refreshRate = Settings.GetSetting("refreshRate").toLong()
+
+ if(this.enabled) {
+ this.start(true)
+ return true
+ } else return false
+ } catch (e: Exception) {
+ this.active = false
+ this.enabled = false
+ Logger.LogMessage(66)
+ return false
+ }
+ }
+ public fun Disable()
+ {
+ Logger.LogMessage(67)
+ enabled = false
+ this.stop()
+ }
+ public fun Enable()
+ {
+ Logger.LogMessage(68)
+ enabled = true
+ this.start()
+ }
+ private fun Demon()
+ {
+ while (active) {
+ try {
+ Thread.sleep(this.refreshRate)
+ //Run data to plugins
+ for (message in ThreadController.GetPluginBuffer()) {
+ //run through loaded plugin's checklist
+ if(message.userName != username) {
+ for(plugin in plugins) {
+ plugin.connector(message)
+ }
+ }
+ }
+ pluginSettings.SaveSettings()
+ } catch (e: Exception) {
+ Logger.LogMessage(70, e.toString())
+ }
+ }
+ }
+
+ //Plugin management
+ public fun unloadPlugin(name :String) :String
+ {
+ // We need to go through all of the plugins since we don't keep an index :(
+ var i = 0
+ for (plugin in plugins) {
+ if(name.toLowerCase() == plugin.pluginName?.toLowerCase())
+ {
+ plugin.stop()
+ plugins.remove(i)
+ Logger.LogPlugin(plugin.pluginName.toString(), "Unloaded.")
+ return "Success"
+ }
+ i++
+ }
+ return "Plugin not found or already disabled."
+ }
+ public fun LoadPlugin(name :String) :Int?
+ {
+ var source = pluginDirectory+name
+ var pluginFile = File(source)
+
+ try {
+ var fileURL = ArrayList()
+ fileURL.add(pluginFile.toURI().toURL())
+
+ var cl = URLClassLoader(fileURL.toTypedArray())
+ var pluginName = name.substring(0, name.lastIndexOf(".")).replace("\\", "")
+ var cls = Class.forName("BotPlugins."+pluginName, true, cl)
+
+ var instance = cls.newInstance() as BasePlugin
+ if (instance.initiate(pluginSettings, Logger, this, ThreadController, pluginName)) {
+ plugins.add(instance)
+ return plugins.size()-1
+ }
+ return null
+ } catch (e: Exception) {
+ Logger.LogMessage(999, e.toString())
+ return null
+ }
+ }
+ public fun reloadPlugin(name :String)
+ {
+ this.unloadPlugin(name)
+ this.LoadPlugin(name+".kt")
+ }
+ private fun reloadAllPlugins()
+ {
+ pluginList.clear()
+ plugins.clear()
+ generateFileList()
+
+ for(plugin in pluginList) {
+ this.LoadPlugin(plugin)
+ }
+ }
+
+ //CLI API
+ public fun changeRefreshRate(newRate :Long)
+ {
+ this.refreshRate = newRate
+ }
+ public fun SavePluginSettings()
+ {
+ pluginSettings.SaveSettings()
+ }
+ public fun SetSetting(setting :String, value :String)
+ {
+ pluginSettings.SetSetting(setting, value)
+ }
+ public fun GetAllSettings() :String
+ {
+ return pluginSettings.GetAllSettings()
+ }
+
+ //Misc
+ private fun generateFileList(node: File = File(pluginDirectory))
+ {
+ if (node.isFile) {
+ pluginList.add(node.toString().substring(pluginDirectory.length(), node.toString().length()))
+ }
+
+ if (node.isDirectory) {
+ val subNote = node.list()
+ for (filename in subNote) {
+ generateFileList(File(node, filename))
+ }
+ }
+ }
+ public fun isAdmin() :Boolean
+ {
+ return Settings.GetSetting("isAdmin").toBoolean()
+ }
+}
\ No newline at end of file
diff --git a/src/Settings.kt b/src/Settings.kt
new file mode 100644
index 0000000..e2eb21b
--- /dev/null
+++ b/src/Settings.kt
@@ -0,0 +1,179 @@
+/**
+ * Class to manage settings loading, editing, saving
+ * Created by zingmars on 02.10.2015.
+ */
+import java.util.*
+import java.io.File
+
+public class Settings(private var settingsFileName :String = "settings.cfg", private var empty :Boolean = false)
+{
+ private val settingsContainer = Properties()
+ private var settingsFile = File(settingsFileName)
+ private var logger :Logger? = null
+ private var changed :Boolean = false
+
+ init
+ {
+ // Check if the settings file exists. If not, create one.
+ if (!settingsFile.exists() && !empty) {
+ settingsFile.createNewFile()
+ //Fill default values
+ settingsContainer.set("maxLogs", "100")
+ settingsContainer.set("logFolder", "logs")
+ settingsContainer.set("logFile", "log.txt")
+ settingsContainer.set("chatLogFile", "chat.txt")
+ settingsContainer.set("pluginLogFile", "plugins.txt")
+ settingsContainer.set("fileLog", "true")
+ settingsContainer.set("logChat", "true")
+ settingsContainer.set("consoleLog", "true")
+ settingsContainer.set("logLevel", "1")
+ settingsContainer.set("pluginDirectory", "BotPlugins")
+ settingsContainer.set("logRotate", "true")
+
+ var input :String?
+ println("Settings file not found/not configured, please enter the following details")
+
+ // CLI daemon configuration
+ println("Enable CLI daemon? (true):")
+ input = readLine()
+ if(input == "") input = "true"
+ settingsContainer.set("daemonEnabled", input)
+
+ if(input == "true") {
+ println("Enter daemon port (9970):")
+ input = readLine()
+ if(input == "") input = "9970"
+ settingsContainer.set("daemonPort", input)
+ } else {
+ settingsContainer.set("daemonPort", "9970")
+ }
+
+ // Log Archiving
+ println("Enable log archiving? [true/false] (true):")
+ input = readLine()
+ if(input == "") input = "true"
+ settingsContainer.set("archiveLogs", input)
+
+ // Plugins
+ // Log Archiving
+ println("Enable plugins? [true/false] (true):")
+ input = readLine()
+ if(input == "") input = "true"
+ settingsContainer.set("enablePlugins", input)
+
+ // cbox.ws connection configuration
+ println("cbox.ws server (0):")
+ input = readLine()
+ if(input == "") input = "0"
+ settingsContainer.set("server", input)
+
+ println("Box ID (0):")
+ input = readLine()
+ if(input == "") input = "0"
+ settingsContainer.set("boxid", input)
+
+ println("Box tag (0):")
+ input = readLine()
+ if(input == "") input = "0"
+ settingsContainer.set("boxtag", input)
+
+ println("Box refresh rate in miliseconds (5000):")
+ input = readLine()
+ if(input == "") input = "5000"
+ settingsContainer.set("refreshRate", input)
+
+ println("Username (blank for no login): ")
+ input = readLine()
+ settingsContainer.set("username", input)
+ if(input == "") {
+ settingsContainer.set("avatar", input)
+ settingsContainer.set("password", input)
+ } else {
+ println("Avatar URL: ")
+ input = readLine()
+ settingsContainer.set("avatar", input)
+
+ println("Password - WARNING: If you enter a password, please ensure that the settings file has proper privileges since it is available in plain text: ")
+ input = readLine()
+ settingsContainer.set("password", input)
+ }
+
+ changed = true
+ this.SaveSettings()
+ }
+
+ //Load the settings file using Java's properties lib
+ this.LoadSettings()
+ }
+
+ // Get
+ public fun GetSetting (setting :String) :String
+ {
+ try {
+ return settingsContainer.getProperty(setting)
+ }
+ catch (e:Exception) {
+ return ""
+ }
+ }
+ public fun checkSetting (setting :String, create :Boolean = false) :Boolean
+ {
+ try {
+ settingsContainer.getProperty(setting)
+ return true
+ } catch (e: Exception){
+ if(create) {
+ this.SetSetting(setting, "")
+ return true
+ }
+ return false
+ }
+ }
+ public fun LoadSettings ()
+ {
+ if(!settingsFile.exists()) settingsFile.createNewFile()
+
+ settingsContainer.load(settingsFile.inputStream())
+ logger?.LogMessage(9)
+ }
+ public fun GetAllSettings () :String
+ {
+ var output = ""
+ var keys = settingsContainer.keySet().iterator()
+ while(keys.hasNext()) {
+ var key = keys.next().toString()
+ var data = settingsContainer.get(key)
+ output += key + ":" + data + "\n"
+ }
+ return output
+ }
+
+ // Set / Save
+ public fun SetSetting (setting :String, value :String)
+ {
+ settingsContainer.setProperty(setting, value)
+ if(setting != "password") logger?.LogBoringMessage(2, setting+":"+value)
+ changed = true
+ }
+ public fun SaveSettings ()
+ {
+ if(changed) {
+ settingsContainer.store(settingsFile.outputStream(), "CBox Bot settings file. Please don't touch unless you know specifically what to change, else you might cause a crash.")
+ logger?.LogBoringMessage(1)
+ } else {
+ logger?.LogBoringMessage(3)
+ }
+ }
+
+ // API (management) functions
+ public fun SetLogger(newLogger :Logger?)
+ {
+ logger = newLogger
+ logger?.LogMessage(10)
+ }
+ public fun ChangeSettingsFile( FileName :String )
+ {
+ settingsFile = File(FileName)
+ this.LoadSettings()
+ }
+}
\ No newline at end of file
diff --git a/src/ThreadController.kt b/src/ThreadController.kt
new file mode 100644
index 0000000..5bd74a7
--- /dev/null
+++ b/src/ThreadController.kt
@@ -0,0 +1,101 @@
+import Containers.PluginBufferItem
+import java.util.*
+
+/**
+ * Simple class to theoretically control threads from one place
+ * in reality it's basically where all meet to exchange information
+ * Created by zingmars on 03.10.2015.
+ */
+public class ThreadController(private val Logger :Logger)
+{
+ private var CLI :Thread? = null
+ private var BOX :Thread? = null
+ private var PLUGINS :Thread? = null
+
+ private var CLIBuffer= ArrayList()
+ private var BoxBuffer= ArrayList()
+ private var PluginBuffer = ArrayList()
+ private var MainThreadBuffer = ArrayList()
+
+ init
+ {
+ Logger.LogMessage(46)
+ }
+
+ // Connect threads
+ public fun ConnectCLI(CLI :Thread)
+ {
+ Logger.LogMessage(47)
+ this.CLI = CLI
+ }
+ public fun ConnectBox(BOX :Thread)
+ {
+ Logger.LogMessage(49)
+ this.BOX = BOX
+ }
+ public fun ConnectPlugins(PLUGINS :Thread)
+ {
+ Logger.LogMessage(48)
+ this.PLUGINS = PLUGINS
+ }
+
+ // Disconnect threads
+ public fun DisconnectCLI()
+ {
+ Logger.LogMessage(38)
+ CLI = null
+ }
+ public fun DisconnectBox()
+ {
+ Logger.LogMessage(39)
+ BOX = null
+ }
+ public fun DisconnectPlugins()
+ {
+ Logger.LogMessage(40)
+ PLUGINS = null
+ }
+
+ // Inter-thread communication
+ public fun AddToCLIBuffer(data :String)
+ {
+ if(CLI != null) CLIBuffer.add(data)
+ }
+ public fun AddToMainBuffer(data :String)
+ {
+ MainThreadBuffer.add(data)
+ }
+ public fun AddToBoxBuffer(data :String)
+ {
+ if(BOX != null) BoxBuffer.add(data)
+ }
+ public fun AddToPluginBuffer(data :PluginBufferItem)
+ {
+ if(PLUGINS != null) PluginBuffer.add(data)
+ }
+
+ public fun GetMainBuffer(clear :Boolean = true) :ArrayList
+ {
+ var buffer = ArrayList(MainThreadBuffer)
+ if(clear) MainThreadBuffer.clear()
+ return buffer
+ }
+ public fun GetCLIBuffer(clear :Boolean = true) :ArrayList
+ {
+ var buffer = ArrayList(CLIBuffer)
+ if(clear) CLIBuffer.clear()
+ return buffer
+ }
+ public fun GetBoxBuffer(clear :Boolean = true) :ArrayList
+ {
+ var buffer = ArrayList(BoxBuffer)
+ if(clear) BoxBuffer.clear()
+ return buffer
+ }
+ public fun GetPluginBuffer(clear :Boolean = true) :ArrayList
+ {
+ var buffer = ArrayList(PluginBuffer)
+ if(clear) PluginBuffer.clear()
+ return buffer
+ }
+}
\ No newline at end of file
diff --git a/src/ZipUtil.kt b/src/ZipUtil.kt
new file mode 100644
index 0000000..4b1945f
--- /dev/null
+++ b/src/ZipUtil.kt
@@ -0,0 +1,108 @@
+/**
+ * Created by kark on 12.04.2013.
+ * Code found https://stackoverflow.com/a/15970455
+ * Kotlinified and modified for this project by zingmars on 01.10.2015
+ */
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.io.IOException
+import java.util.ArrayList
+import java.util.zip.ZipEntry
+import java.util.zip.ZipOutputStream
+
+public class ZipUtil(private val SOURCE_FOLDER :String, private val OUTPUT_ZIP_FILE :String, private val ActiveFile :String, private val ActiveFile2 :String, private val ActiveFile3 :String, private val Logger :Logger? = null)
+{
+ private val fileList: MutableList
+
+ init
+ {
+ fileList = ArrayList()
+ }
+
+ public fun getFileList(): MutableList
+ {
+ return fileList
+ }
+ public fun clearOriginalFolder()
+ {
+ var source :String
+ try {
+ source = SOURCE_FOLDER.substring(SOURCE_FOLDER.lastIndexOf("\\") + 1, SOURCE_FOLDER.length())
+ } catch (e: Exception) {
+ source = SOURCE_FOLDER
+ }
+ for (file in fileList) {
+ Logger?.LogMessage(4, file)
+ if(file != ActiveFile && file != ActiveFile2 && file != ActiveFile3) File(source+file).delete()
+ }
+ Logger?.LogMessage(5)
+ }
+ public fun zipIt(zipFile: String = OUTPUT_ZIP_FILE)
+ {
+ val buffer = ByteArray(1024)
+ var source :String
+ var fos: FileOutputStream?
+ var zos: ZipOutputStream? = null
+ try {
+ try {
+ source = SOURCE_FOLDER.substring(SOURCE_FOLDER.lastIndexOf("\\") + 1, SOURCE_FOLDER.length())
+ } catch (e: Exception) {
+ source = SOURCE_FOLDER
+ }
+
+ fos = FileOutputStream(zipFile)
+ zos = ZipOutputStream(fos)
+
+ Logger?.LogMessage(6, zipFile)
+ var `in`: FileInputStream? = null
+
+ for (file in this.fileList) {
+ Logger?.LogMessage(7, file)
+ val ze = ZipEntry(source + File.separator + file)
+ zos.putNextEntry(ze)
+ try {
+ `in` = FileInputStream(SOURCE_FOLDER + File.separator + file)
+ var len: Int =`in`.read(buffer)
+ while (len > 0) {
+ zos.write(buffer, 0, len)
+ len = `in`.read(buffer)
+ }
+ } finally {
+ `in`?.close()
+ }
+ }
+
+ zos.closeEntry()
+ Logger?.LogMessage(8)
+
+ } catch (ex: IOException) {
+ ex.printStackTrace()
+ } finally {
+ try {
+ zos!!.close()
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+
+ }
+ }
+ public fun generateFileList(node: File = File(SOURCE_FOLDER))
+ {
+ // add file only
+ if (node.isFile) {
+ fileList.add(generateZipEntry(node.toString()))
+ }
+
+ if (node.isDirectory) {
+ val subNote = node.list()
+ for (filename in subNote) {
+ generateFileList(File(node, filename))
+ }
+ }
+ }
+ private fun generateZipEntry(file: String): String
+ {
+ return file.substring(SOURCE_FOLDER.length(), file.length())
+ }
+}
\ No newline at end of file
diff --git a/src/app.kt b/src/app.kt
new file mode 100644
index 0000000..0d22d77
--- /dev/null
+++ b/src/app.kt
@@ -0,0 +1,53 @@
+/**
+ * Overengineered cbox.ws bot for OverTheGun.org
+ * Mostly a side project to learn kotlin
+ * Created by zingmars on 29.09.2015.
+ */
+import java.util.*
+
+fun main(args: Array)
+{
+ //Change the default time zone to UTC to avoid outputting local time.
+ TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
+
+ // Load and set overengineered settings
+ var SettingsFile = "settings.cfg"
+ if(!args.isEmpty()) SettingsFile = args[0]
+ var Settings = Settings(SettingsFile)
+
+ // Load an overengineered logging system
+ val Logger = Logger(Settings)
+
+ // Thread controller
+ val ThreadController = ThreadController(Logger)
+
+ // Initiate an instance of a cbox connection
+ val Box = Box(Settings, Logger, ThreadController)
+
+ // Load a plugin loader system
+ val Plugins = Plugins(Settings, Logger, ThreadController)
+
+ // Load an overengineered CLI access system
+ val CLI = CLI(Settings, Logger, Box, Plugins, ThreadController)
+
+ var refreshRate = Settings.GetSetting("refreshRate").toLong()
+ while (Box.isActive() || CLI.isActive() || Plugins.isActive()) {
+ Thread.sleep(refreshRate)
+ //General commands. Feel free to extend with your own stuff.
+ for(command in ThreadController.GetMainBuffer()) {
+ var splitCommand = command.split(",")
+ when (splitCommand[0]) {
+ "Restart" -> {
+ Logger.LogMessage(56, splitCommand[1])
+ when(splitCommand[1]) {
+ "Box" -> Box.Reload()
+ "Plugins" -> Plugins.Reload()
+ "Logger" -> Logger.Reload()
+ "CLI" -> CLI.Reload()
+ }
+ }
+ }
+ }
+ }
+ System.exit(1337) // Kill all rogue plugin daemons and whatnot
+}
\ No newline at end of file