Initial commit

This commit is contained in:
Andrew Golovashevich 2025-01-20 07:01:29 +03:00
commit dc108dcc55
39 changed files with 1671 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
/.gradle/
build/
/gradlew*
*.sqlite
/.idea/
/gradle/
/.kotlin/

6
build.gradle.kts Normal file
View File

@ -0,0 +1,6 @@
allprojects {
repositories {
mavenCentral()
maven("https://maven.landgrafhomyak.ru/")
}
}

36
exe/build.gradle.kts Normal file
View 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"))
}
}
}
}

View File

@ -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
View File

@ -0,0 +1,2 @@
kotlin.native.ignoreDisabledTargets=true
kotlin.suppressGradlePluginWarnings=IncorrectCompileOnlyDependencyWarning

34
impl/build.gradle.kts Normal file
View 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"))
}
}
}
}

View File

@ -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()
}
}

View File

@ -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>")
}
}
}
}

View File

@ -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")))
}

View File

@ -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
View 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
View 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;

View 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()
}

View File

@ -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
}

View File

@ -0,0 +1,6 @@
package ru.landgrafhomyak.bgtu.db0.storage.api
interface Queue {
val name: String?
val members: List<User?>
}

View File

@ -0,0 +1,7 @@
package ru.landgrafhomyak.bgtu.db0.storage.api
interface QueueSet {
val name: String
val queues: List<Queue>
val isOpen: Boolean
}

View File

@ -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
}

View File

@ -0,0 +1,6 @@
package ru.landgrafhomyak.bgtu.db0.storage.api
interface User {
val telegramId: Long
val displayName: String
}

View 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")
}
}
}
}

View File

@ -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")
}
}

View File

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

View File

@ -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 {
}

View File

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

View File

@ -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()

View 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")
}
}
}
}

View File

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

View File

@ -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()
}

View File

@ -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"))))
}
}
}
}

View File

@ -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) }
}

View File

@ -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()
}

View File

@ -0,0 +1,3 @@
package ru.landgrafhomyak.bgtu.db0.telegram.impl
class NotTextMessageException : Exception()

View File

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

View 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()
}

View File

@ -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
}

View File

@ -0,0 +1,3 @@
package ru.landgrafhomyak.bgtu.db0.telegram.api
class InlineButton(val displayText: String, val data: String)

View File

@ -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>>?
}

View File

@ -0,0 +1,3 @@
package ru.landgrafhomyak.bgtu.db0.telegram.api
class TelegramApiException(val description: String) : Exception(description)

View File

@ -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)
}

View File

@ -0,0 +1,6 @@
package ru.landgrafhomyak.bgtu.db0.telegram.api
interface User {
val id: Long
val displayName: String
}