Skip to content
This repository was archived by the owner on Aug 18, 2020. It is now read-only.

Adding RCON Connector #68

Merged
merged 18 commits into from
Nov 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@ What if you could **combine** the power of
with your interactive chat in your livestream. What if you could easily react on events, e.g.

- Automatically **share** your new subscribers on twitter
- Automatically **control** your studio's lighting colors trough chat messages
- Automatically **control** your studio's lighting colors through chat messages
- Automatically **post** an user's cheer on your minecraft server
- Automatically **upload** a youtube video with stream highlights when your stream stops

and so much more. We know, there is [IFTTT](https://ifttt.com/). But sometimes, building blocks are to generic and services not optimized for your streaming environment.
and so much more. We know, there is [IFTTT](https://ifttt.com/). But sometimes, building blocks are to generic and services aren't optimized for your streaming environment.

The alternative: Develop everything by yourself and waste hundreds of hours with API-integration. We already solved this problem for you. This is **Chat Overflow**.

## The ChatOverflow Project

**Chat Overflow** is a plugin framework, which offers ready-to-use platform integrations for all* major streaming- and social-media-sites.

**Chat Overflow** enables you to to level up your stream with by writing simple, platform-independent plugins in java or scala**.
**Chat Overflow** enables you to to level up your stream by writing simple, platform-independent plugins in java or scala**.

It's getting even better: The **Chat Overflow** license allows you to sell your custom plugins, creating new services for other streamers.

Expand All @@ -39,22 +39,22 @@ And it's so easy. Here is all the code to get started with a simple twitch chat

\* There are still missing platforms. This is a open-source project. You can [help](https://github.com/codeoverflow-org/chatoverflow/issues), too!

\** The API is written in java. So, every JVM-compatible language is possible. Java, Scala, Kotlin, ...
\** The API is written in java. So, every JVM-compatible language is possible. Java, Scala, Kotlin, etc.

### Installation / Releases
Head over to [releases](https://github.com/codeoverflow-org/chatoverflow/releases).

Just download the newest zip file, make sure that java is installed and launch the framework.

Note, that you'll have to develop your own plugins or search for plugins online (e.g. on our [Discord Server](https://discord.gg/p2HDsme)). **Chat Overflow** is only the framework.
Note that you'll have to develop your own plugins or search for plugins online (e.g. on our [Discord Server](https://discord.gg/p2HDsme)). **Chat Overflow** is only the framework.

### Development

Start with the [Installation](https://github.com/codeoverflow-org/chatoverflow/wiki/Installation). Then learn more about the [CLI](https://github.com/codeoverflow-org/chatoverflow/wiki/Using-the-CLI).

Please see the wiki to learn how to code new [platform sources](https://github.com/codeoverflow-org/chatoverflow/wiki/Adding-a-new-platform-source) and new [plugins](https://github.com/codeoverflow-org/chatoverflow/wiki/Writing-a-plugin).
Please consult the wiki to learn how to code new [platform sources](https://github.com/codeoverflow-org/chatoverflow/wiki/Adding-a-new-platform-source) and new [plugins](https://github.com/codeoverflow-org/chatoverflow/wiki/Writing-a-plugin).

***Pre-Alpha note***: Please note, that the development workflow and the documentation will be updated soon.
***Pre-Alpha note***: Please note that the development workflow and the documentation will be updated soon.

### Discord

Expand Down
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// ---------------------------------------------------------------------------------------------------------------------

name := "ChatOverflow"
version := "0.2"
version := "0.2.1"
mainClass := Some("org.codeoverflow.chatoverflow.Launcher")

// One version for all sub projects. Use "retrieveManaged := true" to download and show all library dependencies.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,15 @@ abstract class EventInputImpl[T <: Event, C <: Connector](implicit ctc: ClassTag
handlers.filter(handler => handler.clazz == cts.runtimeClass)
.foreach(handler => handler.consumer.asInstanceOf[Consumer[S]].accept(event))
}

override def shutdown(): Boolean = {
if (sourceConnector.isDefined) {
val stopped = stop()
handlers.clear()
stopped & sourceConnector.get.shutdown()
} else {
logger warn "Source connector not set."
false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ class DiscordChatConnector(override val sourceIdentifier: String) extends Connec
def addReactionDelEventListener(listener: MessageReactionRemoveEvent => Unit): Unit =
discordChatListener.addReactionDelEventListener(listener)

def removeMessageReceivedListener(listener: MessageReceivedEvent => Unit): Unit =
discordChatListener.removeMessageReceivedListener(listener)

def removeMessageUpdateListener(listener: MessageUpdateEvent => Unit): Unit =
discordChatListener.removeMessageUpdateEventListener(listener)

def removeMessageDeleteListener(listener: MessageDeleteEvent => Unit): Unit =
discordChatListener.removeMessageDeleteEventListener(listener)

def removeReactionAddEventListener(listener: MessageReactionAddEvent => Unit): Unit =
discordChatListener.removeReactionAddEventListener(listener)

def removeReactionDelEventListener(listener: MessageReactionRemoveEvent => Unit): Unit =
discordChatListener.removeReactionDelEventListener(listener)

/**
* Connects to discord
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ class DiscordChatListener extends EventListener {

def addReactionDelEventListener(listener: MessageReactionRemoveEvent => Unit): Unit = reactionDelEventListener += listener

def removeMessageReceivedListener(listener: MessageReceivedEvent => Unit): Unit = messageEventListener -= listener

def removeMessageUpdateEventListener(listener: MessageUpdateEvent => Unit): Unit = messageUpdateEventListener -= listener

def removeMessageDeleteEventListener(listener: MessageDeleteEvent => Unit): Unit = messageDeleteEventListener -= listener

def removeReactionAddEventListener(listener: MessageReactionAddEvent => Unit): Unit = reactionAddEventListener -= listener

def removeReactionDelEventListener(listener: MessageReactionRemoveEvent => Unit): Unit = reactionDelEventListener -= listener

override def onEvent(event: Event): Unit = {
event match {
case receivedEvent: MessageReceivedEvent => messageEventListener.foreach(listener => listener(receivedEvent))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,18 @@ class DiscordChatInputImpl extends EventInputImpl[DiscordEvent, DiscordChatConne
private val privateMessages = ListBuffer[DiscordChatMessage]()
private var channelId: Option[String] = None

private val onMessageFn = onMessage _
private val onMessageUpdateFn = onMessageUpdate _
private val onMessageDeleteFn = onMessageDelete _
private val onReactionAddedFn = onReactionAdded _
private val onReactionRemovedFn = onReactionRemoved _

override def start(): Boolean = {
sourceConnector.get.addMessageReceivedListener(onMessage)
sourceConnector.get.addMessageUpdateListener(onMessageUpdate)
sourceConnector.get.addMessageDeleteListener(onMessageDelete)
sourceConnector.get.addReactionAddEventListener(onReactionAdded)
sourceConnector.get.addReactionDelEventListener(onReactionRemoved)
sourceConnector.get.addMessageReceivedListener(onMessageFn)
sourceConnector.get.addMessageUpdateListener(onMessageUpdateFn)
sourceConnector.get.addMessageDeleteListener(onMessageDeleteFn)
sourceConnector.get.addReactionAddEventListener(onReactionAddedFn)
sourceConnector.get.addReactionDelEventListener(onReactionRemovedFn)
true
}

Expand Down Expand Up @@ -81,7 +87,14 @@ class DiscordChatInputImpl extends EventInputImpl[DiscordEvent, DiscordChatConne
*
* @return true if stopping was successful
*/
override def stop(): Boolean = true
override def stop(): Boolean = {
sourceConnector.get.removeMessageReceivedListener(onMessageFn)
sourceConnector.get.removeMessageUpdateListener(onMessageUpdateFn)
sourceConnector.get.removeMessageDeleteListener(onMessageDeleteFn)
sourceConnector.get.removeReactionAddEventListener(onReactionAddedFn)
sourceConnector.get.removeReactionDelEventListener(onReactionRemovedFn)
true
}

/**
* Listens for received messages, parses the data, adds them to the buffer and handles them over to the correct handler
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package org.codeoverflow.chatoverflow.requirement.service.rcon

import java.io.{DataInputStream, IOException, InputStream, OutputStream}
import java.net.{Socket, SocketException}
import java.nio.{ByteBuffer, ByteOrder}
import java.util.Random

import org.codeoverflow.chatoverflow.WithLogger
import org.codeoverflow.chatoverflow.connector.Connector

class RconConnector(override val sourceIdentifier: String) extends Connector(sourceIdentifier) with WithLogger {
override protected var requiredCredentialKeys: List[String] = List("password", "address")
override protected var optionalCredentialKeys: List[String] = List("port")

private var socket: Socket = _
private var outputStream: OutputStream = _
private var inputStream: InputStream = _
private var requestId: Int = 0

def sendCommand(command: String): String = {
logger debug s"Sending $command to RCON"
requestId += 1
if (write(2, command.getBytes("ASCII"))) {
return read()
}
null
}


/**
* Starts the connector, e.g. creates a connection with its platform.
*/
override def start(): Boolean = {
logger info s"Starting rcon connection to ${credentials.get.getValue("address").get}"
var port: Int = 25575
if (credentials.get.exists("port")) {
try{
port = credentials.get.getValue("port").get.toInt
} catch {
case e: NumberFormatException => {
logger error "Please enter a valid port"
return false
}
}
if (port < 1 || port > 65535) {
logger error "Please enter a valid port"
return false
}
}
try {
socket = new Socket(credentials.get.getValue("address").get, port)
socket.setKeepAlive(true)
outputStream = socket.getOutputStream
inputStream = socket.getInputStream
} catch {
case e: IOException => {
logger error "No Connection to RCON Server. Is it up?"
return false
}
}
val loggedIn = login()
// Sleeping here to allow the (minecraft) server to start its own rcon procedure. Otherwise it caused errors in my tests.
Thread.sleep(5000)
loggedIn
}

private def login(): Boolean = {
requestId = new Random().nextInt(Integer.MAX_VALUE)
logger info "Logging RCON in..."
val password = credentials.get.getValue("password").get
if (write(3, password.getBytes("ASCII"))) {
if (read() == null) {
logger error "Could not log in to RCON Server. Password is Wrong!"
return false
} else {
logger debug "Login to RCON was successful"
return true
}
}
false
}

private def write(packageType: Int, payload: Array[Byte]): Boolean = {
try {
val length = 4 + 4 + payload.length + 1 + 1
var byteBuffer: ByteBuffer = ByteBuffer.allocate(length + 4)
byteBuffer.order(ByteOrder.LITTLE_ENDIAN)

byteBuffer.putInt(length)
byteBuffer.putInt(requestId)
byteBuffer.putInt(packageType)
byteBuffer.put(payload)
byteBuffer.put(0x00.toByte)
byteBuffer.put(0x00.toByte)

outputStream.write(byteBuffer.array())
outputStream.flush()
} catch {
case e: SocketException => {
logger error "Connection Error to RCON Server. This request will not be sended!"
return false
}
}
true
}

private def read(): String = {
try {
val header: Array[Byte] = Array.ofDim[Byte](4*3)
inputStream.read(header)
val headerBuffer: ByteBuffer = ByteBuffer.wrap(header)
headerBuffer.order(ByteOrder.LITTLE_ENDIAN)
val length = headerBuffer.getInt()
val packageType = headerBuffer.getInt
val payload: Array[Byte] = Array.ofDim[Byte](length - 4 - 4 - 2)
val dataInputStream: DataInputStream = new DataInputStream(inputStream)
dataInputStream.readFully(payload)
dataInputStream.read(Array.ofDim[Byte](2))
if (packageType == -1) {
return null
}
new String(payload, "ASCII")
} catch {
case e: NegativeArraySizeException => null;
}
}

/**
* This stops the activity of the connector, e.g. by closing the platform connection.
*/
override def stop(): Boolean = {
logger info s"Stopped RCON connector to ${credentials.get.getValue("address").get}!"
socket.close()
true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.codeoverflow.chatoverflow.requirement.service.rcon.impl

import org.codeoverflow.chatoverflow.WithLogger
import org.codeoverflow.chatoverflow.api.io.input.RconInput
import org.codeoverflow.chatoverflow.registry.Impl
import org.codeoverflow.chatoverflow.requirement.impl.InputImpl
import org.codeoverflow.chatoverflow.requirement.service.rcon.RconConnector

@Impl(impl = classOf[RconInput], connector = classOf[RconConnector])
class RconInputImpl extends InputImpl[RconConnector] with RconInput with WithLogger {
override def getCommandOutput(command: String): String = sourceConnector.get.sendCommand(command)

/**
* Start the input, called after source connector did init
*
* @return true if starting the input was successful, false if some problems occurred
*/
override def start(): Boolean = true

override def stop(): Boolean = true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.codeoverflow.chatoverflow.requirement.service.rcon.impl

import org.codeoverflow.chatoverflow.WithLogger
import org.codeoverflow.chatoverflow.api.io.output.RconOutput
import org.codeoverflow.chatoverflow.registry.Impl
import org.codeoverflow.chatoverflow.requirement.impl.OutputImpl
import org.codeoverflow.chatoverflow.requirement.service.rcon.RconConnector

@Impl(impl = classOf[RconOutput], connector = classOf[RconConnector])
class RconOutputImpl extends OutputImpl[RconConnector] with RconOutput with WithLogger {
override def sendCommand(command: String): Boolean = {
sourceConnector.get.sendCommand(command) != null
}

/**
* Start the input, called after source connector did init
*
* @return true if starting the input was successful, false if some problems occurred
*/
override def start(): Boolean = true

/**
* Stops the output, called before source connector will shutdown
*
* @return true if stopping was successful
*/
override def stop(): Boolean = true
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package org.codeoverflow.chatoverflow.requirement.service.serial

import java.io.{InputStream, PrintStream}

import com.fazecast.jSerialComm.{SerialPort, SerialPortInvalidPortException}
import com.fazecast.jSerialComm.{SerialPort, SerialPortEvent, SerialPortInvalidPortException}
import org.codeoverflow.chatoverflow.WithLogger
import org.codeoverflow.chatoverflow.connector.Connector

import scala.collection.mutable

/**
* The serial connector allows to communicate with a device connected to the pcs serial port (like an Arduino)
*
Expand All @@ -19,6 +21,7 @@ class SerialConnector(override val sourceIdentifier: String) extends Connector(s
private var serialPort: Option[SerialPort] = None
private var out: Option[PrintStream] = None
private var in: Option[InputStream] = None
private val inputListeners: mutable.Map[Array[Byte] => Unit, SerialPortEvent => Unit] = mutable.Map()

/**
* @throws java.lang.IllegalStateException if the serial port is not available yet
Expand Down Expand Up @@ -49,11 +52,20 @@ class SerialConnector(override val sourceIdentifier: String) extends Connector(s
@throws(classOf[IllegalStateException])
def addInputListener(listener: Array[Byte] => Unit): Unit = {
if (serialPort.isEmpty) throw new IllegalStateException("Serial port is not available yet")
serialPortInputListener.addDataAvailableListener(_ => {
val l: SerialPortEvent => Unit = _ => {
val buffer = new Array[Byte](serialPort.get.bytesAvailable())
serialPort.get.readBytes(buffer, buffer.length)
listener(buffer)
})
}
inputListeners += (listener -> l)
serialPortInputListener.addDataAvailableListener(l)
}

def removeInputListener(listener: Array[Byte] => Unit): Unit = {
inputListeners remove listener match {
case Some(l) => serialPortInputListener.removeDataAvailableListener(l)
case _ => //listener not found, do nothing
}
}

/**
Expand Down Expand Up @@ -93,6 +105,7 @@ class SerialConnector(override val sourceIdentifier: String) extends Connector(s
* Closes the connection with the port
*/
override def stop(): Boolean = {

serialPort.foreach(_.closePort())
true
}
Expand Down
Loading