Initial commit
This commit is contained in:
commit
dc108dcc55
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/.gradle/
|
||||||
|
build/
|
||||||
|
/gradlew*
|
||||||
|
*.sqlite
|
||||||
|
/.idea/
|
||||||
|
/gradle/
|
||||||
|
/.kotlin/
|
6
build.gradle.kts
Normal file
6
build.gradle.kts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven("https://maven.landgrafhomyak.ru/")
|
||||||
|
}
|
||||||
|
}
|
36
exe/build.gradle.kts
Normal file
36
exe/build.gradle.kts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven("https://maven.landgrafhomyak.ru/")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath("ru.landgrafhomyak.kotlin:kotlin-mpp-gradle-build-helper:v0.2k2.0.20")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("multiplatform") version "2.0.20"
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(21)
|
||||||
|
jvm()
|
||||||
|
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
jvmMain {
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":storage-api"))
|
||||||
|
implementation(project(":telegram-api"))
|
||||||
|
implementation(project(":impl"))
|
||||||
|
implementation(project(":telegram-api-impl"))
|
||||||
|
implementation(project(":storage-jdbc-sqlite"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.exe
|
||||||
|
|
||||||
|
import java.net.http.HttpClient
|
||||||
|
import java.sql.DriverManager
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.impl.Handlers
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.storage.sqlite_jdbc.JdbcSqliteStorage
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.telegram.impl.IncomingUpdate
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.telegram.impl.JavaBlockingHttpClientTelegramApiImpl
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.telegram.impl.JavaBlockingJson2JsonHttpClient
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
val db = JdbcSqliteStorage(DriverManager.getConnection("jdbc:sqlite:test.sqlite"))
|
||||||
|
val tg = JavaBlockingHttpClientTelegramApiImpl(
|
||||||
|
token = TODO("No token"),
|
||||||
|
httpClient = JavaBlockingJson2JsonHttpClient(HttpClient.newHttpClient())
|
||||||
|
)
|
||||||
|
|
||||||
|
val impl = Handlers(db)
|
||||||
|
|
||||||
|
tg.receiveUpdatesUntilError { u ->
|
||||||
|
try {
|
||||||
|
when (u) {
|
||||||
|
is IncomingUpdate.TextMessage -> impl.onTextMessage(tg, u.msg)
|
||||||
|
is IncomingUpdate.CallbackQuery -> impl.onCallbackQuery(tg, u.query)
|
||||||
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
t.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2
gradle.properties
Normal file
2
gradle.properties
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
kotlin.native.ignoreDisabledTargets=true
|
||||||
|
kotlin.suppressGradlePluginWarnings=IncorrectCompileOnlyDependencyWarning
|
34
impl/build.gradle.kts
Normal file
34
impl/build.gradle.kts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import ru.landgrafhomyak.kotlin.kmp_gradle_build_helper.defineAllMultiplatformTargets
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven("https://maven.landgrafhomyak.ru/")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath("ru.landgrafhomyak.kotlin:kotlin-mpp-gradle-build-helper:v0.2k2.0.20")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("multiplatform") version "2.0.20"
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(21)
|
||||||
|
defineAllMultiplatformTargets()
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
compileOnly(project(":storage-api"))
|
||||||
|
compileOnly(project(":telegram-api"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.impl
|
||||||
|
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.storage.api.Queue
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.storage.api.QueueSet
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.storage.api.User
|
||||||
|
|
||||||
|
internal class EmptyFakeQueueSet(
|
||||||
|
override val name: String,
|
||||||
|
queueNames: List<String>
|
||||||
|
) : QueueSet {
|
||||||
|
override val queues: List<Queue> =
|
||||||
|
if (queueNames.isEmpty()) listOf(EmptyFakeQueue(null))
|
||||||
|
else queueNames.map(::EmptyFakeQueue)
|
||||||
|
override val isOpen: Boolean
|
||||||
|
get() = true
|
||||||
|
|
||||||
|
private class EmptyFakeQueue(override val name: String?) : Queue {
|
||||||
|
override val members: List<User?> get() = emptyList()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,226 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.impl
|
||||||
|
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.storage.api.ModeratorPermissions
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.storage.api.QueueSet
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.storage.api.Storage
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.telegram.api.CallbackQuery
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.telegram.api.Message
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.telegram.api.TelegramBotApi
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
class Handlers(private val storage: Storage) {
|
||||||
|
fun onTextMessage(api: TelegramBotApi, message: Message) {
|
||||||
|
val (command, args) = Parsers.tryParseCommand(message.rawText) ?: return
|
||||||
|
when (command) {
|
||||||
|
"queue" -> this._createQueue(api, message, args)
|
||||||
|
"qopen" -> this._openQueue(api, message, args)
|
||||||
|
"qfinalize" -> this._closeQueue(api, message, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCallbackQuery(api: TelegramBotApi, query: CallbackQuery) {
|
||||||
|
when (query.data) {
|
||||||
|
"qpush" -> when (val result = this.storage.addUserToQueue(query.message.chatId, query.message.messageId, query.user.id, query.user.displayName)) {
|
||||||
|
Storage.Result_AddOrRemoveUser.ALREADY_IN_STATE -> api.answerCallbackQuery(query, "")
|
||||||
|
Storage.Result_AddOrRemoveUser.QUEUE_NOT_FOUND -> api.answerCallbackQuery(query, "\u26A0\uFE0F NullPointerException")
|
||||||
|
Storage.Result_AddOrRemoveUser.QUEUE_OVERFLOW -> api.answerCallbackQuery(query, "\u26A0\uFE0F QueueOverflowException")
|
||||||
|
Storage.Result_AddOrRemoveUser.QUEUE_IS_FINAL -> api.answerCallbackQuery(query, "\u26A0\uFE0F AccessToFinalQueueException")
|
||||||
|
is Storage.Result_AddOrRemoveUser.SUCCESSFUL -> {
|
||||||
|
api.answerCallbackQuery(query, "\u2705 Pushed")
|
||||||
|
api.editMessageText(query.message, this.__fmtQueueMessage(result.updatedQueue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"qpop" -> when (val result = this.storage.removeUserFromQueue(query.message.chatId, query.message.messageId, query.user.id)) {
|
||||||
|
Storage.Result_AddOrRemoveUser.QUEUE_NOT_FOUND -> api.answerCallbackQuery(query, "\u26A0\uFE0F NullPointerException")
|
||||||
|
Storage.Result_AddOrRemoveUser.QUEUE_OVERFLOW -> api.answerCallbackQuery(query, "\u26A0\uFE0F QueueOverflowException")
|
||||||
|
Storage.Result_AddOrRemoveUser.QUEUE_IS_FINAL -> api.answerCallbackQuery(query, "\u26A0\uFE0F AccessToFinalQueueException")
|
||||||
|
Storage.Result_AddOrRemoveUser.ALREADY_IN_STATE -> api.answerCallbackQuery(query, "")
|
||||||
|
is Storage.Result_AddOrRemoveUser.SUCCESSFUL -> {
|
||||||
|
api.answerCallbackQuery(query, "\u2705 Popped")
|
||||||
|
api.editMessageText(query.message, this.__fmtQueueMessage(result.updatedQueue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"qup" -> when (val result = this.storage.moveUserToStartOfQueue(query.message.chatId, query.message.messageId, query.user.id)) {
|
||||||
|
Storage.Result_MoveToStartOrEnd.QUEUE_NOT_FOUND -> api.answerCallbackQuery(query, "\u26A0\uFE0F NullPointerException")
|
||||||
|
Storage.Result_MoveToStartOrEnd.NOT_IN_QUEUE -> api.answerCallbackQuery(query, "\u26A0\uFE0F UserNotFoundException")
|
||||||
|
Storage.Result_MoveToStartOrEnd.ON_EDGE -> api.answerCallbackQuery(query, "\u26A0\uFE0F Can't accelerate")
|
||||||
|
Storage.Result_MoveToStartOrEnd.QUEUE_IS_FINAL -> api.answerCallbackQuery(query, "\u26A0\uFE0F AccessToFinalQueueException")
|
||||||
|
is Storage.Result_MoveToStartOrEnd.SUCCESSFUL -> {
|
||||||
|
api.answerCallbackQuery(query, "\u2705 Time goes faster...")
|
||||||
|
api.editMessageText(query.message, this.__fmtQueueMessage(result.updatedQueue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"qdown" -> when (val result = this.storage.moveUserToEndOfQueue(query.message.chatId, query.message.messageId, query.user.id)) {
|
||||||
|
Storage.Result_MoveToStartOrEnd.QUEUE_NOT_FOUND -> api.answerCallbackQuery(query, "\u26A0\uFE0F NullPointerException")
|
||||||
|
Storage.Result_MoveToStartOrEnd.NOT_IN_QUEUE -> api.answerCallbackQuery(query, "\u26A0\uFE0F UserNotFoundException")
|
||||||
|
Storage.Result_MoveToStartOrEnd.ON_EDGE -> api.answerCallbackQuery(query, "\u26A0\uFE0F Can't slow down")
|
||||||
|
Storage.Result_MoveToStartOrEnd.QUEUE_IS_FINAL -> api.answerCallbackQuery(query, "\u26A0\uFE0F AccessToFinalQueueException")
|
||||||
|
is Storage.Result_MoveToStartOrEnd.SUCCESSFUL -> {
|
||||||
|
api.answerCallbackQuery(query, "\u2705 Time goes slower...")
|
||||||
|
api.editMessageText(query.message, this.__fmtQueueMessage(result.updatedQueue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"qprev" -> when (val result = this.storage.moveUserToPrevQueue(query.message.chatId, query.message.messageId, query.user.id)) {
|
||||||
|
Storage.Result_MoveToNextOrPrevQueue.QUEUE_NOT_FOUND -> api.answerCallbackQuery(query, "\u26A0\uFE0F NullPointerException")
|
||||||
|
Storage.Result_MoveToNextOrPrevQueue.QUEUE_IS_FINAL -> api.answerCallbackQuery(query, "\u26A0\uFE0F AccessToFinalQueueException")
|
||||||
|
Storage.Result_MoveToNextOrPrevQueue.NOT_IN_QUEUE -> api.answerCallbackQuery(query, "\u26A0\uFE0F UserNotFoundException")
|
||||||
|
Storage.Result_MoveToNextOrPrevQueue.ON_EDGE -> api.answerCallbackQuery(query, "\u26A0\uFE0F You already at first queue")
|
||||||
|
is Storage.Result_MoveToNextOrPrevQueue.SUCCESSFUL -> {
|
||||||
|
api.answerCallbackQuery(query, "\u2705 Queue changed")
|
||||||
|
api.editMessageText(query.message, this.__fmtQueueMessage(result.updatedQueue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"qnext" -> when (val result = this.storage.moveUserToNextQueue(query.message.chatId, query.message.messageId, query.user.id)) {
|
||||||
|
Storage.Result_MoveToNextOrPrevQueue.QUEUE_NOT_FOUND -> api.answerCallbackQuery(query, "\u26A0\uFE0F NullPointerException")
|
||||||
|
Storage.Result_MoveToNextOrPrevQueue.QUEUE_IS_FINAL -> api.answerCallbackQuery(query, "\u26A0\uFE0F AccessToFinalQueueException")
|
||||||
|
Storage.Result_MoveToNextOrPrevQueue.NOT_IN_QUEUE -> api.answerCallbackQuery(query, "\u26A0\uFE0F UserNotFoundException")
|
||||||
|
Storage.Result_MoveToNextOrPrevQueue.ON_EDGE -> api.answerCallbackQuery(query, "\u26A0\uFE0F You already at last queue")
|
||||||
|
is Storage.Result_MoveToNextOrPrevQueue.SUCCESSFUL -> {
|
||||||
|
api.answerCallbackQuery(query, "\u2705 Queue changed")
|
||||||
|
api.editMessageText(query.message, this.__fmtQueueMessage(result.updatedQueue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun __checkPermissions(api: TelegramBotApi, chat: Long, user: Long, permission: ModeratorPermissions.PermissionType): Boolean =
|
||||||
|
true|| api.isChatOwner(chat, user) || this.storage.getPermissions(chat, user)?.get(permission) ?: false
|
||||||
|
|
||||||
|
|
||||||
|
private fun _createQueue(api: TelegramBotApi, message: Message, commandArgs: String?) {
|
||||||
|
if (!this.__checkPermissions(api, message.chatId, message.fromUser.id, ModeratorPermissions.PermissionType.CAN_CREATE_QUEUES)) {
|
||||||
|
api.sendMessage(message, "<b>\u26A0\uFE0F У вас здесь нет власти чтобы создавать куеуе</b>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commandArgs.isNullOrBlank()) {
|
||||||
|
api.sendMessage(message, "<b>\u26A0\uFE0F Сдесь могла быть ваша куеуе, но вы не указали ее название</b>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val names = commandArgs.split(Parsers.spaceSeparator)
|
||||||
|
if (!names.all(Parsers::checkIsNameValid)) {
|
||||||
|
api.sendMessage(message, "<b>\u26A0\uFE0F Сдесь могла быть ваша куеуе, но вы не умеете в названия переменных</b>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val setName = names.first()
|
||||||
|
val queueNames = names.slice(1..<names.size)
|
||||||
|
|
||||||
|
|
||||||
|
val queueMessage = api.sendMessage(
|
||||||
|
message,
|
||||||
|
this.__fmtQueueMessage(EmptyFakeQueueSet(setName, queueNames)),
|
||||||
|
if (queueNames.isEmpty()) Keyboards.singleQueueKeyboard else Keyboards.multiQueueKeyboard
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
|
||||||
|
this.storage.allocQueue(message.chatId, queueMessage.messageId, setName, queueNames)
|
||||||
|
return
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
api.editMessageText(queueMessage, "<b>\u274C Не удалось создать куеуе в базе данных</b>", keepButtons = false)
|
||||||
|
throw t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun __fmtQueueMessage(queueSet: QueueSet): String = buildString {
|
||||||
|
this@buildString.append("<b>").append(if (queueSet.isOpen) "open" else "final").append("</b>")
|
||||||
|
|
||||||
|
this@buildString.append(" куеуе ")
|
||||||
|
this@buildString.append("<u><b><i>").append(queueSet.name).append("</i></b></u>")
|
||||||
|
this@buildString.append(" {\n")
|
||||||
|
|
||||||
|
for (queue in queueSet.queues) {
|
||||||
|
queue.name?.let { qname ->
|
||||||
|
this@buildString.append("<u>").append(qname).append("</u>:\n")
|
||||||
|
}
|
||||||
|
queue.members.forEachIndexed { i, u ->
|
||||||
|
if (u != null) {
|
||||||
|
this@buildString.append("<code> ").append(i).append(" </code>")
|
||||||
|
this@buildString.append("<a href='tg://user?id=").append(u.telegramId).append("'>").append(u.displayName).append("</a>\n")
|
||||||
|
} else {
|
||||||
|
this@buildString.append("<code> ").append(i).append("</code>\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this@buildString.append("}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun _closeQueue(api: TelegramBotApi, message: Message, commandArgs: String?) {
|
||||||
|
if (!this.__checkPermissions(api, message.chatId, message.fromUser.id, ModeratorPermissions.PermissionType.CAN_CLOSE_OR_OPEN_QUEUES)) {
|
||||||
|
api.sendMessage(message, "<b>\u26A0\uFE0F AccessDeniedException</b>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!commandArgs.isNullOrBlank()) {
|
||||||
|
api.sendMessage(message, "<b>\u26A0\uFE0F TypeError: Too many arguments</b>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val queueMessage = message.replyToMessage
|
||||||
|
if (queueMessage == null) {
|
||||||
|
api.sendMessage(message, "<b>\u26A0\uFE0F NullPointerException</b>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
when (val result = this.storage.closeQueue(message.chatId, queueMessage.messageId)) {
|
||||||
|
Storage.Result_QueueCloseOrOpen.ALREADY_IN_STATE -> {
|
||||||
|
api.sendMessage(message, "<b>\u26A0\uFE0F You are too late, it's already final</b>")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Storage.Result_QueueCloseOrOpen.QUEUE_NOT_FOUND -> {
|
||||||
|
api.sendMessage(message, "<b>\u26A0\uFE0F NullPointerException</b>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
is Storage.Result_QueueCloseOrOpen.SUCCESSFUL -> {
|
||||||
|
api.editMessageText(queueMessage, this.__fmtQueueMessage(result.updatedQueue))
|
||||||
|
api.sendMessage(message, "<b>\u2705 Queue finalized</b>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun _openQueue(api: TelegramBotApi, message: Message, commandArgs: String?) {
|
||||||
|
if (!this.__checkPermissions(api, message.chatId, message.fromUser.id, ModeratorPermissions.PermissionType.CAN_CLOSE_OR_OPEN_QUEUES)) {
|
||||||
|
api.sendMessage(message, "<b>\u26A0\uFE0F AccessDeniedException</b>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!commandArgs.isNullOrBlank()) {
|
||||||
|
api.sendMessage(message, "<b>\u26A0\uFE0F TypeError: Too many arguments</b>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val queueMessage = message.replyToMessage
|
||||||
|
if (queueMessage == null) {
|
||||||
|
api.sendMessage(message, "<b>\u26A0\uFE0F NullPointerException</b>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
when (val result = this.storage.reopenQueue(message.chatId, queueMessage.messageId)) {
|
||||||
|
Storage.Result_QueueCloseOrOpen.ALREADY_IN_STATE -> {
|
||||||
|
api.sendMessage(message, "<b>\u26A0\uFE0F You are so early, it's mutable yet</b>")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Storage.Result_QueueCloseOrOpen.QUEUE_NOT_FOUND -> {
|
||||||
|
api.sendMessage(message, "<b>\u26A0\uFE0F NullPointerException</b>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
is Storage.Result_QueueCloseOrOpen.SUCCESSFUL -> {
|
||||||
|
api.editMessageText(queueMessage, this.__fmtQueueMessage(result.updatedQueue))
|
||||||
|
api.sendMessage(message, "<b>\u2705 Queue finalized</b>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.impl
|
||||||
|
|
||||||
|
import kotlin.jvm.JvmStatic
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.telegram.api.InlineButton
|
||||||
|
|
||||||
|
internal object Keyboards {
|
||||||
|
@JvmStatic
|
||||||
|
val singleQueueKeyboard = listOf(
|
||||||
|
listOf(InlineButton("push", "qpush"), InlineButton("pop", "qpop")),
|
||||||
|
listOf(InlineButton("up", "qup"), InlineButton("down", "qdown"))
|
||||||
|
)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
val multiQueueKeyboard: List<List<InlineButton>> = this.singleQueueKeyboard
|
||||||
|
.plusElement(listOf(InlineButton("prev", "qprev"), InlineButton("next", "qnext")))
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.impl
|
||||||
|
|
||||||
|
import kotlin.jvm.JvmStatic
|
||||||
|
|
||||||
|
internal object Parsers {
|
||||||
|
@JvmStatic
|
||||||
|
private val commandPattern = Regex("""^/([a-zA-Z0-9_]+)(?:@[a-zA-Z0-9_]*)?(?:\s+([\s\S]+))?$""")
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun tryParseCommand(text: String): Pair<String, String?>? {
|
||||||
|
val match = this.commandPattern.matchEntire(text) ?: return null
|
||||||
|
val command = match.groupValues[1]
|
||||||
|
val arguments = match.groups[2]?.value
|
||||||
|
return Pair(command, arguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
private val validNamePattern = Regex("""^(?!\d)[\p{L}_0-9]+$""")
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun checkIsNameValid(name: String): Boolean = this.validNamePattern.matchEntire(name) != null
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
val spaceSeparator = Regex("""\s++""")
|
||||||
|
}
|
9
settings.gradle.kts
Normal file
9
settings.gradle.kts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
include(":storage-api")
|
||||||
|
include(":telegram-api")
|
||||||
|
|
||||||
|
include(":impl")
|
||||||
|
|
||||||
|
include(":telegram-api-impl")
|
||||||
|
include(":storage-jdbc-sqlite")
|
||||||
|
|
||||||
|
include(":exe")
|
99
setup_db.sql
Normal file
99
setup_db.sql
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
CREATE TABLE users
|
||||||
|
(
|
||||||
|
_internal_id INTEGER UNIQUE NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"telegram::user_id" INTEGER UNIQUE NOT NULL,
|
||||||
|
display_name TEXT NOT NULL
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
CREATE TABLE chats
|
||||||
|
(
|
||||||
|
_internal_id INTEGER UNIQUE NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"telegram::chat_id" INTEGER UNIQUE NOT NULL
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
CREATE TABLE moderators
|
||||||
|
(
|
||||||
|
user INTEGER NOT NULL REFERENCES users (_internal_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
chat INTEGER NOT NULL REFERENCES chats (_internal_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
can_create_queues INTEGER NOT NULL DEFAULT FALSE,
|
||||||
|
can_finalize_queues INTEGER NOT NULL DEFAULT FALSE,
|
||||||
|
|
||||||
|
UNIQUE (user, chat)
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
CREATE TABLE queue_sets
|
||||||
|
(
|
||||||
|
_internal_id INTEGER UNIQUE NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
chat INTEGER NOT NULL REFERENCES chats (_internal_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
"telegram::message_id" INTEGER NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
is_open INTEGER NOT NULL DEFAULT TRUE,
|
||||||
|
|
||||||
|
UNIQUE (chat, "telegram::message_id")
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
CREATE TABLE queues
|
||||||
|
(
|
||||||
|
_internal_id INTEGER UNIQUE NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
queue_set INTEGER NOT NULL REFERENCES queue_sets (_internal_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
ordinal INTEGER NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE (queue_set, ordinal)
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE "user->queue"
|
||||||
|
(
|
||||||
|
user INTEGER NOT NULL REFERENCES users (_internal_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
queue_set INTEGER NOT NULL REFERENCES queue_sets (_internal_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
queue INTEGER REFERENCES queues (_internal_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
ordinal INTEGER NOT NULL CHECK ( ordinal >= 0 ),
|
||||||
|
|
||||||
|
UNIQUE (user, queue_set),
|
||||||
|
UNIQUE (queue_set, queue, ordinal)
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
CREATE TRIGGER "user->queue::check_insert_queue_in_set"
|
||||||
|
BEFORE INSERT
|
||||||
|
ON "user->queue"
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN NEW.queue IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE WHEN queues.queue_set != NEW.queue_set THEN raise(ABORT, 'Queue references another set') END
|
||||||
|
FROM queues
|
||||||
|
WHERE queues._internal_id = NEW.queue;
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER "user->queue::check_insert_null_queue"
|
||||||
|
BEFORE INSERT
|
||||||
|
ON "user->queue"
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN NEW.queue IS NULL
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE WHEN count() > 0 THEN raise(ABORT, 'Cant use NULL when set contains queues') END
|
||||||
|
FROM queues
|
||||||
|
WHERE queues.queue_set = NEW.queue_set;
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER "user->queue::check_update_queue_in_set"
|
||||||
|
BEFORE UPDATE OF queue, queue_set
|
||||||
|
ON "user->queue"
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN NEW.queue IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE WHEN queues.queue_set != NEW.queue_set THEN raise(ABORT, 'Queue references another set') END
|
||||||
|
FROM queues
|
||||||
|
WHERE queues._internal_id = NEW.queue;
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER "user->queue::check_update_null_queue"
|
||||||
|
BEFORE UPDATE OF queue, queue_set
|
||||||
|
ON "user->queue"
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN NEW.queue IS NULL
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE WHEN count() > 0 THEN raise(ABORT, 'Cant use NULL when set contains queues') END
|
||||||
|
FROM queues
|
||||||
|
WHERE queues.queue_set = NEW.queue_set;
|
||||||
|
END;
|
25
storage-api/build.gradle.kts
Normal file
25
storage-api/build.gradle.kts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import ru.landgrafhomyak.kotlin.kmp_gradle_build_helper.defineAllMultiplatformTargets
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven("https://maven.landgrafhomyak.ru/")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath("ru.landgrafhomyak.kotlin:kotlin-mpp-gradle-build-helper:v0.2k2.0.20")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("multiplatform") version "2.0.20"
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(21)
|
||||||
|
defineAllMultiplatformTargets()
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.storage.api
|
||||||
|
|
||||||
|
interface ModeratorPermissions {
|
||||||
|
enum class PermissionType {
|
||||||
|
CAN_CREATE_QUEUES,
|
||||||
|
CAN_CLOSE_OR_OPEN_QUEUES
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun get(permission: PermissionType): Boolean
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.storage.api
|
||||||
|
|
||||||
|
interface Queue {
|
||||||
|
val name: String?
|
||||||
|
val members: List<User?>
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.storage.api
|
||||||
|
|
||||||
|
interface QueueSet {
|
||||||
|
val name: String
|
||||||
|
val queues: List<Queue>
|
||||||
|
val isOpen: Boolean
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.storage.api
|
||||||
|
|
||||||
|
@Suppress("ConvertObjectToDataObject")
|
||||||
|
interface Storage {
|
||||||
|
fun allocQueue(chat: Long, message: Long, setName: String, queueNames: List<String> = emptyList())
|
||||||
|
|
||||||
|
@Suppress("ClassName")
|
||||||
|
sealed class Result_QueueCloseOrOpen {
|
||||||
|
class SUCCESSFUL(val updatedQueue: QueueSet) : Result_QueueCloseOrOpen()
|
||||||
|
object QUEUE_NOT_FOUND : Result_QueueCloseOrOpen()
|
||||||
|
object ALREADY_IN_STATE : Result_QueueCloseOrOpen()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeQueue(chat: Long, message: Long): Result_QueueCloseOrOpen
|
||||||
|
fun reopenQueue(chat: Long, message: Long): Result_QueueCloseOrOpen
|
||||||
|
|
||||||
|
@Suppress("ClassName")
|
||||||
|
sealed class Result_AddOrRemoveUser {
|
||||||
|
class SUCCESSFUL(val updatedQueue: QueueSet) : Result_AddOrRemoveUser()
|
||||||
|
object QUEUE_NOT_FOUND : Result_AddOrRemoveUser()
|
||||||
|
object QUEUE_OVERFLOW : Result_AddOrRemoveUser()
|
||||||
|
object ALREADY_IN_STATE : Result_AddOrRemoveUser()
|
||||||
|
object QUEUE_IS_FINAL : Result_AddOrRemoveUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addUserToQueue(chat: Long, message: Long, user: Long, userDisplayName: String): Result_AddOrRemoveUser
|
||||||
|
fun removeUserFromQueue(chat: Long, message: Long, user: Long): Result_AddOrRemoveUser
|
||||||
|
|
||||||
|
|
||||||
|
@Suppress("ClassName")
|
||||||
|
sealed class Result_MoveToNextOrPrevQueue {
|
||||||
|
class SUCCESSFUL(val updatedQueue: QueueSet) : Result_MoveToNextOrPrevQueue()
|
||||||
|
object QUEUE_NOT_FOUND : Result_MoveToNextOrPrevQueue()
|
||||||
|
object NOT_IN_QUEUE : Result_MoveToNextOrPrevQueue()
|
||||||
|
object ON_EDGE : Result_MoveToNextOrPrevQueue()
|
||||||
|
object QUEUE_IS_FINAL : Result_MoveToNextOrPrevQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun moveUserToNextQueue(chat: Long, message: Long, user: Long): Result_MoveToNextOrPrevQueue
|
||||||
|
fun moveUserToPrevQueue(chat: Long, message: Long, user: Long): Result_MoveToNextOrPrevQueue
|
||||||
|
|
||||||
|
@Suppress("ClassName")
|
||||||
|
sealed class Result_MoveToStartOrEnd {
|
||||||
|
class SUCCESSFUL(val updatedQueue: QueueSet) : Result_MoveToStartOrEnd()
|
||||||
|
object QUEUE_NOT_FOUND : Result_MoveToStartOrEnd()
|
||||||
|
object NOT_IN_QUEUE : Result_MoveToStartOrEnd()
|
||||||
|
object ON_EDGE : Result_MoveToStartOrEnd()
|
||||||
|
object QUEUE_IS_FINAL : Result_MoveToStartOrEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun moveUserToStartOfQueue(chat: Long, message: Long, user: Long): Result_MoveToStartOrEnd
|
||||||
|
fun moveUserToEndOfQueue(chat: Long, message: Long, user: Long): Result_MoveToStartOrEnd
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return `false` on success, `true` if already moderator
|
||||||
|
*/
|
||||||
|
fun addModerator(chat: Long, user: Long): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return `false` on success, `true` if already not moderator
|
||||||
|
*/
|
||||||
|
fun removeModerator(chat: Long, message: Long): Boolean
|
||||||
|
|
||||||
|
fun getPermissions(chat: Long, user: Long): ModeratorPermissions?
|
||||||
|
|
||||||
|
@Suppress("ClassName")
|
||||||
|
sealed class Result_SetModeratorPermission {
|
||||||
|
class SUCCESSFUL(val newPermissions: QueueSet) : Result_SetModeratorPermission()
|
||||||
|
object NOT_MODERATOR : Result_SetModeratorPermission()
|
||||||
|
object ALREADY_SET : Result_SetModeratorPermission()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPermission(chat: Long, user: Long, permissionType: ModeratorPermissions.PermissionType, value: Boolean): Result_SetModeratorPermission
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.storage.api
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
val telegramId: Long
|
||||||
|
val displayName: String
|
||||||
|
}
|
33
storage-jdbc-sqlite/build.gradle.kts
Normal file
33
storage-jdbc-sqlite/build.gradle.kts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven("https://maven.landgrafhomyak.ru/")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath("ru.landgrafhomyak.kotlin:kotlin-mpp-gradle-build-helper:v0.2k2.0.20")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("multiplatform") version "2.0.20"
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(21)
|
||||||
|
jvm()
|
||||||
|
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
jvmMain {
|
||||||
|
dependencies {
|
||||||
|
compileOnly(project(":storage-api"))
|
||||||
|
implementation("org.xerial:sqlite-jdbc:3.47.2.0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,539 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.storage.sqlite_jdbc
|
||||||
|
|
||||||
|
import java.sql.Connection
|
||||||
|
import java.sql.PreparedStatement
|
||||||
|
import java.sql.ResultSet
|
||||||
|
import java.sql.Types
|
||||||
|
import kotlin.contracts.ExperimentalContracts
|
||||||
|
import kotlin.contracts.InvocationKind
|
||||||
|
import kotlin.contracts.contract
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.storage.api.ModeratorPermissions
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.storage.api.QueueSet
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.storage.api.Storage
|
||||||
|
|
||||||
|
class JdbcSqliteStorage(
|
||||||
|
private val connection: Connection
|
||||||
|
) : Storage {
|
||||||
|
|
||||||
|
init {
|
||||||
|
this.connection.autoCommit = false
|
||||||
|
this.connection.executeStatement("ROLLBACK TRANSACTION")
|
||||||
|
this.connection.executeStatement("PRAGMA foreign_keys=TRUE")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
private fun _nothingInserted(): Nothing = throw RuntimeException("Nothing inserted, but must be")
|
||||||
|
|
||||||
|
private fun PreparedStatement.executeQueryReturningRowId(): Long = this@executeQueryReturningRowId.executeQuery { rs ->
|
||||||
|
if (!rs.next())
|
||||||
|
this@JdbcSqliteStorage._nothingInserted()
|
||||||
|
return@executeQueryReturningRowId rs.getLong(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun allocQueue(chat: Long, message: Long, setName: String, queueNames: List<String>): Unit = this.connection.transaction { conn ->
|
||||||
|
val chatDbId: Long
|
||||||
|
conn.prepareStatement(
|
||||||
|
"""
|
||||||
|
INSERT INTO chats("telegram::chat_id")
|
||||||
|
VALUES (?)
|
||||||
|
ON CONFLICT DO UPDATE SET "telegram::chat_id" = EXCLUDED."telegram::chat_id"
|
||||||
|
RETURNING _internal_id
|
||||||
|
"""
|
||||||
|
) { ps ->
|
||||||
|
ps.setLong(1, chat)
|
||||||
|
chatDbId = ps.executeQueryReturningRowId()
|
||||||
|
}
|
||||||
|
|
||||||
|
val queueSetDbId: Long
|
||||||
|
this.connection.prepareStatement(
|
||||||
|
"""
|
||||||
|
INSERT INTO queue_sets(chat, "telegram::message_id", name)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
RETURNING _internal_id
|
||||||
|
"""
|
||||||
|
) { ps ->
|
||||||
|
ps.setLong(1, chatDbId)
|
||||||
|
ps.setLong(2, message)
|
||||||
|
ps.setString(3, setName)
|
||||||
|
queueSetDbId = ps.executeQueryReturningRowId()
|
||||||
|
}
|
||||||
|
this.connection.prepareStatement(
|
||||||
|
"""
|
||||||
|
INSERT INTO queues(queue_set, name, ordinal)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
"""
|
||||||
|
) { ps ->
|
||||||
|
ps.setLong(1, queueSetDbId)
|
||||||
|
|
||||||
|
queueNames.forEachIndexed { i, n ->
|
||||||
|
ps.setString(2, n)
|
||||||
|
ps.setInt(3, i)
|
||||||
|
ps.addBatch()
|
||||||
|
}
|
||||||
|
ps.executeBatch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
private fun ResultSet.__formatQueue(ordinalColumnIndex: Int, tgUidColumnIndex: Int, displayNameColumnIndex: Int): List<UserStruct?> {
|
||||||
|
val users = ArrayList<UserStruct?>()
|
||||||
|
while (this@__formatQueue.next()) {
|
||||||
|
val userOrdinal = this@__formatQueue.getInt(ordinalColumnIndex)
|
||||||
|
val user = UserStruct(
|
||||||
|
telegramId = this@__formatQueue.getLong(tgUidColumnIndex),
|
||||||
|
displayName = this@__formatQueue.getString(displayNameColumnIndex)
|
||||||
|
)
|
||||||
|
while (users.size < userOrdinal)
|
||||||
|
users.add(null)
|
||||||
|
|
||||||
|
users.add(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
private fun _formatQueueSet(conn: Connection, qs: FoundQueue): QueueSet {
|
||||||
|
val queuesMeta = conn.prepareStatement(
|
||||||
|
"""
|
||||||
|
SELECT _internal_id, name
|
||||||
|
FROM queues
|
||||||
|
WHERE queue_set = ?
|
||||||
|
ORDER BY ordinal ASC
|
||||||
|
"""
|
||||||
|
) { ps ->
|
||||||
|
ps.setLong(1, qs.dbId)
|
||||||
|
return@prepareStatement ps.executeQuery()
|
||||||
|
.mapToThenClose(LinkedHashSet()) { rs -> Pair(rs.getLong(1), rs.getString(2)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val queues = ArrayList<QueueStruct>()
|
||||||
|
conn.prepareStatement(
|
||||||
|
"""
|
||||||
|
SELECT "user->queue".ordinal, users."telegram::user_id", users.display_name
|
||||||
|
FROM "user->queue"
|
||||||
|
INNER JOIN users ON users._internal_id = "user->queue".user
|
||||||
|
WHERE "user->queue".${if (queuesMeta.isEmpty()) "queue_set" else "queue"} = ?
|
||||||
|
ORDER BY "user->queue".ordinal ASC
|
||||||
|
"""
|
||||||
|
) { ps ->
|
||||||
|
if (queuesMeta.isEmpty()) {
|
||||||
|
ps.setLong(1, qs.dbId)
|
||||||
|
ps.executeQuery { rs -> queues.add(QueueStruct(null, rs.__formatQueue(1, 2, 3))) }
|
||||||
|
} else {
|
||||||
|
for ((queueDbId, queueName) in queuesMeta) {
|
||||||
|
ps.setLong(1, queueDbId)
|
||||||
|
ps.executeQuery { rs -> queues.add(QueueStruct(queueName, rs.__formatQueue(1, 2, 3))) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return QueueSetStruct(
|
||||||
|
name = qs.name,
|
||||||
|
queues = queues,
|
||||||
|
isOpen = qs.isOpen
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FoundQueue(
|
||||||
|
val dbId: Long,
|
||||||
|
val isOpen: Boolean,
|
||||||
|
val name: String
|
||||||
|
) {
|
||||||
|
fun withIsOpen(newValue: Boolean) = FoundQueue(
|
||||||
|
dbId = this.dbId,
|
||||||
|
isOpen = newValue,
|
||||||
|
name = this.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("FunctionName", "IMPLICIT_NOTHING_TYPE_ARGUMENT_AGAINST_NOT_NOTHING_EXPECTED_TYPE")
|
||||||
|
private fun _findQueueSet(conn: Connection, chat: Long, message: Long): FoundQueue? = conn.prepareStatement(
|
||||||
|
"""
|
||||||
|
SELECT queue_sets._internal_id, queue_sets.is_open, queue_sets.name
|
||||||
|
FROM queue_sets
|
||||||
|
INNER JOIN chats on chats._internal_id = queue_sets.chat
|
||||||
|
WHERE chats."telegram::chat_id" = ? AND queue_sets."telegram::message_id" = ?
|
||||||
|
"""
|
||||||
|
) { ps ->
|
||||||
|
ps.setLong(1, chat)
|
||||||
|
ps.setLong(2, message)
|
||||||
|
ps.executeQuery { rs ->
|
||||||
|
if (rs.next())
|
||||||
|
return@_findQueueSet FoundQueue(rs.getLong(1), rs.getBoolean(2), rs.getString(3))
|
||||||
|
else
|
||||||
|
return@_findQueueSet null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
private fun _closeOrOpenQueue(chat: Long, message: Long, newIsOpen: Boolean): Storage.Result_QueueCloseOrOpen = this.connection.transaction { conn ->
|
||||||
|
val qs = this._findQueueSet(conn, chat, message) ?: return Storage.Result_QueueCloseOrOpen.QUEUE_NOT_FOUND
|
||||||
|
|
||||||
|
if (qs.isOpen == newIsOpen)
|
||||||
|
return Storage.Result_QueueCloseOrOpen.ALREADY_IN_STATE
|
||||||
|
|
||||||
|
conn.prepareStatement(
|
||||||
|
"""
|
||||||
|
UPDATE queue_sets
|
||||||
|
SET is_open = ?
|
||||||
|
WHERE _internal_id = ?
|
||||||
|
"""
|
||||||
|
) { ps ->
|
||||||
|
ps.setBoolean(1, newIsOpen)
|
||||||
|
ps.setLong(2, qs.dbId)
|
||||||
|
ps.executeUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Storage.Result_QueueCloseOrOpen.SUCCESSFUL(this._formatQueueSet(conn, qs.withIsOpen(newIsOpen)))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun closeQueue(chat: Long, message: Long): Storage.Result_QueueCloseOrOpen = this._closeOrOpenQueue(chat, message, false)
|
||||||
|
|
||||||
|
override fun reopenQueue(chat: Long, message: Long): Storage.Result_QueueCloseOrOpen = this._closeOrOpenQueue(chat, message, true)
|
||||||
|
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
private inline fun ResultSet._getFirstFreeOrdinal(ordinalColumnIndex: Int = 1, extraActions: (ResultSet) -> Unit = {}): Int {
|
||||||
|
var last = -1
|
||||||
|
while (this@_getFirstFreeOrdinal.next()) {
|
||||||
|
val current = this@_getFirstFreeOrdinal.getInt(ordinalColumnIndex)
|
||||||
|
if (last + 1 < current)
|
||||||
|
return last + 1
|
||||||
|
last = current
|
||||||
|
extraActions(this@_getFirstFreeOrdinal)
|
||||||
|
}
|
||||||
|
return last + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalContracts::class)
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
private inline fun <R> Connection._prepareStatementWithQueueDbId(
|
||||||
|
queueIdParamIndex: Int,
|
||||||
|
queueSetDbId: Long,
|
||||||
|
queueDbId: Long?,
|
||||||
|
queryBuilder: (isNull: Boolean) -> String,
|
||||||
|
extra: (PreparedStatement) -> R
|
||||||
|
): R {
|
||||||
|
contract {
|
||||||
|
callsInPlace(queryBuilder, InvocationKind.EXACTLY_ONCE)
|
||||||
|
callsInPlace(extra, InvocationKind.EXACTLY_ONCE)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queueDbId == null) {
|
||||||
|
return this@_prepareStatementWithQueueDbId.prepareStatement(queryBuilder(true)) { ps ->
|
||||||
|
ps.setLong(queueIdParamIndex, queueSetDbId)
|
||||||
|
return@prepareStatement extra(ps)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return this@_prepareStatementWithQueueDbId.prepareStatement(queryBuilder(false)) { ps ->
|
||||||
|
ps.setLong(queueIdParamIndex, queueDbId)
|
||||||
|
return@prepareStatement extra(ps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addUserToQueue(chat: Long, message: Long, user: Long, userDisplayName: String): Storage.Result_AddOrRemoveUser = this.connection.transaction { conn ->
|
||||||
|
val qs = this._findQueueSet(conn, chat, message) ?: return Storage.Result_AddOrRemoveUser.QUEUE_NOT_FOUND
|
||||||
|
if (!qs.isOpen) return Storage.Result_AddOrRemoveUser.QUEUE_IS_FINAL
|
||||||
|
|
||||||
|
|
||||||
|
val userDbId: Long
|
||||||
|
conn.prepareStatement(
|
||||||
|
"""
|
||||||
|
INSERT INTO users("telegram::user_id", display_name)
|
||||||
|
VALUES (?, ?)
|
||||||
|
ON CONFLICT("telegram::user_id") DO UPDATE SET display_name = EXCLUDED.display_name
|
||||||
|
RETURNING _internal_id
|
||||||
|
"""
|
||||||
|
) { ps ->
|
||||||
|
ps.setLong(1, user)
|
||||||
|
ps.setString(2, userDisplayName)
|
||||||
|
userDbId = ps.executeQueryReturningRowId()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
conn.prepareStatement(
|
||||||
|
"""
|
||||||
|
SELECT TRUE
|
||||||
|
FROM "user->queue"
|
||||||
|
WHERE queue_set = ? AND user = ?
|
||||||
|
"""
|
||||||
|
) { ps ->
|
||||||
|
ps.setLong(1, qs.dbId)
|
||||||
|
ps.setLong(2, userDbId)
|
||||||
|
ps.executeQuery { rs ->
|
||||||
|
if (rs.next())
|
||||||
|
return Storage.Result_AddOrRemoveUser.ALREADY_IN_STATE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val firstQueueDbId: Long?
|
||||||
|
conn.prepareStatement(
|
||||||
|
"""
|
||||||
|
SELECT _internal_id
|
||||||
|
FROM queues
|
||||||
|
WHERE queue_set = ?
|
||||||
|
ORDER BY ordinal
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
) { ps ->
|
||||||
|
ps.setLong(1, qs.dbId)
|
||||||
|
firstQueueDbId = ps.executeQuery { rs -> if (!rs.next()) return@executeQuery null else return@executeQuery rs.getLong(1) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val userOrdinal: Int
|
||||||
|
conn._prepareStatementWithQueueDbId(
|
||||||
|
1, qs.dbId, firstQueueDbId,
|
||||||
|
{ isNull ->
|
||||||
|
"""
|
||||||
|
SELECT ordinal
|
||||||
|
FROM "user->queue"
|
||||||
|
WHERE ${if (isNull) "queue_set" else "queue"} = ?
|
||||||
|
ORDER BY ordinal ASC
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
) { ps ->
|
||||||
|
userOrdinal = ps.executeQuery { rs -> rs._getFirstFreeOrdinal() }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
conn.prepareStatement(
|
||||||
|
"""
|
||||||
|
INSERT INTO "user->queue"(user, queue_set, queue, ordinal)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
"""
|
||||||
|
) { ps ->
|
||||||
|
ps.setLong(1, userDbId)
|
||||||
|
ps.setLong(2, qs.dbId)
|
||||||
|
if (firstQueueDbId == null)
|
||||||
|
ps.setNull(3, Types.INTEGER)
|
||||||
|
else
|
||||||
|
ps.setLong(3, firstQueueDbId)
|
||||||
|
ps.setInt(4, userOrdinal)
|
||||||
|
ps.executeUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Storage.Result_AddOrRemoveUser.SUCCESSFUL(this._formatQueueSet(conn, qs))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeUserFromQueue(chat: Long, message: Long, user: Long): Storage.Result_AddOrRemoveUser = this.connection.transaction { conn ->
|
||||||
|
val qs = this._findQueueSet(conn, chat, message) ?: return Storage.Result_AddOrRemoveUser.QUEUE_NOT_FOUND
|
||||||
|
if (!qs.isOpen) return Storage.Result_AddOrRemoveUser.QUEUE_IS_FINAL
|
||||||
|
|
||||||
|
|
||||||
|
conn.prepareStatement(
|
||||||
|
"""
|
||||||
|
DELETE FROM "user->queue"
|
||||||
|
WHERE queue_set = ? AND user = (SELECT _internal_id FROM users WHERE "telegram::user_id" = ?)
|
||||||
|
RETURNING TRUE
|
||||||
|
"""
|
||||||
|
) { ps ->
|
||||||
|
ps.setLong(1, qs.dbId)
|
||||||
|
ps.executeQuery { rs ->
|
||||||
|
if (!rs.next())
|
||||||
|
return Storage.Result_AddOrRemoveUser.ALREADY_IN_STATE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return Storage.Result_AddOrRemoveUser.SUCCESSFUL(this._formatQueueSet(conn, qs))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
private fun _moveUserToAnotherQueue(chat: Long, message: Long, user: Long, moveToNext: Boolean): Storage.Result_MoveToNextOrPrevQueue = this.connection.transaction { conn ->
|
||||||
|
val qs = this._findQueueSet(conn, chat, message) ?: return Storage.Result_MoveToNextOrPrevQueue.QUEUE_NOT_FOUND
|
||||||
|
if (!qs.isOpen) return Storage.Result_MoveToNextOrPrevQueue.QUEUE_IS_FINAL
|
||||||
|
|
||||||
|
val userDbId: Long
|
||||||
|
val queueDbId: Long
|
||||||
|
conn.prepareStatement(
|
||||||
|
"""
|
||||||
|
SELECT users._internal_id, "user->queue".queue
|
||||||
|
FROM "user->queue"
|
||||||
|
INNER JOIN users ON users._internal_id = "user->queue".user
|
||||||
|
WHERE users."telegram::user_id" = ? AND "user->queue".queue_set = ?
|
||||||
|
"""
|
||||||
|
) { ps ->
|
||||||
|
ps.setLong(1, user)
|
||||||
|
ps.setLong(2, qs.dbId)
|
||||||
|
ps.executeQuery { rs ->
|
||||||
|
if (!rs.next())
|
||||||
|
return@_moveUserToAnotherQueue Storage.Result_MoveToNextOrPrevQueue.NOT_IN_QUEUE
|
||||||
|
|
||||||
|
userDbId = rs.getLong(1)
|
||||||
|
queueDbId = rs.getLong(2)
|
||||||
|
if (rs.wasNull()) {
|
||||||
|
return@_moveUserToAnotherQueue Storage.Result_MoveToNextOrPrevQueue.ON_EDGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newQueueDbId: Long
|
||||||
|
conn.prepareStatement(
|
||||||
|
"""
|
||||||
|
SELECT _internal_id
|
||||||
|
FROM queues
|
||||||
|
WHERE queue_set = ?
|
||||||
|
AND ordinal ${if (moveToNext) '>' else '<'} (SELECT current_queue.ordinal
|
||||||
|
FROM queues AS current_queue
|
||||||
|
WHERE current_queue._internal_id = ?)
|
||||||
|
ORDER BY ordinal ${if (moveToNext) "ASC" else "DESC"}
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
) { ps ->
|
||||||
|
ps.setLong(1, qs.dbId)
|
||||||
|
ps.setLong(2, queueDbId)
|
||||||
|
ps.executeQuery { rs ->
|
||||||
|
if (!rs.next())
|
||||||
|
return@_moveUserToAnotherQueue Storage.Result_MoveToNextOrPrevQueue.ON_EDGE
|
||||||
|
newQueueDbId = rs.getLong(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newOrdinal: Int
|
||||||
|
conn.prepareStatement(
|
||||||
|
"""
|
||||||
|
SELECT ordinal
|
||||||
|
FROM "user->queue"
|
||||||
|
WHERE queue = ?
|
||||||
|
ORDER BY ordinal ASC
|
||||||
|
"""
|
||||||
|
) { ps ->
|
||||||
|
ps.setLong(1, newQueueDbId)
|
||||||
|
newOrdinal = ps.executeQuery { rs -> rs._getFirstFreeOrdinal() }
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.prepareStatement(
|
||||||
|
"""
|
||||||
|
UPDATE "user->queue"
|
||||||
|
SET queue = ?, ordinal = ?
|
||||||
|
WHERE user = ? AND queue_set = ?
|
||||||
|
"""
|
||||||
|
) { ps ->
|
||||||
|
ps.setLong(1, newQueueDbId)
|
||||||
|
ps.setInt(2, newOrdinal)
|
||||||
|
ps.setLong(3, userDbId)
|
||||||
|
ps.setLong(4, qs.dbId)
|
||||||
|
ps.executeUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Storage.Result_MoveToNextOrPrevQueue.SUCCESSFUL(this._formatQueueSet(conn, qs))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun moveUserToNextQueue(chat: Long, message: Long, user: Long): Storage.Result_MoveToNextOrPrevQueue =
|
||||||
|
this._moveUserToAnotherQueue(chat, message, user, true)
|
||||||
|
|
||||||
|
override fun moveUserToPrevQueue(chat: Long, message: Long, user: Long): Storage.Result_MoveToNextOrPrevQueue =
|
||||||
|
this._moveUserToAnotherQueue(chat, message, user, false)
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
private fun _moveUserInQueue(chat: Long, message: Long, user: Long, moveToStart: Boolean): Storage.Result_MoveToStartOrEnd = this.connection.transaction { conn ->
|
||||||
|
val qs = this._findQueueSet(conn, chat, message) ?: return Storage.Result_MoveToStartOrEnd.QUEUE_NOT_FOUND
|
||||||
|
if (!qs.isOpen) return Storage.Result_MoveToStartOrEnd.QUEUE_IS_FINAL
|
||||||
|
|
||||||
|
val userDbId: Long
|
||||||
|
val queueDbId: Long?
|
||||||
|
val currentOrdinal: Int
|
||||||
|
conn.prepareStatement(
|
||||||
|
"""
|
||||||
|
SELECT users._internal_id, "user->queue".queue, "user->queue".ordinal
|
||||||
|
FROM "user->queue"
|
||||||
|
INNER JOIN users ON users._internal_id = "user->queue".user
|
||||||
|
WHERE users."telegram::user_id" = ? AND "user->queue".queue_set = ?
|
||||||
|
"""
|
||||||
|
) { ps ->
|
||||||
|
ps.setLong(1, user)
|
||||||
|
ps.setLong(2, qs.dbId)
|
||||||
|
ps.executeQuery { rs ->
|
||||||
|
if (!rs.next())
|
||||||
|
return@_moveUserInQueue Storage.Result_MoveToStartOrEnd.NOT_IN_QUEUE
|
||||||
|
|
||||||
|
userDbId = rs.getLong(1)
|
||||||
|
queueDbId = rs.getLong(2).takeUnless { rs.wasNull() }
|
||||||
|
currentOrdinal = rs.getInt(3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newOrdinal: Int
|
||||||
|
conn._prepareStatementWithQueueDbId(
|
||||||
|
1, qs.dbId, queueDbId,
|
||||||
|
{ isNull ->
|
||||||
|
"""
|
||||||
|
SELECT ordinal
|
||||||
|
FROM "user->queue"
|
||||||
|
WHERE ${if (isNull) "queue_set" else "queue"} = ?
|
||||||
|
AND ordinal ${if (moveToStart) '<' else '>'} ?
|
||||||
|
ORDER BY ordinal ${if (moveToStart) "DESC" else "ASC"}
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
) { ps ->
|
||||||
|
ps.setInt(2, currentOrdinal)
|
||||||
|
newOrdinal = ps.executeQuery { rs ->
|
||||||
|
var last = currentOrdinal
|
||||||
|
if (moveToStart) {
|
||||||
|
while (rs.next()) {
|
||||||
|
val current = rs.getInt(1)
|
||||||
|
if (last - 1 > current)
|
||||||
|
return@executeQuery last - 1
|
||||||
|
last = current
|
||||||
|
}
|
||||||
|
if (last > 0)
|
||||||
|
return@executeQuery 0
|
||||||
|
else
|
||||||
|
return@_moveUserInQueue Storage.Result_MoveToStartOrEnd.ON_EDGE
|
||||||
|
} else {
|
||||||
|
while (rs.next()) {
|
||||||
|
val current = rs.getInt(1)
|
||||||
|
if (last + 1 < current)
|
||||||
|
return@executeQuery last + 1
|
||||||
|
last = current
|
||||||
|
}
|
||||||
|
return@executeQuery last + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.prepareStatement(
|
||||||
|
"""
|
||||||
|
UPDATE "user->queue"
|
||||||
|
SET ordinal = ?
|
||||||
|
WHERE user = ? AND queue_set = ?
|
||||||
|
"""
|
||||||
|
) { ps ->
|
||||||
|
ps.setInt(1, newOrdinal)
|
||||||
|
ps.setLong(2, userDbId)
|
||||||
|
ps.setLong(3, qs.dbId)
|
||||||
|
ps.executeUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Storage.Result_MoveToStartOrEnd.SUCCESSFUL(this._formatQueueSet(conn, qs))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun moveUserToStartOfQueue(chat: Long, message: Long, user: Long): Storage.Result_MoveToStartOrEnd =
|
||||||
|
this._moveUserInQueue(chat, message, user, true)
|
||||||
|
|
||||||
|
override fun moveUserToEndOfQueue(chat: Long, message: Long, user: Long): Storage.Result_MoveToStartOrEnd =
|
||||||
|
this._moveUserInQueue(chat, message, user, false)
|
||||||
|
|
||||||
|
|
||||||
|
override fun addModerator(chat: Long, user: Long): Boolean {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeModerator(chat: Long, message: Long): Boolean {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPermissions(chat: Long, user: Long): ModeratorPermissions? {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setPermission(chat: Long, user: Long, permissionType: ModeratorPermissions.PermissionType, value: Boolean): Storage.Result_SetModeratorPermission {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.storage.sqlite_jdbc
|
||||||
|
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.storage.api.Queue
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.storage.api.QueueSet
|
||||||
|
|
||||||
|
internal class QueueSetStruct(override val name: String, override val queues: List<Queue>, override val isOpen: Boolean) : QueueSet
|
@ -0,0 +1,7 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.storage.sqlite_jdbc
|
||||||
|
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.storage.api.Queue
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.storage.api.User
|
||||||
|
|
||||||
|
internal class QueueStruct(override val name: String?, override val members: List<User?>) :Queue {
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.storage.sqlite_jdbc
|
||||||
|
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.storage.api.User
|
||||||
|
|
||||||
|
internal class UserStruct(override val telegramId: Long, override val displayName: String) : User
|
@ -0,0 +1,100 @@
|
|||||||
|
@file:JvmName("ExtensionsKt")
|
||||||
|
@file:Suppress("SqlSourceToSinkFlow")
|
||||||
|
@file:OptIn(ExperimentalContracts::class)
|
||||||
|
|
||||||
|
package ru.landgrafhomyak.bgtu.db0.storage.sqlite_jdbc
|
||||||
|
|
||||||
|
import org.intellij.lang.annotations.Language
|
||||||
|
import java.sql.Connection
|
||||||
|
import java.sql.PreparedStatement
|
||||||
|
import java.sql.ResultSet
|
||||||
|
import kotlin.contracts.ExperimentalContracts
|
||||||
|
import kotlin.contracts.InvocationKind
|
||||||
|
import kotlin.contracts.contract
|
||||||
|
|
||||||
|
|
||||||
|
internal inline fun <R> Connection.transaction(t: (Connection) -> R): R {
|
||||||
|
contract {
|
||||||
|
callsInPlace(t, InvocationKind.EXACTLY_ONCE)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.prepareStatement("BEGIN TRANSACTION").use { ps -> ps.executeUpdate() }
|
||||||
|
val r: R
|
||||||
|
var isSuccessful = true
|
||||||
|
try {
|
||||||
|
r = t(this)
|
||||||
|
} catch (e1: Throwable) {
|
||||||
|
isSuccessful = false
|
||||||
|
try {
|
||||||
|
this.prepareStatement("ROLLBACK TRANSACTION").use { ps -> ps.executeUpdate() }
|
||||||
|
} catch (e2: Throwable) {
|
||||||
|
e1.addSuppressed(e2)
|
||||||
|
}
|
||||||
|
throw e1
|
||||||
|
} finally {
|
||||||
|
if (isSuccessful)
|
||||||
|
this.prepareStatement("COMMIT TRANSACTION").use { ps -> ps.executeUpdate() }
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal fun Connection.executeStatement(@Language("SQL") sql: String) {
|
||||||
|
this.prepareStatement(sql) { ps -> ps.execute() }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal inline fun <R> Connection.prepareStatement(@Language("SQL") sql: String, action: (PreparedStatement) -> R): R {
|
||||||
|
contract {
|
||||||
|
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
|
||||||
|
}
|
||||||
|
this.prepareStatement(sql).use { ps -> return action(ps) }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal inline fun <R, C : MutableCollection<R>> ResultSet.mapTo(to: C, transform: (rs: ResultSet) -> R): C {
|
||||||
|
while (this.next()) {
|
||||||
|
to.add(transform(this))
|
||||||
|
}
|
||||||
|
return to
|
||||||
|
}
|
||||||
|
|
||||||
|
internal inline fun <R> ResultSet.map(transform: (rs: ResultSet) -> R): List<R> =
|
||||||
|
this.mapTo(ArrayList(), transform)
|
||||||
|
|
||||||
|
internal inline fun <R, C : MutableCollection<R>> ResultSet.mapToThenClose(to: C, transform: (rs: ResultSet) -> R): C =
|
||||||
|
this.use { rs -> rs.mapTo(to, transform) }
|
||||||
|
|
||||||
|
internal inline fun <R> ResultSet.mapThenClose(transform: (rs: ResultSet) -> R): List<R> =
|
||||||
|
this.use { rs -> rs.map(transform) }
|
||||||
|
|
||||||
|
internal inline fun <R> PreparedStatement.executeQuery(action: (ResultSet) -> R): R {
|
||||||
|
contract {
|
||||||
|
callsInPlace(action, kotlin.contracts.InvocationKind.EXACTLY_ONCE)
|
||||||
|
}
|
||||||
|
return this.executeQuery().use(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal fun PreparedStatement.setUByte(parameterIndex: Int, value: UByte): Unit =
|
||||||
|
this.setByte(parameterIndex, value.toByte())
|
||||||
|
|
||||||
|
internal fun PreparedStatement.setUShort(parameterIndex: Int, value: UShort): Unit =
|
||||||
|
this.setShort(parameterIndex, value.toShort())
|
||||||
|
|
||||||
|
internal fun PreparedStatement.setUInt(parameterIndex: Int, value: UInt): Unit =
|
||||||
|
this.setInt(parameterIndex, value.toInt())
|
||||||
|
|
||||||
|
internal fun PreparedStatement.setULong(parameterIndex: Int, value: ULong): Unit =
|
||||||
|
this.setLong(parameterIndex, value.toLong())
|
||||||
|
|
||||||
|
internal fun ResultSet.getUByte(columnIndex: Int): UByte =
|
||||||
|
this.getByte(columnIndex).toUByte()
|
||||||
|
|
||||||
|
internal fun ResultSet.getUShort(columnIndex: Int): UShort =
|
||||||
|
this.getShort(columnIndex).toUShort()
|
||||||
|
|
||||||
|
internal fun ResultSet.getUInt(columnIndex: Int): UInt =
|
||||||
|
this.getInt(columnIndex).toUInt()
|
||||||
|
|
||||||
|
internal fun ResultSet.getULong(columnIndex: Int): ULong =
|
||||||
|
this.getLong(columnIndex).toULong()
|
32
telegram-api-impl/build.gradle.kts
Normal file
32
telegram-api-impl/build.gradle.kts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven("https://maven.landgrafhomyak.ru/")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath("ru.landgrafhomyak.kotlin:kotlin-mpp-gradle-build-helper:v0.2k2.0.20")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("multiplatform") version "2.0.20"
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(21)
|
||||||
|
jvm {}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
jvmMain {
|
||||||
|
dependencies {
|
||||||
|
compileOnly(project(":telegram-api"))
|
||||||
|
api("org.json:json:20250107")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.telegram.impl
|
||||||
|
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.telegram.api.CallbackQuery
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.telegram.api.Message
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.telegram.api.User
|
||||||
|
|
||||||
|
class CallbackQueryStruct(
|
||||||
|
override val data: String,
|
||||||
|
override val user: User,
|
||||||
|
override val message: Message,
|
||||||
|
override val queryId: String
|
||||||
|
) : CallbackQuery
|
@ -0,0 +1,8 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.telegram.impl
|
||||||
|
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.telegram.api.Message
|
||||||
|
|
||||||
|
sealed class IncomingUpdate {
|
||||||
|
class TextMessage(val msg: Message) : IncomingUpdate()
|
||||||
|
class CallbackQuery(val query: ru.landgrafhomyak.bgtu.db0.telegram.api.CallbackQuery) : IncomingUpdate()
|
||||||
|
}
|
@ -0,0 +1,177 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.telegram.impl
|
||||||
|
|
||||||
|
import kotlin.math.max
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.telegram.api.CallbackQuery
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.telegram.api.InlineButton
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.telegram.api.Message
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.telegram.api.TelegramApiException
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.telegram.api.TelegramBotApi
|
||||||
|
|
||||||
|
class JavaBlockingHttpClientTelegramApiImpl(
|
||||||
|
private val baseUrl: String,
|
||||||
|
private val httpClient: JavaBlockingJson2JsonHttpClient
|
||||||
|
) : TelegramBotApi {
|
||||||
|
constructor(
|
||||||
|
token: String,
|
||||||
|
baseUrlFactory: BaseUrlFactory = BaseUrlFactory.OfficialServer,
|
||||||
|
httpClient: JavaBlockingJson2JsonHttpClient
|
||||||
|
) : this(baseUrlFactory.bindWithBotToken(token), httpClient)
|
||||||
|
|
||||||
|
|
||||||
|
fun interface BaseUrlFactory {
|
||||||
|
fun bindWithBotToken(token: String): String
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
val tokenPattern = Regex("""\d+:[a-zA-Z0-9\-_]+""")
|
||||||
|
}
|
||||||
|
|
||||||
|
object OfficialServer : BaseUrlFactory {
|
||||||
|
override fun bindWithBotToken(token: String): String {
|
||||||
|
if (null == BaseUrlFactory.tokenPattern.matchEntire(token))
|
||||||
|
throw IllegalArgumentException("Bad bot token")
|
||||||
|
return "https://api.telegram.org/bot${token}/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
private inline fun <reified O> _doRequest(method: String, payload: JSONObject): O {
|
||||||
|
val response = this.httpClient.request(this.baseUrl + method, payload)
|
||||||
|
if (!response.getBoolean("ok")) {
|
||||||
|
throw TelegramApiException(response.getString("description"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("LiftReturnOrAssignment")
|
||||||
|
when (O::class) {
|
||||||
|
Unit::class -> return Unit as O
|
||||||
|
JSONObject::class -> return response.getJSONObject("result") as O
|
||||||
|
JSONArray::class -> return response.getJSONArray("result") as O
|
||||||
|
Boolean::class -> return response.getBoolean("result") as O
|
||||||
|
String::class -> return response.getString("result") as O
|
||||||
|
else -> throw IllegalArgumentException("Unsupported return type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun isChatOwner(chat: Long, user: Long): Boolean = this
|
||||||
|
._doRequest<JSONArray>("getChatAdministrators", JSONObject(mapOf("chat_id" to chat)))
|
||||||
|
.let { o -> o as Iterable<JSONObject> }
|
||||||
|
.filter { jo -> jo.getString("status") == "creator" }
|
||||||
|
.any { jo -> jo.getJSONObject("user").getLong("id") == user }
|
||||||
|
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
private fun _parseUser(raw: JSONObject) = UserStruct(
|
||||||
|
id = raw.getLong("id"),
|
||||||
|
displayName = "${raw.optString("first_name")} ${raw.optString("last_name")}".trim()
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
private fun _parseMessage(raw: JSONObject): MessageStruct = MessageStruct(
|
||||||
|
fromUser = this._parseUser(raw.getJSONObject("from")),
|
||||||
|
chatId = raw.getJSONObject("chat").getLong("id"),
|
||||||
|
messageId = raw.getLong("message_id"),
|
||||||
|
_rawText = if (!raw.has("text")) null else raw.getString("text"),
|
||||||
|
replyToMessage = if (!raw.has("reply_to_message")) null else this._parseMessage(raw.getJSONObject("reply_to_message")),
|
||||||
|
inlineKeyboard = if (!raw.has("reply_markup")) null else raw.getJSONObject("reply_markup").let { kbd ->
|
||||||
|
if (!kbd.has("inline_keyboard"))
|
||||||
|
return@let null
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return@let kbd.getJSONArray("inline_keyboard")
|
||||||
|
.let { rows -> rows as Iterable<Iterable<JSONObject>> }
|
||||||
|
.map row@{ row ->
|
||||||
|
return@row row.map btn@{ btn ->
|
||||||
|
if (!btn.has("callback_data"))
|
||||||
|
return@let null
|
||||||
|
return@btn InlineButton(btn.getString("text"), btn.getString("callback_data"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
private fun _formatInlineKeyboard(api: List<List<InlineButton>>): JSONObject = api
|
||||||
|
.map { row -> row.map { btn -> JSONObject(mapOf("text" to btn.displayText, "callback_data" to btn.data)) }.let(::JSONArray) }
|
||||||
|
.let { rr -> JSONObject(mapOf("inline_keyboard" to rr)) }
|
||||||
|
|
||||||
|
override fun sendMessage(replyTo: Message, htmlText: String, buttons: List<List<InlineButton>>?): Message =
|
||||||
|
this._parseMessage(
|
||||||
|
this._doRequest<JSONObject>(
|
||||||
|
"sendMessage", JSONObject(
|
||||||
|
mapOf(
|
||||||
|
"chat_id" to replyTo.chatId,
|
||||||
|
"text" to htmlText,
|
||||||
|
"parse_mode" to "html",
|
||||||
|
"reply_markup" to buttons?.let(this::_formatInlineKeyboard)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun editMessageText(message: Message, htmlText: String, keepButtons: Boolean): Message =
|
||||||
|
this._parseMessage(
|
||||||
|
this._doRequest<JSONObject>(
|
||||||
|
"editMessageText", JSONObject(
|
||||||
|
mapOf(
|
||||||
|
"chat_id" to message.chatId,
|
||||||
|
"message_id" to message.messageId,
|
||||||
|
"text" to htmlText,
|
||||||
|
"parse_mode" to "html",
|
||||||
|
"reply_markup" to (
|
||||||
|
if (!keepButtons) null
|
||||||
|
else message.inlineKeyboard?.let(this::_formatInlineKeyboard)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun answerCallbackQuery(query: CallbackQuery, text: String) =
|
||||||
|
this._doRequest<Unit>(
|
||||||
|
"answerCallbackQuery", JSONObject(
|
||||||
|
mapOf(
|
||||||
|
"callback_query_id" to query.queryId,
|
||||||
|
"text" to text
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
private fun _parseCallbackQuery(raw: JSONObject): CallbackQuery =
|
||||||
|
CallbackQueryStruct(
|
||||||
|
queryId = raw.getString("id"),
|
||||||
|
user = this._parseUser(raw.getJSONObject("from")),
|
||||||
|
message = this._parseMessage(raw.getJSONObject("message")),
|
||||||
|
data = raw.getString("data")
|
||||||
|
)
|
||||||
|
|
||||||
|
fun interface UpdatesConsumer {
|
||||||
|
fun processUpdate(u: IncomingUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun receiveUpdatesUntilError(consumer: UpdatesConsumer) {
|
||||||
|
var nextId = 0L
|
||||||
|
while (true) {
|
||||||
|
this
|
||||||
|
._doRequest<JSONArray>(
|
||||||
|
"getUpdates",
|
||||||
|
JSONObject(mapOf("offset" to nextId, "timeout" to 1, "allowed_updates" to JSONArray(listOf("message", "callback_query"))))
|
||||||
|
)
|
||||||
|
.let { a -> a as Iterable<JSONObject> }
|
||||||
|
.forEach { u ->
|
||||||
|
nextId = max(nextId, u.getLong("update_id") + 1)
|
||||||
|
if (u.has("message"))
|
||||||
|
consumer.processUpdate(IncomingUpdate.TextMessage(this._parseMessage(u.getJSONObject("message"))))
|
||||||
|
else if (u.has("callback_query"))
|
||||||
|
consumer.processUpdate(IncomingUpdate.CallbackQuery(this._parseCallbackQuery(u.getJSONObject("callback_query"))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.telegram.impl
|
||||||
|
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.http.HttpClient
|
||||||
|
import java.net.http.HttpRequest
|
||||||
|
import java.net.http.HttpRequest.BodyPublishers
|
||||||
|
import java.net.http.HttpResponse.BodyHandlers
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class JavaBlockingJson2JsonHttpClient(
|
||||||
|
private val jClient: HttpClient
|
||||||
|
) {
|
||||||
|
fun request(url: String, payload: JSONObject): JSONObject =
|
||||||
|
this.jClient.send(
|
||||||
|
HttpRequest.newBuilder(URI(url))
|
||||||
|
.POST(BodyPublishers.ofString(payload.toString(0), Charsets.UTF_8))
|
||||||
|
.setHeader("Content-Type", "application/json; charset=utf-8")
|
||||||
|
.build(),
|
||||||
|
BodyHandlers.ofString()
|
||||||
|
).body().let { s -> JSONObject(s) }
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.telegram.impl
|
||||||
|
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.telegram.api.InlineButton
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.telegram.api.Message
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.telegram.api.User
|
||||||
|
|
||||||
|
internal class MessageStruct(
|
||||||
|
override val fromUser: User,
|
||||||
|
override val chatId: Long,
|
||||||
|
override val messageId: Long,
|
||||||
|
private val _rawText: String?,
|
||||||
|
override val replyToMessage: Message?,
|
||||||
|
override val inlineKeyboard: List<List<InlineButton>>?
|
||||||
|
) : Message {
|
||||||
|
override val rawText: String
|
||||||
|
get() = this._rawText ?: throw NotTextMessageException()
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.telegram.impl
|
||||||
|
|
||||||
|
class NotTextMessageException : Exception()
|
@ -0,0 +1,8 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.telegram.impl
|
||||||
|
|
||||||
|
import ru.landgrafhomyak.bgtu.db0.telegram.api.User
|
||||||
|
|
||||||
|
internal class UserStruct(
|
||||||
|
override val id: Long,
|
||||||
|
override val displayName: String
|
||||||
|
) : User
|
25
telegram-api/build.gradle.kts
Normal file
25
telegram-api/build.gradle.kts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import ru.landgrafhomyak.kotlin.kmp_gradle_build_helper.defineAllMultiplatformTargets
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven("https://maven.landgrafhomyak.ru/")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath("ru.landgrafhomyak.kotlin:kotlin-mpp-gradle-build-helper:v0.2k2.0.20")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("multiplatform") version "2.0.20"
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(21)
|
||||||
|
defineAllMultiplatformTargets()
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.telegram.api
|
||||||
|
|
||||||
|
interface CallbackQuery {
|
||||||
|
val data: String
|
||||||
|
val user: User
|
||||||
|
val message: Message
|
||||||
|
val queryId: String
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.telegram.api
|
||||||
|
|
||||||
|
class InlineButton(val displayText: String, val data: String)
|
@ -0,0 +1,10 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.telegram.api
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
val fromUser: User
|
||||||
|
val chatId: Long
|
||||||
|
val messageId: Long
|
||||||
|
val rawText: String
|
||||||
|
val replyToMessage: Message?
|
||||||
|
val inlineKeyboard: List<List<InlineButton>>?
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.telegram.api
|
||||||
|
|
||||||
|
class TelegramApiException(val description: String) : Exception(description)
|
@ -0,0 +1,10 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.telegram.api
|
||||||
|
|
||||||
|
interface TelegramBotApi {
|
||||||
|
fun isChatOwner(chat: Long, user: Long): Boolean
|
||||||
|
|
||||||
|
fun sendMessage(replyTo: Message, htmlText: String, buttons: List<List<InlineButton>>? = null): Message
|
||||||
|
fun editMessageText(message: Message, htmlText: String, keepButtons: Boolean = true): Message
|
||||||
|
|
||||||
|
fun answerCallbackQuery(query: CallbackQuery, text: String)
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package ru.landgrafhomyak.bgtu.db0.telegram.api
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
val id: Long
|
||||||
|
val displayName: String
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user