diff --git a/README.md b/README.md index c3f5176f..46501964 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,11 @@ 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**. @@ -22,7 +22,7 @@ The alternative: Develop everything by yourself and waste hundreds of hours with **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. @@ -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 diff --git a/build.sbt b/build.sbt index af0f91b9..307b300c 100644 --- a/build.sbt +++ b/build.sbt @@ -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. diff --git a/src/main/scala/org/codeoverflow/chatoverflow/requirement/impl/EventInputImpl.scala b/src/main/scala/org/codeoverflow/chatoverflow/requirement/impl/EventInputImpl.scala index 695c5634..ac44bcec 100644 --- a/src/main/scala/org/codeoverflow/chatoverflow/requirement/impl/EventInputImpl.scala +++ b/src/main/scala/org/codeoverflow/chatoverflow/requirement/impl/EventInputImpl.scala @@ -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 + } + } } diff --git a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/discord/DiscordChatConnector.scala b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/discord/DiscordChatConnector.scala index 37cb8dbb..59aafbb2 100644 --- a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/discord/DiscordChatConnector.scala +++ b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/discord/DiscordChatConnector.scala @@ -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 */ diff --git a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/discord/DiscordChatListener.scala b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/discord/DiscordChatListener.scala index fb94c5a2..7196b36d 100644 --- a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/discord/DiscordChatListener.scala +++ b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/discord/DiscordChatListener.scala @@ -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)) diff --git a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/discord/impl/DiscordChatInputImpl.scala b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/discord/impl/DiscordChatInputImpl.scala index 9ce19b36..efcdd05b 100644 --- a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/discord/impl/DiscordChatInputImpl.scala +++ b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/discord/impl/DiscordChatInputImpl.scala @@ -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 } @@ -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 diff --git a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/rcon/RconConnector.scala b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/rcon/RconConnector.scala new file mode 100644 index 00000000..82615095 --- /dev/null +++ b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/rcon/RconConnector.scala @@ -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 + } +} diff --git a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/rcon/impl/RconInputImpl.scala b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/rcon/impl/RconInputImpl.scala new file mode 100644 index 00000000..e837b9a8 --- /dev/null +++ b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/rcon/impl/RconInputImpl.scala @@ -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 +} diff --git a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/rcon/impl/RconOutputImpl.scala b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/rcon/impl/RconOutputImpl.scala new file mode 100644 index 00000000..087a19f9 --- /dev/null +++ b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/rcon/impl/RconOutputImpl.scala @@ -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 +} diff --git a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/serial/SerialConnector.scala b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/serial/SerialConnector.scala index d570613b..70b75cda 100644 --- a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/serial/SerialConnector.scala +++ b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/serial/SerialConnector.scala @@ -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) * @@ -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 @@ -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 + } } /** @@ -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 } diff --git a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/serial/SerialPortInputListener.scala b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/serial/SerialPortInputListener.scala index ee41ec79..caa7c796 100644 --- a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/serial/SerialPortInputListener.scala +++ b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/serial/SerialPortInputListener.scala @@ -17,4 +17,6 @@ class SerialPortInputListener extends SerialPortDataListener { } def addDataAvailableListener(listener: SerialPortEvent => Unit): Unit = listeners += listener + + def removeDataAvailableListener(listener: SerialPortEvent => Unit): Unit = listeners += listener } diff --git a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/serial/impl/SerialInputImpl.scala b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/serial/impl/SerialInputImpl.scala index 103b7529..ad9d4f27 100644 --- a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/serial/impl/SerialInputImpl.scala +++ b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/serial/impl/SerialInputImpl.scala @@ -12,11 +12,15 @@ import org.codeoverflow.chatoverflow.requirement.service.serial.SerialConnector @Impl(impl = classOf[SerialInput], connector = classOf[SerialConnector]) class SerialInputImpl extends EventInputImpl[SerialEvent, SerialConnector] with SerialInput with WithLogger { + private val onInputFn = onInput _ + override def start(): Boolean = { - sourceConnector.get.addInputListener(bytes => call(new SerialDataAvailableEvent(bytes))) + sourceConnector.get.addInputListener(onInputFn) true } + private def onInput(bytes: Array[Byte]): Unit = call(new SerialDataAvailableEvent(bytes)) + override def getInputStream: InputStream = sourceConnector.get.getInputStream /** @@ -24,5 +28,8 @@ class SerialInputImpl extends EventInputImpl[SerialEvent, SerialConnector] with * * @return true if stopping was successful */ - override def stop(): Boolean = true + override def stop(): Boolean = { + sourceConnector.get.removeInputListener(onInputFn) + true + } } diff --git a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/tipeeestream/TipeeestreamConnector.scala b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/tipeeestream/TipeeestreamConnector.scala index f8d160c0..655aa3f0 100644 --- a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/tipeeestream/TipeeestreamConnector.scala +++ b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/tipeeestream/TipeeestreamConnector.scala @@ -86,6 +86,12 @@ class TipeeestreamConnector(override val sourceIdentifier: String) extends Conne def addFollowEventListener(listener: JSONObject => Unit): Unit = tipeeeStreamListener.addFollowEventListener(listener) + def removeSubscriptionEventListener(listener: JSONObject => Unit): Unit = tipeeeStreamListener.removeSubscriptionEventListener(listener) + + def removeDonationEventListener(listener: JSONObject => Unit): Unit = tipeeeStreamListener.removeDonationEventListener(listener) + + def removeFollowEventListener(listener: JSONObject => Unit): Unit = tipeeeStreamListener.removeFollowEventListener(listener) + override def stop(): Boolean = { socket.foreach(_.close()) true diff --git a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/tipeeestream/TipeeestreamListener.scala b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/tipeeestream/TipeeestreamListener.scala index 180f370d..92aef724 100644 --- a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/tipeeestream/TipeeestreamListener.scala +++ b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/tipeeestream/TipeeestreamListener.scala @@ -23,6 +23,18 @@ class TipeeestreamListener { followEventListeners += listener } + def removeSubscriptionEventListener(listener: JSONObject => Unit): Unit = { + subscriptionEventListeners -= listener + } + + def removeDonationEventListener(listener: JSONObject => Unit): Unit = { + donationEventListeners -= listener + } + + def removeFollowEventListener(listener: JSONObject => Unit): Unit = { + followEventListeners -= listener + } + def onSocketEvent(objects : Array[AnyRef]) : Unit = { val json: JSONObject = objects(0).asInstanceOf[JSONObject] val event: JSONObject = json.getJSONObject("event") diff --git a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/tipeeestream/impl/TipeestreamEventInputImpl.scala b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/tipeeestream/impl/TipeestreamEventInputImpl.scala index d0241b2a..e6b89887 100644 --- a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/tipeeestream/impl/TipeestreamEventInputImpl.scala +++ b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/tipeeestream/impl/TipeestreamEventInputImpl.scala @@ -19,10 +19,14 @@ class TipeestreamEventInputImpl extends EventInputImpl[TipeeestreamEvent, Tipeee private val DATE_FORMATTER = new DateTimeFormatterBuilder() .parseCaseInsensitive().append(DateTimeFormatter.ISO_LOCAL_DATE_TIME).appendOffset("+HHMM", "Z").toFormatter + private val onFollowFn = onFollow _ + private val onSubscriptionFn = onSubscription _ + private val onDonationFn = onDonation _ + override def start(): Boolean = { - sourceConnector.get.addFollowEventListener(onFollow) - sourceConnector.get.addSubscriptionEventListener(onSubscription) - sourceConnector.get.addDonationEventListener(onDonation) + sourceConnector.get.addFollowEventListener(onFollowFn) + sourceConnector.get.addSubscriptionEventListener(onSubscriptionFn) + sourceConnector.get.addDonationEventListener(onDonationFn) true } @@ -83,5 +87,10 @@ class TipeestreamEventInputImpl extends EventInputImpl[TipeeestreamEvent, Tipeee } } - override def stop(): Boolean = true + override def stop(): Boolean = { + sourceConnector.get.removeFollowEventListener(onFollowFn) + sourceConnector.get.removeSubscriptionEventListener(onSubscriptionFn) + sourceConnector.get.removeDonationEventListener(onDonationFn) + true + } } diff --git a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/twitch/chat/TwitchChatConnectListener.scala b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/twitch/chat/TwitchChatConnectListener.scala new file mode 100644 index 00000000..e3a74bfa --- /dev/null +++ b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/twitch/chat/TwitchChatConnectListener.scala @@ -0,0 +1,25 @@ +package org.codeoverflow.chatoverflow.requirement.service.twitch.chat + +import org.pircbotx.hooks.events.{ConnectAttemptFailedEvent, ConnectEvent, NoticeEvent} +import org.pircbotx.hooks.{Event, ListenerAdapter} + +/** + * Handles connection events for the TwitchChatConnector. + * Calls the callback function once the bot connected and reports connection errors. + * @param fn the callback which will be called once suitable event has been received. + * The first param informs whether the connection could be established successfully + * and the second param includes a error description if something has gone wrong. + */ +class TwitchChatConnectListener(fn: (Boolean, String) => Unit) extends ListenerAdapter { + override def onEvent(event: Event): Unit = { + event match { + case _: ConnectEvent => fn(true, "") + case e: ConnectAttemptFailedEvent => fn(false, "couldn't connect to irc chat server") + case e: NoticeEvent => + if (e.getNotice.contains("authentication failed")) { + fn(false, "authentication failed") + } + case _ => + } + } +} diff --git a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/twitch/chat/TwitchChatConnector.scala b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/twitch/chat/TwitchChatConnector.scala index 3ee7107f..e925b08b 100644 --- a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/twitch/chat/TwitchChatConnector.scala +++ b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/twitch/chat/TwitchChatConnector.scala @@ -15,10 +15,12 @@ import scala.collection.mutable.ListBuffer */ class TwitchChatConnector(override val sourceIdentifier: String) extends Connector(sourceIdentifier) with WithLogger { private val twitchChatListener = new TwitchChatListener + private val connectionListener = new TwitchChatConnectListener(onConnect) private val oauthKey = "oauth" override protected var requiredCredentialKeys: List[String] = List(oauthKey) override protected var optionalCredentialKeys: List[String] = List() private var bot: PircBotX = _ + private var status: Option[(Boolean, String)] = None private val channels = ListBuffer[String]() def addMessageEventListener(listener: MessageEvent => Unit): Unit = { @@ -29,6 +31,14 @@ class TwitchChatConnector(override val sourceIdentifier: String) extends Connect twitchChatListener.addUnknownEventListener(listener) } + def removeMessageEventListener(listener: MessageEvent => Unit): Unit = { + twitchChatListener.removeMessageEventListener(listener) + } + + def removeUnknownEventListener(listener: UnknownEvent => Unit): Unit = { + twitchChatListener.removeUnknownEventListener(listener) + } + def joinChannel(channel: String): Unit = { bot.send().joinChannel(channel) channels += channel @@ -63,6 +73,7 @@ class TwitchChatConnector(override val sourceIdentifier: String) extends Connect .setName(credentials.get.credentialsIdentifier) .setServerPassword(password.getOrElse("")) .addListener(twitchChatListener) + .addListener(connectionListener) .buildConfiguration() } else { logger error "No credentials set!" @@ -71,33 +82,47 @@ class TwitchChatConnector(override val sourceIdentifier: String) extends Connect } + /** + * Gets called by the TwitchChatConnectListener when the bot has connected. + * Saves the passed information into the status variable. + */ + private def onConnect(success: Boolean, msg: String): Unit = { + status.synchronized { + // tell the thread which starts the connector that the status has been reported + status.notify() + status = Some((success, msg)) + } + } + /** * Starts the connector, e.g. creates a connection with its platform. */ override def start(): Boolean = { bot = new PircBotX(getConfig) startBot() - true } - private def startBot(): Unit = { - - var errorCount = 0 - + private def startBot(): Boolean = { new Thread(() => { bot.startBot() }).start() - while (bot.getState != PircBotX.State.CONNECTED && errorCount < 30) { - logger info "Waiting while the bot is connecting..." - Thread.sleep(100) - errorCount += 1 + logger info "Waiting while the bot is connecting and logging in..." + status.synchronized { + status.wait(10000) + } + + if (status.isEmpty) { + logger error "Bot couldn't connect within timeout of 10 seconds." + return false } - if (errorCount >= 30) { - logger error "Fatal. Unable to start bot." + val (success, msg) = status.get + if (!success) { + logger error s"Bot couldn't connect. Reason: $msg." } + success } /** @@ -106,6 +131,8 @@ class TwitchChatConnector(override val sourceIdentifier: String) extends Connect override def stop(): Boolean = { bot.sendIRC().quitServer() bot.close() + status = None + channels.clear() true } } diff --git a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/twitch/chat/TwitchChatListener.scala b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/twitch/chat/TwitchChatListener.scala index d4148403..c8037496 100644 --- a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/twitch/chat/TwitchChatListener.scala +++ b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/twitch/chat/TwitchChatListener.scala @@ -29,4 +29,12 @@ class TwitchChatListener extends ListenerAdapter { unknownEventListener += listener } + def removeMessageEventListener(listener: MessageEvent => Unit): Unit = { + messageEventListener -= listener + } + + def removeUnknownEventListener(listener: UnknownEvent => Unit): Unit = { + unknownEventListener -= listener + } + } diff --git a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/twitch/chat/impl/TwitchChatInputImpl.scala b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/twitch/chat/impl/TwitchChatInputImpl.scala index 35594adc..4271dcb8 100644 --- a/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/twitch/chat/impl/TwitchChatInputImpl.scala +++ b/src/main/scala/org/codeoverflow/chatoverflow/requirement/service/twitch/chat/impl/TwitchChatInputImpl.scala @@ -9,7 +9,7 @@ import org.codeoverflow.chatoverflow.api.io.dto.chat.{ChatEmoticon, TextChannel} import org.codeoverflow.chatoverflow.api.io.event.chat.twitch.{TwitchChatMessageReceiveEvent, TwitchEvent, TwitchPrivateChatMessageReceiveEvent} import org.codeoverflow.chatoverflow.api.io.input.chat._ import org.codeoverflow.chatoverflow.registry.Impl -import org.codeoverflow.chatoverflow.requirement.impl.{EventInputImpl, InputImpl} +import org.codeoverflow.chatoverflow.requirement.impl.EventInputImpl import org.codeoverflow.chatoverflow.requirement.service.twitch.chat import org.codeoverflow.chatoverflow.requirement.service.twitch.chat.TwitchChatConnector import org.pircbotx.hooks.events.{MessageEvent, UnknownEvent} @@ -31,9 +31,12 @@ class TwitchChatInputImpl extends EventInputImpl[TwitchEvent, chat.TwitchChatCon private var currentChannel: Option[String] = None + private val onMessageFn = onMessage _ + private val onUnknownFn = onUnknown _ + override def start(): Boolean = { - sourceConnector.get.addMessageEventListener(onMessage) - sourceConnector.get.addUnknownEventListener(onUnknown) + sourceConnector.get.addMessageEventListener(onMessageFn) + sourceConnector.get.addUnknownEventListener(onUnknownFn) true } @@ -104,5 +107,9 @@ class TwitchChatInputImpl extends EventInputImpl[TwitchEvent, chat.TwitchChatCon * * @return true if stopping was successful */ - override def stop(): Boolean = true + override def stop(): Boolean = { + sourceConnector.get.removeMessageEventListener(onMessageFn) + sourceConnector.get.removeUnknownEventListener(onUnknownFn) + true + } } \ No newline at end of file