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/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