-
Notifications
You must be signed in to change notification settings - Fork 0
Example implementation code
This page will contain example code for parsing IRC chat commands into queue commands and supporting these queue commands.
These are the data types I'll be using:
The data from Twitch deserialized into a data class. Most properties aren't going to be useful for this, just message
which is the chat message, and user
which is the user sending the message.
@Serializable
data class IrcPrivateMessage(
@SerialName("raw") val raw: String,
@SerialName("raw_type") val rawType: String,
@SerialName("message") val message: String,
@SerialName("tags") val tags: Map<String, String>,
@SerialName("emotes") val emotes: List<IrcDataEmote>?,
@SerialName("bits") val bits: Int = 0,
@SerialName("first_message") val firstMessage: Boolean,
@SerialName("user") val user: IrcUser,
)
sealed class ChatCommand() {
abstract val name: String
// region Queuing
class QueueClear(val userId: String): ChatCommand() {
override val name = "clear"
}
class QueueInfo(): ChatCommand() {
override val name = "info"
}
class QueueJoin(
val username: String,
val userId: String,
val notes: String,
): ChatCommand() {
override val name = "join"
}
class QueueLeave(val userId: String): ChatCommand() {
override val name = "leave"
}
class QueueNext(val userId: String): ChatCommand() {
override val name = "next"
}
class QueuePosition(
val username: String,
val userId: String
): ChatCommand() {
override val name = "position"
}
class QueueUnknown(val userId: String, val username: String): ChatCommand() {
override val name = "unknown"
}
// endregion Queuing
}
These classes will be helpful for making API calls.
The following libraries are used:
plugins {
id("org.jetbrains.kotlin.plugin.serialization") version "1.8.20"
}
dependencies {
// Ktor HTTP client
implementation("io.ktor:ktor-client-core:2.3.0")
implementation("io.ktor:ktor-client-cio:2.3.0")
// JSON (de)serialization
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:2.3.0")
}
class QueuerrrApiClient(
private val apiToken: String,
val baseUrl: String,
) {
val httpClient = HttpClient(CIO) {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
ignoreUnknownKeys = true
})
}
}
fun authHeaders(): Map<String, String> = mapOf(
"Authorization" to "Bearer $apiToken",
)
suspend inline fun <reified ResType>makeGet(
path: String
): Result<ResType> {
val url = baseUrl + path
return httpClient.makeGet<ResType>(url, authHeaders())
}
}
// region Requests and Responses
@Serializable
data class QueuerrrResponseStatus(
val status: String,
)
@Serializable
data class QueuerrrResponsePosition(
val position: Int,
)
@Serializable
data class QueuerrrUser(
@SerialName("created_at") val createdAt: String,
@SerialName("twitch_username") val username: String,
@SerialName("twitch_user_id") val userId: String,
@SerialName("notes") val notes: String,
)
@Serializable
data class QueuerrrResponseInfo(
val total: Int,
val users: List<QueuerrrUser>,
)
// endregion Requests and Responses
HTTP client helper:
suspend inline fun <reified T> HttpClient.makeGet(
url: String,
headersMap: Map<String, String>? = null,
): Result<T> =
try {
val response: HttpResponse = get(url) {
contentType(ContentType.Application.Json)
headers {
headersMap?.forEach { (k, v) -> append(k, v) }
}
}
if (response.status.isSuccess()) {
val responseBody: T = response.body()
Result.success(responseBody)
} else {
val responseText = response.bodyAsText()
Result.failure(Throwable(responseText))
}
} catch (e: Exception) {
Result.failure(e)
}
Example initialization of the API client:
val queuerrrApiClient = QueuerrrApiClient(
apiToken = appConfig.queuerrrApiToken, // from environment variables
baseUrl = "https://queuerrr.vercel.app/api/"
)
Here's how I'm parsing the commands in Kotlin. There's probably room for improvement, e.g. sending the username
and userId
with every command in case it's needed later.
fun parseCommand(ircPrivateMessage: IrcPrivateMessage): ChatCommand? {
val message = ircPrivateMessage.message
val segments = message.split("\\b".toRegex())
if (segments.size < 2 || segments[0] != "!") {
// Not a command
return null
}
// Look for custom commands
val commandName = segments[1]
if (commandName == "queue") {
val verb = tryOrNull { message.words[1] }
when (verb) {
"clear" -> return ChatCommand.QueueClear(ircPrivateMessage.user.id)
"info" -> return ChatCommand.QueueInfo()
"join" -> {
val notes = message.replace("!queue join", "").trim()
return ChatCommand.QueueJoin(
username = ircPrivateMessage.user.username,
userId = ircPrivateMessage.user.id,
notes = notes
)
}
"leave" -> return ChatCommand.QueueLeave(ircPrivateMessage.user.id)
"next" -> return ChatCommand.QueueNext(ircPrivateMessage.user.id)
"position" -> {
return ChatCommand.QueuePosition(
username = ircPrivateMessage.user.username,
userId = ircPrivateMessage.user.id,
)
}
else -> {
return ChatCommand.QueueUnknown(
userId = ircPrivateMessage.user.id,
username = ircPrivateMessage.user.username,
)
}
}
}
if (commandName == "join") {
val notes = message.replace("!join", "").trim()
return ChatCommand.QueueJoin(
username = ircPrivateMessage.user.username,
userId = ircPrivateMessage.user.id,
notes = notes
)
}
if (commandName == "leave") {
return ChatCommand.QueueLeave(ircPrivateMessage.user.id)
}
if (commandName == "next") {
return ChatCommand.QueueNext(ircPrivateMessage.user.id)
}
if (commandName == "position") {
return ChatCommand.QueuePosition(
username = ircPrivateMessage.user.username,
userId = ircPrivateMessage.user.id,
)
}
return null
}
After the command has been parsed into a sealed class with the relevant data, you can then handle the command by making API calls:
private suspend fun handleQueueCommands(parsedCommand: ChatCommand) {
when (parsedCommand) {
is ChatCommand.QueueClear -> {
if (parsedCommand.userId != userIds.streamer) return
queuerrrApiClient.makeGet<QueuerrrResponseStatus>("clear")
.onSuccess { response ->
sendChat(response.status)
}
.onFailure { error ->
logger.error("QueueClear error: $error")
}
}
is ChatCommand.QueueInfo -> {
queuerrrApiClient.makeGet<QueuerrrResponseInfo>("info")
.onSuccess { response ->
val message = if (response.users.isEmpty()) {
"There are currently no users in the queue. You can join the queue with !join"
} else {
val usersInQueue = response.users.joinToString(", ") { it.username }
"Users currently in queue: $usersInQueue. You can view the queue here: https://queuerrr.vercel.app"
}
sendChat(message)
}
.onFailure { error ->
sendChat("You can view the queue here: https://queuerrr.vercel.app")
logger.error("QueueInfo error: $error")
}
}
is ChatCommand.QueueJoin -> {
val encodedNotes = tryOrNull { URLEncoder.encode(parsedCommand.notes, "utf-8") }
queuerrrApiClient.makeGet<QueuerrrUser>("join?user_id=${parsedCommand.userId}&username=${parsedCommand.username}¬es=$encodedNotes")
.onSuccess { response ->
sendChat("@${response.username} you've been successfully added to the queue!")
}
.onFailure { error ->
sendChat("BibleThump failed to add ${parsedCommand.username} to the queue")
logger.error("QueueJoin error: $error")
}
}
is ChatCommand.QueueLeave -> {
queuerrrApiClient.makeGet<QueuerrrResponseStatus>("leave?user_id=${parsedCommand.userId}")
.onSuccess { response ->
sendChat(response.status)
}
.onFailure { error ->
logger.error("QueueLeave error: $error")
sendChat("user not in queue")
}
}
is ChatCommand.QueueNext -> {
if (parsedCommand.userId != userIds.streamer) {
sendChat("WutFace only techygrrrl can use this command")
return
}
queuerrrApiClient.makeGet<QueuerrrUser>("next")
.onSuccess { response ->
sendChat("@${response.username} it's your turn now!")
}
.onFailure { error ->
logger.error("QueueNext error: $error")
sendChat("BibleThump queue is empty")
}
}
is ChatCommand.QueuePosition -> {
queuerrrApiClient.makeGet<QueuerrrResponsePosition>("position?user_id=${parsedCommand.userId}")
.onSuccess { response ->
if (response.position == -1) {
sendChat("@${parsedCommand.username} you're not in the queue! use command !join to join the queue")
} else {
sendChat("@${parsedCommand.username} you're in position ${response.position}!")
}
}
.onFailure { error ->
logger.error("QueuePosition error: $error")
sendChat("BibleThump error getting queue position")
}
}
is ChatCommand.QueueUnknown -> {
val commands = listOf("!join", "!leave", "!position")
val message = "@${parsedCommand.username} HeyGuys Missing or invalid command verb. Valid commands are: ${commands.joinToString(" ")}. You can also view the queue here: https://queuerrr.vercel.app"
sendChat(message)
}
else -> {}
}
}