Ragequit.

This commit is contained in:
2026-05-13 23:51:00 +03:00
parent efa3cb63b9
commit 547a77d52a
25 changed files with 425 additions and 156 deletions
+7
View File
@@ -20,6 +20,11 @@ shadowJar {
}
}
task DMkt(type: JavaExec) {
classpath = sourceSets.main.runtimeClasspath
mainClass = 'database.DatabaseManagerKt'
}
dependencies {
testImplementation 'org.jetbrains.kotlin:kotlin-test'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0'
@@ -28,6 +33,7 @@ dependencies {
implementation "io.github.oshai:kotlin-logging-jvm:7.0.3"
implementation "org.slf4j:slf4j-api:2.0.13"
implementation "ch.qos.logback:logback-classic:1.5.20"
implementation "io.fusionauth:fusionauth-jwt:6.0.0"
// oh my gawd
implementation "org.jetbrains.exposed:exposed-core:1.2.0"
@@ -38,6 +44,7 @@ dependencies {
implementation "com.zaxxer:HikariCP:4.0.3"
implementation project(':common')
implementation "org.jetbrains.kotlin:kotlin-test:2.2.0"
}
kotlin {
+17
View File
@@ -0,0 +1,17 @@
2026-05-13 20:52:27.910 [main] DEBUG Exposed -
DO $$ BEGIN
CREATE TYPE weapontype AS ENUM ('hammer', 'shotgun', 'bat');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
2026-05-13 20:52:29.769 [main] DEBUG Exposed - CREATE TABLE IF NOT EXISTS "User" (id uuid PRIMARY KEY, username TEXT NOT NULL, "password" TEXT NOT NULL, email TEXT NOT NULL)
2026-05-13 20:52:30.071 [main] DEBUG Exposed - SELECT humanbeing.id, humanbeing."name", humanbeing.coordinates, humanbeing.creation_date, humanbeing.real_hero, humanbeing.has_toothpick, humanbeing.impact_speed, humanbeing.soundtrack_name, humanbeing.minutes_of_waiting, humanbeing.car, humanbeing.weapon_type FROM humanbeing
2026-05-13 20:53:16.919 [main] DEBUG Exposed -
DO $$ BEGIN
CREATE TYPE weapontype AS ENUM ('hammer', 'shotgun', 'bat');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
2026-05-13 20:53:18.805 [main] DEBUG Exposed - SELECT humanbeing.id, humanbeing."name", humanbeing.coordinates, humanbeing.creation_date, humanbeing.real_hero, humanbeing.has_toothpick, humanbeing.impact_speed, humanbeing.soundtrack_name, humanbeing.minutes_of_waiting, humanbeing.car, humanbeing.weapon_type FROM humanbeing
+4 -9
View File
@@ -1,4 +1,4 @@
import database.Repository
import collection.CollectionManager
import commands.*
import file.FileManager
import mu.KotlinLogging
@@ -17,7 +17,7 @@ fun main(args: Array<String>) {
logger.info{ "[startup] Starting server on port $port with $args args" }
val fileManager = FileManager(filePath)
val manager = Repository()
val manager = CollectionManager()
val invoker = CommandInvoker()
// Загружаем коллекцию из файла
@@ -29,8 +29,7 @@ fun main(args: Array<String>) {
// Сохранение при завершении (Ctrl+C или kill)
val server = NetworkManager(port = port, invoker = invoker)
Runtime.getRuntime().addShutdownHook(Thread {
logger.info{ "Saving collection before exit..." }
fileManager.write(manager.getAll())
logger.info{ "Exit..." }
server.stop()
})
@@ -40,10 +39,6 @@ fun main(args: Array<String>) {
logger.info{ "[Server-console] Server-side commands: save, exit" }
while (scanner.hasNextLine()) {
when (scanner.nextLine().trim().lowercase()) {
"save" -> {
fileManager.write(manager.getAll())
logger.info{ "[Server-console] Collection is saved" }
}
"exit" -> {
logger.info{ "[Server-console] Shutting down..." }
exitProcess(0)
@@ -58,7 +53,7 @@ fun main(args: Array<String>) {
fun registerServerCommands(
invoker: CommandInvoker,
manager: Repository,
manager: CollectionManager,
fileManager: FileManager
) {
listOf(
+34
View File
@@ -0,0 +1,34 @@
package auth
import io.fusionauth.jwt.Signer
import io.fusionauth.jwt.Verifier
import io.fusionauth.jwt.domain.JWT
import io.fusionauth.jwt.hmac.HMACSigner
import io.fusionauth.jwt.hmac.HMACVerifier
import java.time.ZoneOffset
import java.time.ZonedDateTime
class JWTManager (
private val jwtSecret: String = "random",
private val issuer: String = "aklivtsov.tech",
private val subject: String = "f1e33ab3-027f-47c5-bb07-8dd8ab37a2d3"
) {
private val signer: Signer = HMACSigner.newSHA256Signer(jwtSecret)
private val verifier: Verifier = HMACVerifier.newVerifier(jwtSecret)
fun createJWT(expiredAfterMinutes: Long): String {
val jwt: JWT = JWT()
.setIssuer(issuer)
.setIssuedAt(ZonedDateTime.now(ZoneOffset.UTC))
.setSubject(subject)
.setExpiration(ZonedDateTime.now(ZoneOffset.UTC).plusMinutes(expiredAfterMinutes))
return JWT.getEncoder().encode(jwt, signer)
}
fun validateJWT(jwt: String): Boolean {
// check if it validates expired tokens
val jwt = JWT.getDecoder().decode(jwt, verifier)
return jwt.subject == subject
}
}
@@ -0,0 +1,92 @@
package collection
import models.HumanBeing
import mu.KotlinLogging
import java.time.LocalDate
import java.util.TreeSet
import java.util.UUID
private val logger = KotlinLogging.logger {}
class CollectionManager {
private val collection: TreeSet<HumanBeing> = TreeSet()
val initDate: LocalDate = LocalDate.now()
fun add(humanBeing: HumanBeing): Boolean {
logger.info { "[ADD] Element created ${humanBeing.id}" }
return collection.add(humanBeing)
}
fun update(id: UUID, updated: HumanBeing): Boolean {
val old = getById(id) ?: return false
collection.remove(old)
val replaced = updated.copy(id = old.id, creationDate = old.creationDate)
logger.info { "[UPDATE] Element updated ${old.id}" }
return collection.add(replaced)
}
fun removeById(id: UUID): Boolean {
val target = getById(id) ?: return false
logger.info { "[REMOVE] Element deleted ${target.id}" }
return collection.remove(target)
}
fun clear() {
collection.clear()
logger.info { "[CLEAR] Collection cleared" }
}
fun getAll(): TreeSet<HumanBeing> = TreeSet(collection)
fun getById(id: UUID): HumanBeing? =
collection.stream()
.filter { it.id == id }
.findFirst()
.orElse(null)
fun size(): Int = collection.size
fun isEmpty(): Boolean = collection.isEmpty()
fun getMax(): HumanBeing? =
collection.stream()
.max(Comparator.naturalOrder())
.orElse(null)
fun getMin(): HumanBeing? =
collection.stream()
.min(Comparator.naturalOrder())
.orElse(null)
fun sumOfMinutesOfWaiting(): Double =
collection.stream()
.mapToDouble { it.minutesOfWaiting.toDouble() }
.sum()
fun minByName(): HumanBeing? =
collection.stream()
.min(Comparator.comparing { it.name })
.orElse(null)
fun getMinutesOfWaitingDescending(): List<Float> =
collection.stream()
.map { it.minutesOfWaiting }
.sorted(Comparator.reverseOrder())
.toList()
fun loadFromFile(items: List<HumanBeing>) {
collection.clear()
items.stream().forEach { collection.add(it) }
logger.info { "[LOAD] loaded ${items.size} elements" }
}
fun getInfo(): String =
"""
|Тип коллекции : ${collection::class.simpleName}
|Тип элементов : ${HumanBeing::class.simpleName}
|Дата инициал. : $initDate
|Кол-во элемен.: ${collection.size}
""".trimMargin()
}
@@ -1,13 +1,13 @@
package commands
import database.Repository
import collection.CollectionManager
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import models.HumanBeing
import network.Response
@Serializable
class AddCommand(@Contextual private val manager: Repository) : Command {
class AddCommand(@Contextual private val manager: CollectionManager) : Command {
override val name = "add"
override val description = "добавить новый элемент в коллекцию"
override val args = listOf(CommandArgType.NONE)
@@ -1,10 +1,10 @@
package commands
import database.Repository
import collection.CollectionManager
import models.HumanBeing
import network.Response
class AddIfMaxCommand(private val manager: Repository) : Command {
class AddIfMaxCommand(private val manager: CollectionManager) : Command {
override val name = "add_if_max"
override val description = "добавить элемент, если он больше максимального"
override val type = CommandType.MODEL
+2 -2
View File
@@ -1,13 +1,13 @@
package commands
import database.Repository
import collection.CollectionManager
import models.HumanBeing
import kotlinx.serialization.Serializable
import kotlinx.serialization.Contextual
import network.Response
@Serializable
class AddIfMinCommand(@Contextual private val manager: Repository) : Command {
class AddIfMinCommand(@Contextual private val manager: CollectionManager) : Command {
override val name = "add_if_min"
override val description = "добавить элемент, если он меньше минимального"
override val type = CommandType.MODEL
@@ -1,13 +1,13 @@
package commands
import database.Repository
import collection.CollectionManager
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import models.HumanBeing
import network.Response
@Serializable
class ClearCommand(@Contextual private val manager: Repository) : Command {
class ClearCommand(@Contextual private val manager: CollectionManager) : Command {
override val name = "clear"
override val description = "очистить коллекцию"
override val args = listOf(CommandArgType.NONE)
@@ -1,13 +1,13 @@
package commands
import database.Repository
import collection.CollectionManager
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import models.HumanBeing
import network.Response
@Serializable
class InfoCommand(@Contextual private val manager: Repository) : Command {
class InfoCommand(@Contextual private val manager: CollectionManager) : Command {
override val name = "info"
override val description = "вывести информацию о коллекции"
override val type = CommandType.SIMPLE
@@ -1,6 +1,6 @@
package commands
import database.Repository
import collection.CollectionManager
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import models.HumanBeing
@@ -10,7 +10,7 @@ import network.Response
* Выводит любой элемент с минимальным значением поля [HumanBeing.name].
*/
@Serializable
class MinByNameCommand(@Contextual private val manager: Repository) : Command {
class MinByNameCommand(@Contextual private val manager: CollectionManager) : Command {
override val name = "min_by_name"
override val description = "вывести элемент с минимальным именем"
override val type = CommandType.SIMPLE
@@ -1,6 +1,6 @@
package commands
import database.Repository
import collection.CollectionManager
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import models.HumanBeing
@@ -10,7 +10,7 @@ import network.Response
* Выводит значения поля [models.HumanBeing.minutesOfWaiting] в порядке убывания.
*/
@Serializable
class PrintDescendingMinutesCommand(@Contextual private val manager: Repository) : Command {
class PrintDescendingMinutesCommand(@Contextual private val manager: CollectionManager) : Command {
override val name = "print_field_descending_minutes_of_waiting"
override val description = "вывести minutesOfWaiting в порядке убывания"
override val type = CommandType.SIMPLE
@@ -1,6 +1,6 @@
package commands
import database.Repository
import collection.CollectionManager
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import java.util.UUID
@@ -8,7 +8,7 @@ import models.HumanBeing
import network.Response
@Serializable
class RemoveByIdCommand(@Contextual private val manager: Repository) : Command {
class RemoveByIdCommand(@Contextual private val manager: CollectionManager) : Command {
override val name = "remove_by_id"
override val description = "удалить элемент по id: remove_by_id <uuid>"
override val type = CommandType.MODEL
@@ -1,15 +1,16 @@
package commands
import database.Repository
import collection.CollectionManager
import file.FileManager
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import models.HumanBeing
import network.Response
import java.util.UUID
@Serializable
class SaveCommand(
@Contextual private val manager: Repository,
@Contextual private val manager: CollectionManager,
@Contextual private val fileManager: FileManager
) : Command {
override val name = "save"
@@ -18,7 +19,8 @@ class SaveCommand(
override val args = listOf(CommandArgType.NONE)
override fun execute(args: List<String>, humanBeing: HumanBeing?): Response {
return if (fileManager.write(manager.getAll())) Response(true, "Коллекция сохранена.")
val dummyUser: UUID = UUID.fromString("68db6bf5-65ce-46c7-881d-c85b23254f92")
return if (fileManager.write(manager.getAll(), dummyUser)) Response(true, "Коллекция сохранена.")
else Response(false, "[Ошибка] Не удалось сохранить коллекцию.")
}
}
@@ -1,13 +1,13 @@
package commands
import database.Repository
import collection.CollectionManager
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import models.HumanBeing
import network.Response
@Serializable
class ShowCommand(@Contextual private val manager: Repository) : Command {
class ShowCommand(@Contextual private val manager: CollectionManager) : Command {
override val name = "show"
override val description = "вывести все элементы коллекции"
override val type = CommandType.SIMPLE
@@ -1,6 +1,6 @@
package commands
import database.Repository
import collection.CollectionManager
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import models.HumanBeing
@@ -10,7 +10,7 @@ import network.Response
* Выводит сумму значений поля [models.HumanBeing.minutesOfWaiting].
*/
@Serializable
class SumOfMinutesCommand(@Contextual private val manager: Repository) : Command {
class SumOfMinutesCommand(@Contextual private val manager: CollectionManager) : Command {
override val name = "sum_of_minutes_of_waiting"
override val description = "вывести сумму minutesOfWaiting всех элементов"
override val type = CommandType.SIMPLE
@@ -1,6 +1,6 @@
package commands
import database.Repository
import collection.CollectionManager
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import models.HumanBeing
@@ -8,7 +8,7 @@ import network.Response
import java.util.UUID
@Serializable
class UpdateCommand(@Contextual private val manager: Repository) : Command {
class UpdateCommand(@Contextual private val manager: CollectionManager) : Command {
override val name = "update"
override val description = "обновить элемент по id: update <uuid>"
override val type = CommandType.MODEL
@@ -1,9 +1,10 @@
package database
import com.zaxxer.hikari.*
import database.tables.CarTable
import database.tables.CoordinatesTable
import database.tables.HumanBeingTable
import models.Coordinates
import database.tables.UserTable
import org.jetbrains.exposed.v1.jdbc.*
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
@@ -24,16 +25,19 @@ class DatabaseManager {
Database.connect(dataSource)
transaction {
exec("""
exec(
"""
DO ${'$'}${'$'} BEGIN
CREATE TYPE weapontype AS ENUM ('hammer', 'shotgun', 'bat');
EXCEPTION
WHEN duplicate_object THEN null;
END ${'$'}${'$'};
""")
"""
)
SchemaUtils.create(CoordinatesTable)
SchemaUtils.create(CarTable)
SchemaUtils.create(HumanBeingTable)
SchemaUtils.create(UserTable)
}
}
@@ -41,20 +45,19 @@ class DatabaseManager {
val smth = mutableListOf<String>()
transaction {
val rows = HumanBeingTable.selectAll().toList()
val rows = HumanBeingTable.selectAll()
rows.forEach { row ->
smth.add(row[HumanBeingTable.name])
}
}
return smth
}
}
fun main(args: Array<String>) {
val manager = DatabaseManager()
print("here we go!")
println("here we go!")
manager.start()
print(manager.test())
print("here we don't go!")
}
println(manager.test())
println("here we don't go!")
}
+39 -109
View File
@@ -2,128 +2,58 @@ package database
import database.tables.CarTable
import database.tables.CoordinatesTable
import models.HumanBeing
import models.Car
import database.tables.HumanBeingTable
import mu.KotlinLogging
import database.tables.UserTable
import io.github.oshai.kotlinlogging.KotlinLogging
import models.HumanBeing
import org.jetbrains.exposed.v1.core.*
import org.jetbrains.exposed.v1.jdbc.*
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.jetbrains.exposed.v1.jdbc.insert
import java.time.LocalDate
import java.util.TreeSet
import java.util.UUID
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
private val logger = KotlinLogging.logger {}
class Repository(private val filePath: String) {
class Repository {
//fun read(): List<HumanBeing> {}
private val collection: TreeSet<HumanBeing> = TreeSet()
val initDate: LocalDate = LocalDate.now()
fun add(humanBeing: HumanBeing): Boolean {
var allGood = true
@OptIn(ExperimentalUuidApi::class)
fun write(items: Collection<HumanBeing>, userId: UUID): Boolean {
transaction {
try {
val cords = CoordinatesTable.insert {
it[x] = humanBeing.coordinates.x
it[y] = humanBeing.coordinates.y
} get CoordinatesTable.id
val theCar = CarTable.insert {
it[cool] = humanBeing.car.cool
} get CarTable.id
HumanBeingTable.insert {
it[name] = humanBeing.name
it[coordinates] = cords
it[creationDate] = humanBeing.creationDate
it[realHero] = humanBeing.realHero
it[hasToothpick] = humanBeing.hasToothpick
it[impactSpeed] = humanBeing.impactSpeed
it[soundtrackName] = humanBeing.soundtrackName
it[minutesOfWaiting] = humanBeing.minutesOfWaiting
it[car] = theCar
it[weaponType] = humanBeing.weaponType
for (item in items) {
val cords = CoordinatesTable.insertAndGetId {
it[x] = item.coordinates.x
it[y] = item.coordinates.y
}
val cars = CarTable.insertAndGetId {
it[cool] = item.car.cool
}
val user = UserTable
.select(UserTable.id)
.where { UserTable.id eq userId }
.first()
HumanBeingTable.insert {
it[owner_id] = userId
it[name] = item.name
it[id] = user[UserTable.id] // it fucking autoincrements (check perplexity)
it[coordinates] = cords
it[creationDate] = item.creationDate
it[realHero] = item.realHero
it[hasToothpick] = item.hasToothpick
it[impactSpeed] = item.impactSpeed
it[soundtrackName] = item.soundtrackName
it[minutesOfWaiting] = item.minutesOfWaiting
it[car] = cars
it[weaponType] = item.weaponType
}
logger.info { "[ADD] Element created ${humanBeing.id}" }
allGood = true
} catch (e: Exception) {
logger.error(e) { "[ADD] Error during inserting: $e" }
allGood = false
}
}
return allGood
}
fun update(id: UUID, updated: HumanBeing): Boolean {
val old = getById(id) ?: return false
collection.remove(old)
val replaced = updated.copy(id = old.id, creationDate = old.creationDate)
logger.info { "[UPDATE] Element updated ${old.id}" }
return collection.add(replaced)
}
fun removeById(id: UUID): Boolean {
val target = getById(id) ?: return false
logger.info { "[REMOVE] Element deleted ${target.id}" }
return collection.remove(target)
}
fun clear() {
collection.clear()
logger.info { "[CLEAR] Collection cleared" }
}
fun getAll(): TreeSet<HumanBeing> = TreeSet(collection)
fun getById(id: UUID): HumanBeing? =
collection.stream()
.filter { it.id == id }
.findFirst()
.orElse(null)
fun size(): Int = collection.size
fun isEmpty(): Boolean = collection.isEmpty()
fun getMax(): HumanBeing? =
collection.stream()
.max(Comparator.naturalOrder())
.orElse(null)
fun getMin(): HumanBeing? =
collection.stream()
.min(Comparator.naturalOrder())
.orElse(null)
fun sumOfMinutesOfWaiting(): Double =
collection.stream()
.mapToDouble { it.minutesOfWaiting.toDouble() }
.sum()
fun minByName(): HumanBeing? =
collection.stream()
.min(Comparator.comparing { it.name })
.orElse(null)
fun getMinutesOfWaitingDescending(): List<Float> =
collection.stream()
.map { it.minutesOfWaiting }
.sorted(Comparator.reverseOrder())
.toList()
fun loadFromFile(items: List<HumanBeing>) {
collection.clear()
items.stream().forEach { collection.add(it) }
logger.info { "[LOAD] loaded ${items.size} elements" }
}
fun getInfo(): String =
"""
|Тип коллекции : ${collection::class.simpleName}
|Тип элементов : ${HumanBeing::class.simpleName}
|Дата инициал. : $initDate
|Кол-во элемен.: ${collection.size}
""".trimMargin()
}
@@ -1,12 +1,16 @@
package database.tables
import models.WeaponType
import org.jetbrains.exposed.v1.core.dao.id.java.UUIDTable
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.javatime.date
import org.postgresql.util.PGobject
import kotlin.uuid.ExperimentalUuidApi
object HumanBeingTable : UUIDTable("humanBeing") {
object HumanBeingTable : Table("humanBeing") {
@OptIn(ExperimentalUuidApi::class)
val id = uuid("id")
val owner_id = reference("owner_id", UserTable.id)
val name = text("name")
val coordinates = reference("coordinates", CoordinatesTable.id)
val creationDate = date("creation_date")
@@ -31,4 +35,7 @@ object HumanBeingTable : UUIDTable("humanBeing") {
}
}
).nullable().default(null)
@OptIn(ExperimentalUuidApi::class)
override val primaryKey = PrimaryKey(id)
}
@@ -0,0 +1,9 @@
package database.tables
import org.jetbrains.exposed.v1.core.dao.id.java.UUIDTable
object UserTable: UUIDTable("user") {
val username = text("username")
val password = text("password") // change
val email = text("email") // ask that or username when logging-in
}
+1 -1
View File
@@ -137,7 +137,7 @@ class FileManager(private val filePath: String) {
* @param items коллекция объектов [HumanBeing] для записи
* @return true если запись прошла успешно
*/
fun write(items: Collection<HumanBeing>): Boolean {
fun write(items: Collection<HumanBeing>, user: UUID): Boolean {
val file = File(filePath)
if (file.exists() && !file.canWrite()) {