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

Commit 4e40b6d

Browse files
committed
Merge branch 'discord-service-v2'
2 parents 3f67216 + 97e67cb commit 4e40b6d

File tree

9 files changed

+1058
-719
lines changed

9 files changed

+1058
-719
lines changed

bootstrap/src/main/resources/dependencies.xml

Lines changed: 632 additions & 694 deletions
Large diffs are not rendered by default.

build.sbt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ libraryDependencies ++= Seq(
5353
//"com.typesafe.akka" %% "akka-testkit" % "2.5.18" % Test
5454
)
5555

56+
// JDA
57+
resolvers += "jcenter-bintray" at "http://jcenter.bintray.com"
58+
libraryDependencies += "net.dv8tion" % "JDA" % "4.ALPHA.0_82"
59+
5660
// ---------------------------------------------------------------------------------------------------------------------
5761
// PLUGIN FRAMEWORK DEFINITIONS
5862
// ---------------------------------------------------------------------------------------------------------------------
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package org.codeoverflow.chatoverflow.requirement.service.discord
2+
3+
import java.util.function.Consumer
4+
5+
import javax.security.auth.login.LoginException
6+
import net.dv8tion.jda.api.entities.{MessageChannel, TextChannel}
7+
import net.dv8tion.jda.api.events.message.{MessageDeleteEvent, MessageReceivedEvent, MessageUpdateEvent}
8+
import net.dv8tion.jda.api.{JDA, JDABuilder}
9+
import org.codeoverflow.chatoverflow.WithLogger
10+
import org.codeoverflow.chatoverflow.connector.Connector
11+
12+
/**
13+
* The discord connector connects to the discord REST API
14+
*
15+
* @param sourceIdentifier the unique source identifier (in this implementation only for identifying)
16+
*/
17+
class DiscordChatConnector(override val sourceIdentifier: String) extends Connector(sourceIdentifier) with WithLogger {
18+
private val discordChatListener = new DiscordChatListener
19+
20+
private var jda: Option[JDA] = None
21+
22+
override protected var requiredCredentialKeys: List[String] = List("authToken")
23+
override protected var optionalCredentialKeys: List[String] = List()
24+
25+
private val defaultFailureHandler: Consumer[_ >: Throwable] =
26+
throwable => logger warn s"Rest action for connector $sourceIdentifier failed: ${throwable.getMessage}"
27+
28+
def addMessageReceivedListener(listener: MessageReceivedEvent => Unit): Unit = {
29+
discordChatListener.addMessageReceivedListener(listener)
30+
}
31+
32+
def addMessageUpdateListener(listener: MessageUpdateEvent => Unit): Unit = {
33+
discordChatListener.addMessageUpdateEventListener(listener)
34+
}
35+
36+
def addMessageDeleteListener(listener: MessageDeleteEvent => Unit): Unit = {
37+
discordChatListener.addMessageDeleteEventListener(listener)
38+
}
39+
40+
/**
41+
* Connects to discord
42+
*/
43+
override def start(): Boolean = {
44+
try {
45+
jda = Some(new JDABuilder(credentials.get.getValue("authToken").get).build())
46+
jda.get.addEventListener(discordChatListener)
47+
logger info "Waiting while the bot is connecting..."
48+
jda.get.awaitReady()
49+
running = true
50+
logger info "Started connector."
51+
true
52+
} catch {
53+
case _: LoginException =>
54+
logger warn "Login failed! Invalid authToken."
55+
false
56+
case _: IllegalArgumentException =>
57+
logger warn "Login failed! Empty authToken."
58+
false
59+
}
60+
}
61+
62+
/**
63+
* Closes the connection to discord
64+
*/
65+
override def stop(): Boolean = {
66+
jda.foreach(_.shutdown())
67+
true
68+
}
69+
70+
/**
71+
* validates that jda is currently available
72+
*
73+
* @return the jda instance
74+
* @throws IllegalStateException if JDA is not available yet
75+
*/
76+
private def validJDA: JDA = {
77+
jda match {
78+
case Some(_jda) => _jda
79+
case None => throw new IllegalStateException("JDA is not available yet")
80+
}
81+
}
82+
83+
/**
84+
* Retrieves a text channel
85+
*
86+
* @param channelId the id of a text channel
87+
* @return Some text channel or None if no text channel with that id exists
88+
*/
89+
def getTextChannel(channelId: String): Option[TextChannel] = Option(validJDA.getTextChannelById(channelId))
90+
91+
/**
92+
* Sends a message to a text channel
93+
*
94+
* @param channelId the id of the text channel
95+
* @param chatMessage the actual message
96+
*/
97+
def sendChatMessage(channelId: String, chatMessage: String): Unit = {
98+
Option(validJDA.getTextChannelById(channelId)) match {
99+
case Some(channel) => channel.sendMessage(chatMessage).queue(null, defaultFailureHandler)
100+
case None => throw new IllegalArgumentException(s"Channel with id $channelId not found")
101+
}
102+
}
103+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.codeoverflow.chatoverflow.requirement.service.discord
2+
3+
import net.dv8tion.jda.api.events.GenericEvent
4+
import net.dv8tion.jda.api.events.message.{MessageDeleteEvent, MessageReceivedEvent, MessageUpdateEvent}
5+
import net.dv8tion.jda.api.hooks.EventListener
6+
7+
import scala.collection.mutable.ListBuffer
8+
9+
/**
10+
* The discord chat listener class holds the handler to react to creation, edits and removal of messages using the rest api
11+
*/
12+
class DiscordChatListener extends EventListener {
13+
14+
private val messageEventListener = ListBuffer[MessageReceivedEvent => Unit]()
15+
16+
private val messageUpdateEventListener = ListBuffer[MessageUpdateEvent => Unit]()
17+
18+
private val messageDeleteEventListener = ListBuffer[MessageDeleteEvent => Unit]()
19+
20+
def addMessageReceivedListener(listener: MessageReceivedEvent => Unit): Unit = messageEventListener += listener
21+
22+
def addMessageUpdateEventListener(listener: MessageUpdateEvent => Unit): Unit = messageUpdateEventListener += listener
23+
24+
def addMessageDeleteEventListener(listener: MessageDeleteEvent => Unit): Unit = messageDeleteEventListener += listener
25+
26+
override def onEvent(event: GenericEvent): Unit = {
27+
event match {
28+
case receivedEvent: MessageReceivedEvent => messageEventListener.foreach(listener => listener(receivedEvent))
29+
case updateEvent: MessageUpdateEvent => messageUpdateEventListener.foreach(listener => listener(updateEvent))
30+
case deleteEvent: MessageDeleteEvent => messageDeleteEventListener.foreach(listener => listener(deleteEvent))
31+
case _ => //Any other event, do nothing
32+
}
33+
}
34+
}
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package org.codeoverflow.chatoverflow.requirement.service.discord.impl
2+
3+
import java.awt.Color
4+
import java.util
5+
import java.util.Calendar
6+
import java.util.function.{BiConsumer, Consumer}
7+
8+
import net.dv8tion.jda.api.entities.{ChannelType, Message, MessageType, PrivateChannel, TextChannel}
9+
import net.dv8tion.jda.api.events.message.{MessageDeleteEvent, MessageReceivedEvent, MessageUpdateEvent}
10+
import org.codeoverflow.chatoverflow.WithLogger
11+
import org.codeoverflow.chatoverflow.api.io.dto.chat.discord.{DiscordChannel, DiscordChatCustomEmoticon, DiscordChatMessage, DiscordChatMessageAuthor}
12+
import org.codeoverflow.chatoverflow.api.io.input.chat.DiscordChatInput
13+
import org.codeoverflow.chatoverflow.registry.Impl
14+
import org.codeoverflow.chatoverflow.requirement.Connection
15+
import org.codeoverflow.chatoverflow.requirement.service.discord.DiscordChatConnector
16+
17+
import scala.collection.JavaConverters._
18+
import scala.collection.mutable.ListBuffer
19+
20+
/**
21+
* This is the implementation of the discord chat input, using the discord connector.
22+
*/
23+
@Impl(impl = classOf[DiscordChatInput], connector = classOf[DiscordChatConnector])
24+
class DiscordChatInputImpl extends Connection[DiscordChatConnector] with DiscordChatInput with WithLogger {
25+
26+
private var channelId = getSourceIdentifier
27+
private val messages: ListBuffer[DiscordChatMessage] = ListBuffer[DiscordChatMessage]()
28+
private val privateMessages: ListBuffer[DiscordChatMessage] = ListBuffer[DiscordChatMessage]()
29+
private val messageHandler = ListBuffer[Consumer[DiscordChatMessage]]()
30+
private val privateMessageHandler = ListBuffer[Consumer[DiscordChatMessage]]()
31+
private val messageEditHandler = ListBuffer[BiConsumer[DiscordChatMessage, DiscordChatMessage]]()
32+
private val messageDeleteHandler = ListBuffer[Consumer[DiscordChatMessage]]()
33+
private val privateMessageEditHandler = ListBuffer[BiConsumer[DiscordChatMessage, DiscordChatMessage]]()
34+
private val privateMessageDeleteHandler = ListBuffer[Consumer[DiscordChatMessage]]()
35+
36+
override def init(): Boolean = {
37+
if (sourceConnector.isDefined) {
38+
if (sourceConnector.get.isRunning || sourceConnector.get.init()) {
39+
setChannel(getSourceIdentifier)
40+
sourceConnector.get.addMessageReceivedListener(onMessage)
41+
sourceConnector.get.addMessageUpdateListener(onMessageUpdate)
42+
sourceConnector.get.addMessageDeleteListener(onMessageDelete)
43+
true
44+
} else false
45+
} else {
46+
logger warn "Source connector not set."
47+
false
48+
}
49+
}
50+
51+
/**
52+
* Listens for received messages, parses the data, adds them to the buffer and handles them over to the correct handler
53+
*
54+
* @param event a event with an new message
55+
*/
56+
private def onMessage(event: MessageReceivedEvent): Unit = {
57+
if (event.getMessage.getType == MessageType.DEFAULT) {
58+
val message = DiscordChatInputImpl.parse(event.getMessage)
59+
event.getChannelType match {
60+
case ChannelType.TEXT if event.getTextChannel.getId == channelId =>
61+
messageHandler.foreach(_.accept(message))
62+
messages += message
63+
case ChannelType.PRIVATE =>
64+
privateMessageHandler.foreach(_.accept(message))
65+
privateMessages += message
66+
case _ => //Unknown channel, do nothing
67+
}
68+
}
69+
}
70+
71+
/**
72+
* Listens for edited messages, parses the data, edits the buffer and handles them over to the correct handler
73+
*
74+
* @param event a event with an edited message
75+
*/
76+
private def onMessageUpdate(event: MessageUpdateEvent): Unit = {
77+
if (event.getMessage.getType == MessageType.DEFAULT) {
78+
val newMessage = DiscordChatInputImpl.parse(event.getMessage)
79+
event.getChannelType match {
80+
case ChannelType.TEXT =>
81+
val i = messages.indexWhere(_.getId == newMessage.getId)
82+
if (i != -1) {
83+
val oldMessage = messages(i)
84+
messages.update(i, newMessage)
85+
messageEditHandler.foreach(_.accept(oldMessage, newMessage))
86+
}
87+
case ChannelType.PRIVATE =>
88+
val i = privateMessages.indexWhere(_.getId == newMessage.getId)
89+
if (i != -1) {
90+
val oldMessage = messages(i)
91+
privateMessages.update(i, newMessage)
92+
privateMessageEditHandler.foreach(_.accept(oldMessage, newMessage))
93+
}
94+
case _ => //Unknown channel, do nothing
95+
}
96+
}
97+
}
98+
99+
/**
100+
* Listens for deleted messages, removes them from the buffer and handles them over to the correct handler
101+
*
102+
* @param event a event with an deleted message
103+
*/
104+
private def onMessageDelete(event: MessageDeleteEvent): Unit = {
105+
val id = event.getMessageId
106+
event.getChannelType match {
107+
case ChannelType.TEXT if event.getTextChannel.getId == channelId =>
108+
val i = messages.indexWhere(_.getId == id)
109+
if (i != -1) {
110+
val oldMessage = messages.remove(i)
111+
messageDeleteHandler.foreach(_.accept(oldMessage))
112+
}
113+
case ChannelType.PRIVATE =>
114+
val i = privateMessages.indexWhere(_.getId == id)
115+
if (i != -1) {
116+
val oldMessage = privateMessages.remove(i)
117+
privateMessageDeleteHandler.foreach(_.accept(oldMessage))
118+
}
119+
}
120+
}
121+
122+
override def getLastMessages(lastMilliseconds: Long): java.util.List[DiscordChatMessage] = {
123+
val currentTime = Calendar.getInstance.getTimeInMillis
124+
125+
messages.filter(_.getTimestamp > currentTime - lastMilliseconds).toList.asJava
126+
}
127+
128+
override def getLastPrivateMessages(lastMilliseconds: Long): util.List[DiscordChatMessage] = {
129+
val currentTime = Calendar.getInstance.getTimeInMillis
130+
131+
privateMessages.filter(_.getTimestamp > currentTime - lastMilliseconds).toList.asJava
132+
}
133+
override def registerMessageHandler(handler: Consumer[DiscordChatMessage]): Unit = messageHandler += handler
134+
135+
override def registerPrivateMessageHandler(handler : Consumer[DiscordChatMessage]): Unit = privateMessageHandler += handler
136+
137+
override def registerMessageEditHandler(handler: BiConsumer[DiscordChatMessage, DiscordChatMessage]): Unit = messageEditHandler += handler
138+
139+
override def registerPrivateMessageEditHandler(handler: BiConsumer[DiscordChatMessage, DiscordChatMessage]): Unit = privateMessageEditHandler += handler
140+
141+
override def registerMessageDeleteHandler(handler: Consumer[DiscordChatMessage]): Unit = messageDeleteHandler += handler
142+
143+
override def registerPrivateMessageDeleteHandler(handler: Consumer[DiscordChatMessage]): Unit = privateMessageDeleteHandler += handler
144+
145+
override def setChannel(channelId: String): Unit = {
146+
sourceConnector.get.getTextChannel(channelId) match {
147+
case Some(_) => this.channelId = channelId
148+
case None => throw new IllegalArgumentException("Channel with that id doesn't exist")
149+
}
150+
}
151+
152+
override def getChannelId: String = channelId
153+
154+
override def serialize(): String = getSourceIdentifier
155+
156+
override def deserialize(value: String): Unit = {
157+
setSourceConnector(value)
158+
}
159+
160+
override def getMessage(messageId: String): DiscordChatMessage =
161+
messages.find(_.getId == messageId).getOrElse(privateMessages.find(_.getId == messageId).orNull)
162+
}
163+
164+
object DiscordChatInputImpl {
165+
166+
/**
167+
* Creates a DiscordChatMessage from the data provided by this message
168+
*
169+
* @param message the discord message object returned by jda
170+
* @return the DiscordChatMessage of the message for work with the api
171+
*/
172+
private def parse(message: Message): DiscordChatMessage = {
173+
val msg = message.getContentRaw
174+
val id = message.getId
175+
val author = Option(message.getMember) match {
176+
case Some(member) =>
177+
Option(message.getMember.getColor) match {
178+
case Some(c) =>
179+
new DiscordChatMessageAuthor(member.getEffectiveName, member.getId, "#%02X%02X%02X".format(c.getRed, c.getBlue, c.getGreen))
180+
case None =>
181+
new DiscordChatMessageAuthor(member.getEffectiveName, member.getId)
182+
}
183+
case None =>
184+
new DiscordChatMessageAuthor(message.getAuthor.getName, message.getAuthor.getId)
185+
}
186+
val channel = message.getChannel match {
187+
case c: TextChannel => new DiscordChannel(c.getName, c.getId, Option(c.getTopic).getOrElse(""))
188+
case c: PrivateChannel => new DiscordChannel(c.getName, c.getId)
189+
}
190+
val timestamp = message.getTimeCreated.toInstant.toEpochMilli
191+
val emotes = DiscordChatInputImpl.listEmotes(message).asJava
192+
new DiscordChatMessage(author, msg, timestamp, channel, emotes, id)
193+
}
194+
195+
/**
196+
* Parses the emotes of a discord message into a list
197+
*
198+
* @param message the discord message object returned by jda
199+
* @return the DiscordChatCustomEmoticon of the message for work with the api
200+
*/
201+
private def listEmotes(message: Message): List[DiscordChatCustomEmoticon] = {
202+
val emotes = ListBuffer[DiscordChatCustomEmoticon]()
203+
for (emote <- message.getEmotes.asScala if !emote.isFake) {
204+
val content = message.getContentRaw
205+
var index = content.indexOf(emote.getAsMention)
206+
while (index != -1) {
207+
index = content.indexOf(emote.getAsMention)
208+
emotes += new DiscordChatCustomEmoticon(emote.getName, index, emote.isAnimated, emote.getId)
209+
}
210+
}
211+
emotes.toList
212+
}
213+
}
214+

0 commit comments

Comments
 (0)