Skip to content
Permalink
Browse files

Merge branch 'discord-service-v2'

  • Loading branch information...
joblo2213 committed May 14, 2019
2 parents 3f67216 + 97e67cb commit 4e40b6d12f91f24b7d346f993c17a2f8e438d657

Large diffs are not rendered by default.

Oops, something went wrong.
@@ -53,6 +53,10 @@ libraryDependencies ++= Seq(
//"com.typesafe.akka" %% "akka-testkit" % "2.5.18" % Test
)

// JDA
resolvers += "jcenter-bintray" at "http://jcenter.bintray.com"
libraryDependencies += "net.dv8tion" % "JDA" % "4.ALPHA.0_82"

// ---------------------------------------------------------------------------------------------------------------------
// PLUGIN FRAMEWORK DEFINITIONS
// ---------------------------------------------------------------------------------------------------------------------
@@ -0,0 +1,103 @@
package org.codeoverflow.chatoverflow.requirement.service.discord

import java.util.function.Consumer

import javax.security.auth.login.LoginException
import net.dv8tion.jda.api.entities.{MessageChannel, TextChannel}
import net.dv8tion.jda.api.events.message.{MessageDeleteEvent, MessageReceivedEvent, MessageUpdateEvent}
import net.dv8tion.jda.api.{JDA, JDABuilder}
import org.codeoverflow.chatoverflow.WithLogger
import org.codeoverflow.chatoverflow.connector.Connector

/**
* The discord connector connects to the discord REST API
*
* @param sourceIdentifier the unique source identifier (in this implementation only for identifying)
*/
class DiscordChatConnector(override val sourceIdentifier: String) extends Connector(sourceIdentifier) with WithLogger {
private val discordChatListener = new DiscordChatListener

private var jda: Option[JDA] = None

override protected var requiredCredentialKeys: List[String] = List("authToken")
override protected var optionalCredentialKeys: List[String] = List()

private val defaultFailureHandler: Consumer[_ >: Throwable] =
throwable => logger warn s"Rest action for connector $sourceIdentifier failed: ${throwable.getMessage}"

def addMessageReceivedListener(listener: MessageReceivedEvent => Unit): Unit = {
discordChatListener.addMessageReceivedListener(listener)
}

def addMessageUpdateListener(listener: MessageUpdateEvent => Unit): Unit = {
discordChatListener.addMessageUpdateEventListener(listener)
}

def addMessageDeleteListener(listener: MessageDeleteEvent => Unit): Unit = {
discordChatListener.addMessageDeleteEventListener(listener)
}

/**
* Connects to discord
*/
override def start(): Boolean = {
try {
jda = Some(new JDABuilder(credentials.get.getValue("authToken").get).build())
jda.get.addEventListener(discordChatListener)
logger info "Waiting while the bot is connecting..."
jda.get.awaitReady()
running = true
logger info "Started connector."
true
} catch {
case _: LoginException =>
logger warn "Login failed! Invalid authToken."
false
case _: IllegalArgumentException =>
logger warn "Login failed! Empty authToken."
false
}
}

/**
* Closes the connection to discord
*/
override def stop(): Boolean = {
jda.foreach(_.shutdown())
true
}

/**
* validates that jda is currently available
*
* @return the jda instance
* @throws IllegalStateException if JDA is not available yet
*/
private def validJDA: JDA = {
jda match {
case Some(_jda) => _jda
case None => throw new IllegalStateException("JDA is not available yet")
}
}

/**
* Retrieves a text channel
*
* @param channelId the id of a text channel
* @return Some text channel or None if no text channel with that id exists
*/
def getTextChannel(channelId: String): Option[TextChannel] = Option(validJDA.getTextChannelById(channelId))

/**
* Sends a message to a text channel
*
* @param channelId the id of the text channel
* @param chatMessage the actual message
*/
def sendChatMessage(channelId: String, chatMessage: String): Unit = {
Option(validJDA.getTextChannelById(channelId)) match {
case Some(channel) => channel.sendMessage(chatMessage).queue(null, defaultFailureHandler)
case None => throw new IllegalArgumentException(s"Channel with id $channelId not found")
}
}
}
@@ -0,0 +1,34 @@
package org.codeoverflow.chatoverflow.requirement.service.discord

import net.dv8tion.jda.api.events.GenericEvent
import net.dv8tion.jda.api.events.message.{MessageDeleteEvent, MessageReceivedEvent, MessageUpdateEvent}
import net.dv8tion.jda.api.hooks.EventListener

import scala.collection.mutable.ListBuffer

/**
* The discord chat listener class holds the handler to react to creation, edits and removal of messages using the rest api
*/
class DiscordChatListener extends EventListener {

private val messageEventListener = ListBuffer[MessageReceivedEvent => Unit]()

private val messageUpdateEventListener = ListBuffer[MessageUpdateEvent => Unit]()

private val messageDeleteEventListener = ListBuffer[MessageDeleteEvent => Unit]()

def addMessageReceivedListener(listener: MessageReceivedEvent => Unit): Unit = messageEventListener += listener

def addMessageUpdateEventListener(listener: MessageUpdateEvent => Unit): Unit = messageUpdateEventListener += listener

def addMessageDeleteEventListener(listener: MessageDeleteEvent => Unit): Unit = messageDeleteEventListener += listener

override def onEvent(event: GenericEvent): Unit = {
event match {
case receivedEvent: MessageReceivedEvent => messageEventListener.foreach(listener => listener(receivedEvent))
case updateEvent: MessageUpdateEvent => messageUpdateEventListener.foreach(listener => listener(updateEvent))
case deleteEvent: MessageDeleteEvent => messageDeleteEventListener.foreach(listener => listener(deleteEvent))
case _ => //Any other event, do nothing
}
}
}
@@ -0,0 +1,214 @@
package org.codeoverflow.chatoverflow.requirement.service.discord.impl

import java.awt.Color
import java.util
import java.util.Calendar
import java.util.function.{BiConsumer, Consumer}

import net.dv8tion.jda.api.entities.{ChannelType, Message, MessageType, PrivateChannel, TextChannel}
import net.dv8tion.jda.api.events.message.{MessageDeleteEvent, MessageReceivedEvent, MessageUpdateEvent}
import org.codeoverflow.chatoverflow.WithLogger
import org.codeoverflow.chatoverflow.api.io.dto.chat.discord.{DiscordChannel, DiscordChatCustomEmoticon, DiscordChatMessage, DiscordChatMessageAuthor}
import org.codeoverflow.chatoverflow.api.io.input.chat.DiscordChatInput
import org.codeoverflow.chatoverflow.registry.Impl
import org.codeoverflow.chatoverflow.requirement.Connection
import org.codeoverflow.chatoverflow.requirement.service.discord.DiscordChatConnector

import scala.collection.JavaConverters._
import scala.collection.mutable.ListBuffer

/**
* This is the implementation of the discord chat input, using the discord connector.
*/
@Impl(impl = classOf[DiscordChatInput], connector = classOf[DiscordChatConnector])
class DiscordChatInputImpl extends Connection[DiscordChatConnector] with DiscordChatInput with WithLogger {

private var channelId = getSourceIdentifier
private val messages: ListBuffer[DiscordChatMessage] = ListBuffer[DiscordChatMessage]()
private val privateMessages: ListBuffer[DiscordChatMessage] = ListBuffer[DiscordChatMessage]()
private val messageHandler = ListBuffer[Consumer[DiscordChatMessage]]()
private val privateMessageHandler = ListBuffer[Consumer[DiscordChatMessage]]()
private val messageEditHandler = ListBuffer[BiConsumer[DiscordChatMessage, DiscordChatMessage]]()
private val messageDeleteHandler = ListBuffer[Consumer[DiscordChatMessage]]()
private val privateMessageEditHandler = ListBuffer[BiConsumer[DiscordChatMessage, DiscordChatMessage]]()
private val privateMessageDeleteHandler = ListBuffer[Consumer[DiscordChatMessage]]()

override def init(): Boolean = {
if (sourceConnector.isDefined) {
if (sourceConnector.get.isRunning || sourceConnector.get.init()) {
setChannel(getSourceIdentifier)
sourceConnector.get.addMessageReceivedListener(onMessage)
sourceConnector.get.addMessageUpdateListener(onMessageUpdate)
sourceConnector.get.addMessageDeleteListener(onMessageDelete)
true
} else false
} else {
logger warn "Source connector not set."
false
}
}

/**
* Listens for received messages, parses the data, adds them to the buffer and handles them over to the correct handler
*
* @param event a event with an new message
*/
private def onMessage(event: MessageReceivedEvent): Unit = {
if (event.getMessage.getType == MessageType.DEFAULT) {
val message = DiscordChatInputImpl.parse(event.getMessage)
event.getChannelType match {
case ChannelType.TEXT if event.getTextChannel.getId == channelId =>
messageHandler.foreach(_.accept(message))
messages += message
case ChannelType.PRIVATE =>
privateMessageHandler.foreach(_.accept(message))
privateMessages += message
case _ => //Unknown channel, do nothing
}
}
}

/**
* Listens for edited messages, parses the data, edits the buffer and handles them over to the correct handler
*
* @param event a event with an edited message
*/
private def onMessageUpdate(event: MessageUpdateEvent): Unit = {
if (event.getMessage.getType == MessageType.DEFAULT) {
val newMessage = DiscordChatInputImpl.parse(event.getMessage)
event.getChannelType match {
case ChannelType.TEXT =>
val i = messages.indexWhere(_.getId == newMessage.getId)
if (i != -1) {
val oldMessage = messages(i)
messages.update(i, newMessage)
messageEditHandler.foreach(_.accept(oldMessage, newMessage))
}
case ChannelType.PRIVATE =>
val i = privateMessages.indexWhere(_.getId == newMessage.getId)
if (i != -1) {
val oldMessage = messages(i)
privateMessages.update(i, newMessage)
privateMessageEditHandler.foreach(_.accept(oldMessage, newMessage))
}
case _ => //Unknown channel, do nothing
}
}
}

/**
* Listens for deleted messages, removes them from the buffer and handles them over to the correct handler
*
* @param event a event with an deleted message
*/
private def onMessageDelete(event: MessageDeleteEvent): Unit = {
val id = event.getMessageId
event.getChannelType match {
case ChannelType.TEXT if event.getTextChannel.getId == channelId =>
val i = messages.indexWhere(_.getId == id)
if (i != -1) {
val oldMessage = messages.remove(i)
messageDeleteHandler.foreach(_.accept(oldMessage))
}
case ChannelType.PRIVATE =>
val i = privateMessages.indexWhere(_.getId == id)
if (i != -1) {
val oldMessage = privateMessages.remove(i)
privateMessageDeleteHandler.foreach(_.accept(oldMessage))
}
}
}

override def getLastMessages(lastMilliseconds: Long): java.util.List[DiscordChatMessage] = {
val currentTime = Calendar.getInstance.getTimeInMillis

messages.filter(_.getTimestamp > currentTime - lastMilliseconds).toList.asJava
}

override def getLastPrivateMessages(lastMilliseconds: Long): util.List[DiscordChatMessage] = {
val currentTime = Calendar.getInstance.getTimeInMillis

privateMessages.filter(_.getTimestamp > currentTime - lastMilliseconds).toList.asJava
}
override def registerMessageHandler(handler: Consumer[DiscordChatMessage]): Unit = messageHandler += handler

override def registerPrivateMessageHandler(handler : Consumer[DiscordChatMessage]): Unit = privateMessageHandler += handler

override def registerMessageEditHandler(handler: BiConsumer[DiscordChatMessage, DiscordChatMessage]): Unit = messageEditHandler += handler

override def registerPrivateMessageEditHandler(handler: BiConsumer[DiscordChatMessage, DiscordChatMessage]): Unit = privateMessageEditHandler += handler

override def registerMessageDeleteHandler(handler: Consumer[DiscordChatMessage]): Unit = messageDeleteHandler += handler

override def registerPrivateMessageDeleteHandler(handler: Consumer[DiscordChatMessage]): Unit = privateMessageDeleteHandler += handler

override def setChannel(channelId: String): Unit = {
sourceConnector.get.getTextChannel(channelId) match {
case Some(_) => this.channelId = channelId
case None => throw new IllegalArgumentException("Channel with that id doesn't exist")
}
}

override def getChannelId: String = channelId

override def serialize(): String = getSourceIdentifier

override def deserialize(value: String): Unit = {
setSourceConnector(value)
}

override def getMessage(messageId: String): DiscordChatMessage =
messages.find(_.getId == messageId).getOrElse(privateMessages.find(_.getId == messageId).orNull)
}

object DiscordChatInputImpl {

/**
* Creates a DiscordChatMessage from the data provided by this message
*
* @param message the discord message object returned by jda
* @return the DiscordChatMessage of the message for work with the api
*/
private def parse(message: Message): DiscordChatMessage = {
val msg = message.getContentRaw
val id = message.getId
val author = Option(message.getMember) match {
case Some(member) =>
Option(message.getMember.getColor) match {
case Some(c) =>
new DiscordChatMessageAuthor(member.getEffectiveName, member.getId, "#%02X%02X%02X".format(c.getRed, c.getBlue, c.getGreen))
case None =>
new DiscordChatMessageAuthor(member.getEffectiveName, member.getId)
}
case None =>
new DiscordChatMessageAuthor(message.getAuthor.getName, message.getAuthor.getId)
}
val channel = message.getChannel match {
case c: TextChannel => new DiscordChannel(c.getName, c.getId, Option(c.getTopic).getOrElse(""))
case c: PrivateChannel => new DiscordChannel(c.getName, c.getId)
}
val timestamp = message.getTimeCreated.toInstant.toEpochMilli
val emotes = DiscordChatInputImpl.listEmotes(message).asJava
new DiscordChatMessage(author, msg, timestamp, channel, emotes, id)
}

/**
* Parses the emotes of a discord message into a list
*
* @param message the discord message object returned by jda
* @return the DiscordChatCustomEmoticon of the message for work with the api
*/
private def listEmotes(message: Message): List[DiscordChatCustomEmoticon] = {
val emotes = ListBuffer[DiscordChatCustomEmoticon]()
for (emote <- message.getEmotes.asScala if !emote.isFake) {
val content = message.getContentRaw
var index = content.indexOf(emote.getAsMention)
while (index != -1) {
index = content.indexOf(emote.getAsMention)
emotes += new DiscordChatCustomEmoticon(emote.getName, index, emote.isAnimated, emote.getId)
}
}
emotes.toList
}
}

Oops, something went wrong.

0 comments on commit 4e40b6d

Please sign in to comment.
You can’t perform that action at this time.