Initial Commit

This commit is contained in:
2025-10-25 12:50:47 +02:00
commit 28f6921788
27 changed files with 853 additions and 0 deletions

17
src/main/kotlin/Main.kt Normal file
View File

@ -0,0 +1,17 @@
package de.steamwar
import com.github.ajalt.clikt.core.main
import com.github.ajalt.clikt.core.subcommands
import de.steamwar.commands.SteamWar
import de.steamwar.commands.database.DatabaseCommand
import de.steamwar.commands.database.InfoCommand
import de.steamwar.commands.database.ResetCommand
import de.steamwar.commands.user.UserCommand
import de.steamwar.commands.user.UsesrInfoCommand
fun main(args: Array<String>) = SteamWar()
.subcommands(
DatabaseCommand().subcommands(InfoCommand(), ResetCommand()),
UserCommand().subcommands(UsesrInfoCommand())
)
.main(args)

View File

@ -0,0 +1,14 @@
package de.steamwar.commands
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.findOrSetObject
import com.github.ajalt.mordant.rendering.TextStyles
import de.steamwar.db.Database
class SteamWar: CliktCommand(name = "sw") {
val db by findOrSetObject { Database() }
override fun run() {
echo("${TextStyles.bold("SteamWar-CLI")} (${db.database})")
}
}

View File

@ -0,0 +1,22 @@
package de.steamwar.commands.database
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.CliktError
import com.github.ajalt.clikt.core.Context
import com.github.ajalt.clikt.core.requireObject
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import de.steamwar.db.Database
class DatabaseCommand: CliktCommand(name = "db") {
val useProduction by option().flag()
val db by requireObject<Database>()
override fun help(context: Context): String = "Run database commands"
override fun run() {
if (!useProduction && db.database == "production") {
throw CliktError("You should not use the production database!")
}
}
}

View File

@ -0,0 +1,23 @@
package de.steamwar.commands.database
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.requireObject
import com.github.ajalt.mordant.table.table
import de.steamwar.db.Database
class InfoCommand: CliktCommand() {
val db by requireObject<Database>()
override fun run() {
val tables = db.execute("SHOW TABLES") { it.getString(1) }
echo(
table {
header { row("Name") }
body {
tables.map { row(it) }
}
}
)
}
}

View File

@ -0,0 +1,31 @@
package de.steamwar.commands.database
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.CliktError
import com.github.ajalt.clikt.core.requireObject
import com.github.ajalt.mordant.rendering.TextColors
import com.github.ajalt.mordant.rendering.TextStyles
import de.steamwar.db.Database
import java.io.File
class ResetCommand: CliktCommand() {
val db by requireObject<Database>()
override fun run() {
val schemaFile = File("/var/Schema.sql")
if (!schemaFile.exists()) {
throw CliktError("Schema file not found!")
}
val schema = schemaFile.readText()
val tables = db.execute("SHOW TABLES;") { it.getString(1) }
for (table in tables) {
db.execute("DROP TABLE IF EXISTS $table;") { }
}
db.execute(schema) { }
echo(TextColors.brightGreen(TextStyles.bold("Database reset!")))
}
}

View File

@ -0,0 +1,26 @@
package de.steamwar.commands.user
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.CliktError
import com.github.ajalt.clikt.core.findOrSetObject
import com.github.ajalt.clikt.parameters.arguments.argument
import de.steamwar.db.schema.SteamwarUser
import de.steamwar.db.schema.SteamwarUserTable
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.or
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
class UserCommand: CliktCommand("user") {
val userId by argument()
val user by findOrSetObject("user") {
transaction {
SteamwarUser.find { (SteamwarUserTable.id eq userId.toIntOrNull()) or (SteamwarUserTable.username eq userId) }
.firstOrNull()
?.let { return@transaction it } ?: throw CliktError("User not found!")
}
}
override fun run() {
user.id
}
}

View File

@ -0,0 +1,50 @@
package de.steamwar.commands.user
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.requireObject
import com.github.ajalt.mordant.table.table
import de.steamwar.db.schema.Session
import de.steamwar.db.schema.SteamwarUser
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import kotlin.time.DurationUnit
import kotlin.time.ExperimentalTime
class UsesrInfoCommand: CliktCommand("info") {
val user by requireObject<SteamwarUser>("user")
@OptIn(ExperimentalTime::class)
override fun run() {
transaction {
val sessions = Session.selectAll().where { Session.user eq user.id.value }.map { it[Session.start] to it[Session.end] }
val totalPlayed = sessions.map { it.second - it.first }.sumOf { it.toDouble(DurationUnit.HOURS) }
val firstJoin = sessions.minByOrNull { it.first }?.first
val lastJoin = sessions.maxByOrNull { it.second }?.second
echo(
table {
body {
row("Name", user.username)
row("UUID", user.uuid)
row("Team", user.team.name)
row("Leader", user.leader)
row("Locale", user.locale)
row("Beigetreten am", firstJoin)
row("Zuletzt gesehen am", lastJoin)
row("Spielzeit", totalPlayed.toString() + "h")
row("Punishments", table {
header { row("Typ", "Ersteller", "Von", "Bis", "Grund") }
body {
user.punishments.map {
row(it.type, it.punisher.username, it.starttime.toString(), if (it.perma) "Perma" else it.endtime.toString(), it.reason)
}
}
})
}
}
)
}
}
}

View File

@ -0,0 +1,83 @@
package de.steamwar.db
import com.github.ajalt.clikt.core.CliktError
import org.jetbrains.exposed.v1.core.Expression
import org.jetbrains.exposed.v1.core.ExpressionWithColumnType
import org.jetbrains.exposed.v1.core.Function
import org.jetbrains.exposed.v1.core.IColumnType
import org.jetbrains.exposed.v1.core.LongColumnType
import org.jetbrains.exposed.v1.core.QueryBuilder
import org.jetbrains.exposed.v1.core.WindowFunction
import org.jetbrains.exposed.v1.core.WindowFunctionDefinition
import org.jetbrains.exposed.v1.core.append
import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import java.io.File
import java.sql.ResultSet
import java.util.Properties
import kotlin.time.ExperimentalTime
import kotlin.time.Instant
class Database {
val host: String
val port: String
val database: String
init {
val config = File(System.getProperty("user.home"), "mysql.properties")
if (!config.exists()) {
throw CliktError("Config file not found!")
}
val props = Properties();
props.load(config.inputStream())
host = props.getProperty("host")
port = props.getProperty("port")
database = props.getProperty("database")
val username = props.getProperty("user")
val password = props.getProperty("password")
val url = "jdbc:mariadb://$host:$port/$database"
Database.Companion.connect(url, driver = "org.mariadb.jdbc.Driver", user = username, password = password)
}
fun <T> execute(sql: String, transform: (ResultSet) -> T): List<T> = transaction {
val result = mutableListOf<T>()
exec(sql) { rs ->
while (rs.next()) {
result += transform(rs)
}
}
return@transaction result
}
fun <T> executeSingle(sql: String, transform: (ResultSet) -> T): T? {
return execute(sql) { rs ->
if (!rs.next()) {
return@execute null
}
transform(rs)
}.single()
}
}
class UnixTimestamp<T: Any, in S>(
val expr: Expression<in S>,
columnType: IColumnType<T>
) : Function<T?>(columnType), WindowFunction<T?> {
override fun toQueryBuilder(queryBuilder: QueryBuilder) = queryBuilder {
append("UNIX_TIMESTAMP(", expr, ")")
}
override fun over(): WindowFunctionDefinition<T?> {
return WindowFunctionDefinition(columnType, this)
}
}
@OptIn(ExperimentalTime::class)
fun ExpressionWithColumnType<Instant>.unixTimestamp(): UnixTimestamp<Long, Instant> = UnixTimestamp(this, LongColumnType())

View File

@ -0,0 +1,32 @@
package de.steamwar.db.schema
import org.jetbrains.exposed.v1.core.dao.id.EntityID
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
import org.jetbrains.exposed.v1.dao.IntEntity
import org.jetbrains.exposed.v1.dao.IntEntityClass
import org.jetbrains.exposed.v1.datetime.timestamp
import kotlin.time.ExperimentalTime
@OptIn(ExperimentalTime::class)
object Punishments: IntIdTable("Punishments", "PunishmentId") {
val user = integer("userid").references(SteamwarUserTable.id)
val punisher = integer("punisher").references(SteamwarUserTable.id)
val type = varchar("type", 16)
val reason = text("reason")
val starttime = timestamp("starttime")
val endtime = timestamp("endtime")
val perma = bool("perma")
}
@OptIn(ExperimentalTime::class)
class Punishment(id: EntityID<Int>): IntEntity(id) {
companion object : IntEntityClass<Punishment>(Punishments)
var user by SteamwarUser referencedOn Punishments.user
var punisher by SteamwarUser referencedOn Punishments.punisher
var type by Punishments.type
var reason by Punishments.reason
var starttime by Punishments.starttime
var endtime by Punishments.endtime
var perma by Punishments.perma
}

View File

@ -0,0 +1,12 @@
package de.steamwar.db.schema
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.datetime.timestamp
import kotlin.time.ExperimentalTime
@OptIn(ExperimentalTime::class)
object Session: Table("Session") {
val user = integer("userid").references(SteamwarUserTable.id)
val start = timestamp("starttime")
val end = timestamp("endtime")
}

View File

@ -0,0 +1,34 @@
package de.steamwar.db.schema
import org.jetbrains.exposed.v1.core.dao.id.EntityID
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
import org.jetbrains.exposed.v1.dao.IntEntity
import org.jetbrains.exposed.v1.dao.IntEntityClass
object SteamwarUserTable: IntIdTable("UserData") {
val uuid = uuid("uuid")
val username = varchar("username", 255)
val team = integer("team").references(TeamTable.id)
val leader = bool("leader")
val locale = varchar("locale", 16)
val manualLocale = bool("manuallocale")
val bedrock = bool("bedrock")
val password = varchar("password", 1024)
val discordId = long("DiscordId")
}
class SteamwarUser(id: EntityID<Int>): IntEntity(id) {
companion object : IntEntityClass<SteamwarUser>(SteamwarUserTable)
var uuid by SteamwarUserTable.uuid
var username by SteamwarUserTable.username
var team by Team referencedOn SteamwarUserTable.team
var leader by SteamwarUserTable.leader
var locale by SteamwarUserTable.locale
var manualLocale by SteamwarUserTable.manualLocale
var bedrock by SteamwarUserTable.bedrock
var password by SteamwarUserTable.password
var discordId by SteamwarUserTable.discordId
val punishments by Punishment referrersOn Punishments.user
}

View File

@ -0,0 +1,26 @@
package de.steamwar.db.schema
import org.jetbrains.exposed.v1.core.dao.id.EntityID
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
import org.jetbrains.exposed.v1.dao.IntEntity
import org.jetbrains.exposed.v1.dao.IntEntityClass
object TeamTable: IntIdTable("Team", "TeamId") {
val kuerzel = varchar("TeamKuerzel", 16)
val color = varchar("TeamColor", 16)
val name = varchar("TeamName", 255)
val deleted = bool("TeamDeleted")
val address = varchar("Address", 16)
val port = integer("Port")
}
class Team(id: EntityID<Int>): IntEntity(id) {
companion object : IntEntityClass<Team>(TeamTable)
var kuerzel by TeamTable.kuerzel
var color by TeamTable.color
var name by TeamTable.name
var deleted by TeamTable.deleted
var address by TeamTable.address
var port by TeamTable.port
}