commit 0686dd7c93f3308b141d6ba7acd73d73b24eb89a Author: slon565 Date: Sat Jul 22 08:09:39 2023 +0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..70e5b08 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,80 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("org.springframework.boot") version "3.1.0" + id("io.spring.dependency-management") version "1.1.0" + kotlin("jvm") version "1.8.21" + kotlin("plugin.spring") version "1.8.21" + kotlin("plugin.serialization") version "1.8.21" + kotlin("kapt") version "1.8.21" +} + +group = "ru.sicamp" +version = "0.0.1-SNAPSHOT" +java.sourceCompatibility = JavaVersion.VERSION_17 + +repositories { + mavenCentral() + maven { url = uri("https://maven.pkg.jetbrains.space/public/p/kotlinx-html/maven") } +} + +val exposedVersion = "0.41.1" +val postgresVersion = "42.5.4" +val telegramBotVersion = "6.5.0" +val springBootVersion = "3.1.0" +val serializationVersion = "1.5.0" +val loggingVersion = "3.0.5" +val securityTestVersion = "6.0.2" +val thymeleafVersion = "3.1.1.RELEASE" +val jacksonVersion = "2.15.0" + +dependencies { + kapt("org.springframework.boot:spring-boot-configuration-processor:$springBootVersion") + implementation("org.telegram:telegrambots:$telegramBotVersion") + implementation("org.telegram:telegrambotsextensions:$telegramBotVersion") + implementation("org.telegram:telegrambots-spring-boot-starter:$telegramBotVersion") + + implementation("org.springframework.boot:spring-boot-starter:$springBootVersion") { + exclude("org.springframework.boot:spring-boot-starter-tomcat") + } + implementation("org.springframework.boot:spring-boot-starter-web:$springBootVersion") { + exclude("org.springframework.boot:spring-boot-starter-tomcat") + } + implementation("org.springframework.boot:spring-boot-starter-security:$springBootVersion") { + exclude("org.springframework.boot:spring-boot-starter-tomcat") + } + implementation("org.springframework.boot:spring-boot-starter-jetty:$springBootVersion") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") + + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.jetbrains.exposed:exposed-core:$exposedVersion") + implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion") + implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion") + implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion") + implementation("io.github.microutils:kotlin-logging-jvm:$loggingVersion") + implementation("org.postgresql:postgresql:$postgresVersion") + + //testImplementation("org.springframework.security:spring-security-test:$securityTestVersion") + testImplementation("org.springframework.boot:spring-boot-starter-test:$springBootVersion") +} + +configurations.all { + resolutionStrategy { + eachDependency { + if (requested.group == "jakarta.servlet") { + useVersion("5.0.0") + } + } + } +} + +tasks.withType { + kotlinOptions { + freeCompilerArgs = listOf("-Xjsr305=strict") + jvmTarget = "17" + } +} + +tasks.withType { + useJUnitPlatform() +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..774fae8 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..a69d9cb --- /dev/null +++ b/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f127cfd --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..776ebca --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "sicamp-helper" diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/ApplicationConfig.kt b/src/main/kotlin/ru/sicamp/sicamphelper/ApplicationConfig.kt new file mode 100644 index 0000000..0c3c52d --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/ApplicationConfig.kt @@ -0,0 +1,50 @@ +package ru.sicamp.sicamphelper + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.KotlinFeature +import com.fasterxml.jackson.module.kotlin.KotlinModule +import org.jetbrains.exposed.sql.DatabaseConfig +import org.postgresql.PGProperty +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import javax.sql.DataSource +import org.postgresql.ds.PGSimpleDataSource +import org.springframework.beans.factory.annotation.Value + +@Configuration +class ApplicationConfig { + @Bean + fun datasource( + @Value("\${spring.datasource.url}") + url: String, + @Value("\${spring.datasource.username}") + username: String, + @Value("\${spring.datasource.password}") + password: String, + ): DataSource { + val dataSource = PGSimpleDataSource() + dataSource.setURL(url) + dataSource.user = username + dataSource.password = password + dataSource.setProperty(PGProperty.REWRITE_BATCHED_INSERTS, "true") + + return dataSource + } + + @Bean + fun objectMapper(): ObjectMapper = ObjectMapper().registerModule( + KotlinModule.Builder() + .withReflectionCacheSize(512) + .configure(KotlinFeature.NullToEmptyCollection, true) + .configure(KotlinFeature.NullToEmptyMap, true) + .configure(KotlinFeature.NullIsSameAsDefault, true) + .configure(KotlinFeature.SingletonSupport, false) + .configure(KotlinFeature.StrictNullChecks, true) + .build() + ) + + @Bean + fun databaseConfig(): DatabaseConfig = DatabaseConfig { + keepLoadedReferencesOutOfTransaction = true + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/Constants.kt b/src/main/kotlin/ru/sicamp/sicamphelper/Constants.kt new file mode 100644 index 0000000..2e6c415 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/Constants.kt @@ -0,0 +1,8 @@ +package ru.sicamp.sicamphelper + +const val CUSTOM_TEXT_SIZE = 500 + +object ConfigNames { + const val CAMP_START = "CAMP_START" + const val CAMP_END = "CAMP_END" +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/InitializingBeanImpl.kt b/src/main/kotlin/ru/sicamp/sicamphelper/InitializingBeanImpl.kt new file mode 100644 index 0000000..8b9b6df --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/InitializingBeanImpl.kt @@ -0,0 +1,61 @@ +package ru.sicamp.sicamphelper + +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.transaction +import org.springframework.beans.factory.InitializingBean +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Component +import ru.sicamp.sicamphelper.db.entity.Group +import ru.sicamp.sicamphelper.db.entity.User +import ru.sicamp.sicamphelper.db.table.Groups +import ru.sicamp.sicamphelper.db.table.Tables +import ru.sicamp.sicamphelper.db.table.Users +import java.time.Duration +import java.time.LocalDateTime +import javax.sql.DataSource + +@Component +class InitializingBeanImpl( + private val datasource: DataSource, + private val passwordEncoder: PasswordEncoder, + private val databaseConfig: DatabaseConfig, +) : InitializingBean { + override fun afterPropertiesSet() { + val db = Database.connect( + datasource = datasource, + databaseConfig = databaseConfig + ) + + transaction(db) { + addLogger(StdOutSqlLogger) + + SchemaUtils.create(*Tables.values().map { it.table }.toTypedArray()) + } + + transaction { + val now = LocalDateTime.now() + val adminGroup = Group.find { + Groups.name eq "admins" + }.firstOrNull() ?: Group.new { + name = "admins" + tgLink = "" + } + + User.find { + Users.login eq "root" + }.firstOrNull() ?: User.new { + login = "root" + _password = passwordEncoder.encode("123456") + role = Users.Role.ADMIN + group = adminGroup + name = "Рут Рутович" + sex = Users.Sex.OTHER + age = 12 + enabled = true + locked = false + accountExpiration = now + Duration.ofDays(60) + credentialsExpiration = now + Duration.ofDays(60) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/SicampHelperApplication.kt b/src/main/kotlin/ru/sicamp/sicamphelper/SicampHelperApplication.kt new file mode 100644 index 0000000..b19c602 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/SicampHelperApplication.kt @@ -0,0 +1,11 @@ +package ru.sicamp.sicamphelper + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class SicampHelperApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/api/bot/BotConfig.kt b/src/main/kotlin/ru/sicamp/sicamphelper/api/bot/BotConfig.kt new file mode 100644 index 0000000..8073dc0 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/api/bot/BotConfig.kt @@ -0,0 +1,15 @@ +package ru.sicamp.sicamphelper.api.bot + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.telegram.telegrambots.meta.api.methods.updates.SetWebhook + +@Configuration +class BotConfig { + @Bean + fun setWebhook( + @Value("bot.token") + token: String + ) = SetWebhook.builder().secretToken(token).build() +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/api/bot/TelegramPrivateBot.kt b/src/main/kotlin/ru/sicamp/sicamphelper/api/bot/TelegramPrivateBot.kt new file mode 100644 index 0000000..d0404a7 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/api/bot/TelegramPrivateBot.kt @@ -0,0 +1,38 @@ +package ru.sicamp.sicamphelper.api.bot + +import mu.KLogging +import org.springframework.core.env.Environment +import org.springframework.stereotype.Component +import org.telegram.telegrambots.extensions.bots.commandbot.TelegramLongPollingCommandBot +import org.telegram.telegrambots.meta.api.methods.send.SendMessage +import org.telegram.telegrambots.meta.api.objects.Update +import ru.sicamp.sicamphelper.api.command.Command + +/* +@Component +class TelegramPrivateBot( + commands: List>, + env: Environment +) : TelegramLongPollingCommandBot() { + private val botName = env.getProperty("bot.username", "SicampHelper") + + init { + commands.forEach { + register(it) + } + } + + override fun getBotUsername(): String { + return botName + } + + override fun processNonCommandUpdate(update: Update?) { + logger.warn("Received non command update. Message: ${update?.message}") + } + + fun send() { + val answer = SendMessage.builder().chatId(10L).text("ssasa") + } + + companion object : KLogging() +}*/ diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/api/bot/TelegramPublicBot.kt b/src/main/kotlin/ru/sicamp/sicamphelper/api/bot/TelegramPublicBot.kt new file mode 100644 index 0000000..d0ce4e8 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/api/bot/TelegramPublicBot.kt @@ -0,0 +1,34 @@ +package ru.sicamp.sicamphelper.api.bot + +import mu.KLogging +import org.springframework.beans.factory.annotation.Value +import org.springframework.core.env.Environment +import org.springframework.stereotype.Component +import org.telegram.telegrambots.extensions.bots.commandbot.TelegramLongPollingCommandBot +import org.telegram.telegrambots.meta.api.methods.send.SendMessage +import org.telegram.telegrambots.meta.api.objects.Update +import org.telegram.telegrambots.starter.SpringWebhookBot +import ru.sicamp.sicamphelper.api.command.PublicCommand + +@Component +class TelegramPublicBot( + commands: List>, + @Value("\${bot.username}") + private val botName: String, +) : TelegramLongPollingCommandBot() { // TODO: switch to webHook? + init { + commands.forEach { + register(it) + } + } + + override fun getBotUsername(): String { + return botName + } + + override fun processNonCommandUpdate(update: Update?) { + logger.warn("Received non command update. Message: ${update?.message}") + } + + companion object : KLogging() +} diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/api/command/Command.kt b/src/main/kotlin/ru/sicamp/sicamphelper/api/command/Command.kt new file mode 100644 index 0000000..e8704f4 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/api/command/Command.kt @@ -0,0 +1,67 @@ +package ru.sicamp.sicamphelper.api.command + +import kotlinx.coroutines.runBlocking +import mu.KLogging +import org.telegram.telegrambots.extensions.bots.commandbot.commands.BotCommand +import org.telegram.telegrambots.meta.api.methods.send.SendMessage +import org.telegram.telegrambots.meta.api.objects.Chat +import org.telegram.telegrambots.meta.api.objects.User +import org.telegram.telegrambots.meta.bots.AbsSender +import ru.sicamp.sicamphelper.model.metadata.RequestSource +import ru.sicamp.sicamphelper.db.table.Users +import ru.sicamp.sicamphelper.model.request.Request +import ru.sicamp.sicamphelper.model.request.RequestWrapper +import ru.sicamp.sicamphelper.model.response.Response +import ru.sicamp.sicamphelper.model.response.ResponseWrapper +import ru.sicamp.sicamphelper.model.response.TelegramResponse +import ru.sicamp.sicamphelper.service.UserService +import ru.sicamp.sicamphelper.util.Extractor + +abstract class Command( + private val command: Commands +) : BotCommand(command.name, command.description), Extractor { + protected abstract val userService: UserService + + protected abstract suspend fun executeRequest(request: RequestWrapper): ResponseWrapper + + override fun execute(absSender: AbsSender?, user: User?, chat: Chat?, arguments: Array?) { + val internalUser = authorizeCommand(user) ?: TODO("Respond unauthorized request") + val request = try { + RequestWrapper( + issuer = internalUser, + source = RequestSource.TELEGRAM, + body = extract(arguments) + ) + } catch (e: Exception) { + TODO("Respond error") + } + try { + val response = runBlocking { + executeRequest(request) + } + response.getTelegramMessage()?.let { + absSender?.execute(it) + TODO("Respond success") + } + } catch (e: Exception) { + logger.error { e } + TODO("Respond internal error") + } + } + + private fun authorizeCommand(user: User?): ru.sicamp.sicamphelper.db.entity.User? = userService.findUserByTgUser( + user ?: error("No user provided for command $commandIdentifier") + ).let { + if (it == null) { + logger.warn("Not found user with tgUsername: ${user.userName}, tgId: ${user.id}") + return@let null + } + if (it.role !in Users.Role.upperRoles(command.minimalRole)) { + logger.warn("User $it is not authorized to perform command $commandIdentifier") + return@let null + } + it + } + + companion object : KLogging() +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/api/command/Commands.kt b/src/main/kotlin/ru/sicamp/sicamphelper/api/command/Commands.kt new file mode 100644 index 0000000..518614f --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/api/command/Commands.kt @@ -0,0 +1,19 @@ +package ru.sicamp.sicamphelper.api.command + +import ru.sicamp.sicamphelper.db.table.Users + +enum class Commands( + val commandName: String, + val description: String, + val minimalRole: Users.Role, +) { + INFO("info", "Получить информацию по пользователю", Users.Role.STUDENT), + SCHEDULE("schedule", "Просмотреть расписание", Users.Role.STUDENT), + SCHEDULE_GROUP("schedule_group", "Просмотреть расписание группы", Users.Role.STUDENT) + ; + + companion object { + const val PREFIX_URL = "command" + const val SCHEDULE_URL = "schedule" + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/api/command/PrivateCommand.kt b/src/main/kotlin/ru/sicamp/sicamphelper/api/command/PrivateCommand.kt new file mode 100644 index 0000000..6f84200 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/api/command/PrivateCommand.kt @@ -0,0 +1,7 @@ +package ru.sicamp.sicamphelper.api.command + +import ru.sicamp.sicamphelper.model.request.Request +import ru.sicamp.sicamphelper.model.response.Response +import ru.sicamp.sicamphelper.model.response.TelegramResponse + +abstract class PrivateCommand(command: Commands) : Command(command) \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/api/command/PublicCommand.kt b/src/main/kotlin/ru/sicamp/sicamphelper/api/command/PublicCommand.kt new file mode 100644 index 0000000..5273f28 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/api/command/PublicCommand.kt @@ -0,0 +1,7 @@ +package ru.sicamp.sicamphelper.api.command + +import ru.sicamp.sicamphelper.model.request.Request +import ru.sicamp.sicamphelper.model.response.Response +import ru.sicamp.sicamphelper.model.response.TelegramResponse + +abstract class PublicCommand(command: Commands) : Command(command) \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/api/command/Utils.kt b/src/main/kotlin/ru/sicamp/sicamphelper/api/command/Utils.kt new file mode 100644 index 0000000..c19f5a9 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/api/command/Utils.kt @@ -0,0 +1,14 @@ +package ru.sicamp.sicamphelper.api.command + +import java.time.LocalDate +import java.time.format.DateTimeParseException + +fun String?.toLocalDate(): LocalDate? = try { + LocalDate.parse(this) +} catch (e: DateTimeParseException) { + null +} + +const val EMPTY_TOKEN = "_" + +fun Array.getValueOrNull(index: Int) = getOrNull(index)?.let { if (it == EMPTY_TOKEN) null else it } diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/api/command/impl/InfoCommand.kt b/src/main/kotlin/ru/sicamp/sicamphelper/api/command/impl/InfoCommand.kt new file mode 100644 index 0000000..ed0681b --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/api/command/impl/InfoCommand.kt @@ -0,0 +1,28 @@ +package ru.sicamp.sicamphelper.api.command.impl + +import org.springframework.stereotype.Component +import ru.sicamp.sicamphelper.api.command.Commands +import ru.sicamp.sicamphelper.api.command.PublicCommand +import ru.sicamp.sicamphelper.api.command.getValueOrNull +import ru.sicamp.sicamphelper.model.request.RequestWrapper +import ru.sicamp.sicamphelper.model.data.UserInput +import ru.sicamp.sicamphelper.model.request.info.InfoRequest +import ru.sicamp.sicamphelper.model.response.info.InfoResponse +import ru.sicamp.sicamphelper.service.InfoService +import ru.sicamp.sicamphelper.service.UserService + +@Component +class InfoCommand( + override val userService: UserService, + private val infoService: InfoService +) : PublicCommand(Commands.INFO) { + override suspend fun executeRequest(request: RequestWrapper) = infoService.getInfo(request) + + override fun extract( + arguments: Array? + ) = InfoRequest( + userInput = arguments?.getValueOrNull(0)?.let { + UserInput(it) + } + ) +} diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/api/command/impl/ScheduleCommand.kt b/src/main/kotlin/ru/sicamp/sicamphelper/api/command/impl/ScheduleCommand.kt new file mode 100644 index 0000000..6838ef3 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/api/command/impl/ScheduleCommand.kt @@ -0,0 +1,35 @@ +package ru.sicamp.sicamphelper.api.command.impl + +import mu.KLogging +import org.springframework.stereotype.Controller +import ru.sicamp.sicamphelper.api.command.Commands +import ru.sicamp.sicamphelper.api.command.PublicCommand +import ru.sicamp.sicamphelper.api.command.getValueOrNull +import ru.sicamp.sicamphelper.api.command.toLocalDate +import ru.sicamp.sicamphelper.model.request.RequestWrapper +import ru.sicamp.sicamphelper.model.data.UserInput +import ru.sicamp.sicamphelper.model.request.schedule.ScheduleRequest +import ru.sicamp.sicamphelper.model.response.ResponseWrapper +import ru.sicamp.sicamphelper.model.response.schedule.ScheduleResponse +import ru.sicamp.sicamphelper.service.ScheduleService +import ru.sicamp.sicamphelper.service.UserService + +@Controller +class ScheduleCommand( + override val userService: UserService, + private val scheduleService: ScheduleService +) : PublicCommand(Commands.SCHEDULE) { + override suspend fun executeRequest( + request: RequestWrapper + ): ResponseWrapper = scheduleService.getUserSchedule(request) + + override fun extract( + arguments: Array? + ) = ScheduleRequest( + date = arguments?.getValueOrNull(0).toLocalDate(), + userInput = arguments?.getValueOrNull(1)?.let { UserInput(it) } + ) + + + companion object : KLogging() +} diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/api/command/impl/ScheduleGroupCommand.kt b/src/main/kotlin/ru/sicamp/sicamphelper/api/command/impl/ScheduleGroupCommand.kt new file mode 100644 index 0000000..89463b2 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/api/command/impl/ScheduleGroupCommand.kt @@ -0,0 +1,30 @@ +package ru.sicamp.sicamphelper.api.command.impl + +import org.springframework.stereotype.Component +import ru.sicamp.sicamphelper.api.command.Commands +import ru.sicamp.sicamphelper.api.command.PublicCommand +import ru.sicamp.sicamphelper.api.command.getValueOrNull +import ru.sicamp.sicamphelper.api.command.toLocalDate +import ru.sicamp.sicamphelper.model.data.GroupInput +import ru.sicamp.sicamphelper.model.request.RequestWrapper +import ru.sicamp.sicamphelper.model.request.schedule.ScheduleGroupRequest +import ru.sicamp.sicamphelper.model.response.ResponseWrapper +import ru.sicamp.sicamphelper.model.response.schedule.ScheduleResponse +import ru.sicamp.sicamphelper.service.ScheduleService +import ru.sicamp.sicamphelper.service.UserService + +@Component +class ScheduleGroupCommand( + override val userService: UserService, + private val scheduleService: ScheduleService, +) : PublicCommand(Commands.SCHEDULE_GROUP) { + override suspend fun executeRequest(request: RequestWrapper): ResponseWrapper { + return scheduleService.getSchedulesByGroup(request) + } + + override fun extract(arguments: Array?): ScheduleGroupRequest = + ScheduleGroupRequest( + date = arguments?.getValueOrNull(0).toLocalDate(), + group = arguments?.getValueOrNull(1)?.let { GroupInput(it) } + ) +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/api/controller/UsersController.kt b/src/main/kotlin/ru/sicamp/sicamphelper/api/controller/UsersController.kt new file mode 100644 index 0000000..faee2d1 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/api/controller/UsersController.kt @@ -0,0 +1,31 @@ +package ru.sicamp.sicamphelper.api.controller + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.convertValue +import mu.KLogging +import org.springframework.web.bind.annotation.* +import ru.sicamp.sicamphelper.model.dto.UserDto +import ru.sicamp.sicamphelper.service.UserService + +@RestController +class UsersController( + private val userService: UserService, + private val objectMapper: ObjectMapper, +) { + @PostMapping("/user/create") + fun create(@RequestParam dto: UserDto): String { + val user = userService.createUser(dto) + return user.toString() + } + + @GetMapping("/custom/users/find/{login}") + fun getUser(@PathVariable(name = "login", required = true) login: String): String { + logger.info("Getting user with username: $login!!!!!") + return objectMapper.convertValue(userService.loadUserByUsername(login)) + } + + @GetMapping("/test") + fun test() = "test" + + companion object : KLogging() +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/Config.kt b/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/Config.kt new file mode 100644 index 0000000..d0aa782 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/Config.kt @@ -0,0 +1,11 @@ +package ru.sicamp.sicamphelper.db.entity + +import org.jetbrains.exposed.dao.Entity +import org.jetbrains.exposed.dao.EntityClass +import org.jetbrains.exposed.dao.id.EntityID +import ru.sicamp.sicamphelper.db.table.Configs + +class Config(id: EntityID): Entity(id) { + companion object : EntityClass(Configs) + val value by Configs.value +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/Event.kt b/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/Event.kt new file mode 100644 index 0000000..080549f --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/Event.kt @@ -0,0 +1,25 @@ +package ru.sicamp.sicamphelper.db.entity + +import org.jetbrains.exposed.dao.LongEntity +import org.jetbrains.exposed.dao.LongEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import ru.sicamp.sicamphelper.db.table.Events +import ru.sicamp.sicamphelper.db.table.Schedules + +class Event(id: EntityID): LongEntity(id) { + companion object : LongEntityClass(Events) + + var name by Events.name + var type by Events.type + var registrationType by Events.registrationType + var group by Group optionalReferencedOn Events.group + var eventDate by Events.eventDate + var eventTime by Events.eventTime + var repeat by Events.repeat + var assigneeCount by Events.assigneeCount + var status by Events.status + var description by Events.description + var responsible by User referencedOn Events.responsible + var defaultRoom by Room optionalReferencedOn Events.defaultRoom + val schedules by Schedule referrersOn Schedules.event +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/Group.kt b/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/Group.kt new file mode 100644 index 0000000..cadcb5a --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/Group.kt @@ -0,0 +1,13 @@ +package ru.sicamp.sicamphelper.db.entity + +import org.jetbrains.exposed.dao.LongEntity +import org.jetbrains.exposed.dao.LongEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import ru.sicamp.sicamphelper.db.table.Groups + +class Group(id: EntityID) : LongEntity(id) { + companion object : LongEntityClass(Groups) + + var name by Groups.name + var tgLink by Groups.tgLink +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/GroupRegistration.kt b/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/GroupRegistration.kt new file mode 100644 index 0000000..59a5238 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/GroupRegistration.kt @@ -0,0 +1,13 @@ +package ru.sicamp.sicamphelper.db.entity + +import org.jetbrains.exposed.dao.LongEntity +import org.jetbrains.exposed.dao.LongEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import ru.sicamp.sicamphelper.db.table.GroupRegistrations + +class GroupRegistration(id: EntityID): LongEntity(id) { + companion object : LongEntityClass(GroupRegistrations) + + var schedule by Schedule referencedOn GroupRegistrations.schedule + var group by Group referencedOn GroupRegistrations.group +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/Room.kt b/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/Room.kt new file mode 100644 index 0000000..eb7dd10 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/Room.kt @@ -0,0 +1,14 @@ +package ru.sicamp.sicamphelper.db.entity + +import org.jetbrains.exposed.dao.LongEntity +import org.jetbrains.exposed.dao.LongEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import ru.sicamp.sicamphelper.db.table.Rooms + +class Room(id: EntityID) : LongEntity(id) { + companion object : LongEntityClass(Rooms) + + var name by Rooms.name + var type by Rooms.type + var description by Rooms.description +} diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/Schedule.kt b/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/Schedule.kt new file mode 100644 index 0000000..915b4f2 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/Schedule.kt @@ -0,0 +1,21 @@ +package ru.sicamp.sicamphelper.db.entity + +import org.jetbrains.exposed.dao.LongEntity +import org.jetbrains.exposed.dao.LongEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import ru.sicamp.sicamphelper.db.table.GroupRegistrations +import ru.sicamp.sicamphelper.db.table.Schedules +import ru.sicamp.sicamphelper.db.table.UserRegistrations + +class Schedule(id: EntityID) : LongEntity(id) { + companion object : LongEntityClass(Schedules) + + var dateTime by Schedules.start + var description by Schedules.description + var responsible by User referencedOn Schedules.responsible + var event by Event referencedOn Schedules.event + var room by Room optionalReferencedOn Schedules.room + + val userRegistrations by UserRegistration referrersOn UserRegistrations.schedule + val groupRegistrations by GroupRegistration referrersOn GroupRegistrations.schedule +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/User.kt b/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/User.kt new file mode 100644 index 0000000..5109858 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/User.kt @@ -0,0 +1,48 @@ +package ru.sicamp.sicamphelper.db.entity + +import org.jetbrains.exposed.dao.LongEntity +import org.jetbrains.exposed.dao.LongEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.userdetails.UserDetails +import ru.sicamp.sicamphelper.db.table.Users +import java.time.LocalDateTime + +class User(id: EntityID) : LongEntity(id), UserDetails { + companion object : LongEntityClass(Users) + + var login by Users.login + var name by Users.name + var supervisor by User optionalReferencedOn Users.supervisor + var _password by Users.password + var tgId by Users.tgId + var tgUsername by Users.tgUsername + var role by Users.role + var group by Group referencedOn Users.group + var sex by Users.sex + var age by Users.age + var locked by Users.locked + var enabled by Users.enabled + var accountExpiration by Users.accountExpiration + var credentialsExpiration by Users.credentialsExpiration + + override fun getAuthorities(): MutableCollection = Users.Role.lowerRoles(role).map { + GrantedAuthority { role.name } + }.toMutableList() + + override fun getPassword(): String = _password + + override fun getUsername(): String = login + + override fun isAccountNonExpired(): Boolean = LocalDateTime.now() < accountExpiration + + override fun isAccountNonLocked(): Boolean = !locked + + override fun isCredentialsNonExpired(): Boolean = LocalDateTime.now() < credentialsExpiration + + override fun isEnabled(): Boolean = enabled + + override fun toString(): String { + return "User: {login:$login, tgUsername:$tgUsername, tgId:$tgId, role:$role}" + } +} diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/UserRegistration.kt b/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/UserRegistration.kt new file mode 100644 index 0000000..cdf21c2 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/db/entity/UserRegistration.kt @@ -0,0 +1,13 @@ +package ru.sicamp.sicamphelper.db.entity + +import org.jetbrains.exposed.dao.LongEntity +import org.jetbrains.exposed.dao.LongEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import ru.sicamp.sicamphelper.db.table.UserRegistrations + +class UserRegistration(id: EntityID): LongEntity(id) { + companion object : LongEntityClass(UserRegistrations) + + var schedule by Schedule referencedOn UserRegistrations.schedule + var user by User referencedOn UserRegistrations.user +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Accommodations.kt b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Accommodations.kt new file mode 100644 index 0000000..d162eaf --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Accommodations.kt @@ -0,0 +1,9 @@ +package ru.sicamp.sicamphelper.db.table + +import org.jetbrains.exposed.dao.id.LongIdTable + +object Accommodations : LongIdTable(Tables.ACCOMMODATIONS.name) { + val user = reference("user", Users).uniqueIndex("users_index") + val placeNumber = integer("place_number") + val room = reference("room", Rooms) +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Assignments.kt b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Assignments.kt new file mode 100644 index 0000000..a58741f --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Assignments.kt @@ -0,0 +1,8 @@ +package ru.sicamp.sicamphelper.db.table + +import org.jetbrains.exposed.dao.id.LongIdTable + +object Assignments : LongIdTable("assignments") { + val schedule = reference("schedule", Schedules) + val assignee = reference("assignee", Users) +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Configs.kt b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Configs.kt new file mode 100644 index 0000000..a949afd --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Configs.kt @@ -0,0 +1,10 @@ +package ru.sicamp.sicamphelper.db.table + +import org.jetbrains.exposed.dao.id.IdTable + +object Configs : IdTable("configs") { + override val id = varchar("id", 255).entityId() + val value = varchar("value", 255) + + override val primaryKey: PrimaryKey = PrimaryKey(id) +} diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Events.kt b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Events.kt new file mode 100644 index 0000000..27838f3 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Events.kt @@ -0,0 +1,27 @@ +package ru.sicamp.sicamphelper.db.table + +import org.jetbrains.exposed.dao.id.LongIdTable +import org.jetbrains.exposed.sql.javatime.date +import org.jetbrains.exposed.sql.javatime.duration +import org.jetbrains.exposed.sql.javatime.time +import ru.sicamp.sicamphelper.CUSTOM_TEXT_SIZE + +object Events : LongIdTable("events") { + val name = varchar("name", 255) + val type = enumerationByName("type", 255) + val registrationType = enumerationByName("registration_type", 255) + val group = reference("group", Groups).nullable() + val eventDate = date("event_date").nullable() + val eventTime = time("event_time") + /**Should be in days*/ + val repeat = duration("repeat").nullable() + val assigneeCount = integer("assignee_count").nullable() + val status = enumerationByName("status", 255) + val description = varchar("description", CUSTOM_TEXT_SIZE) + val responsible = reference("responsible", Users) + val defaultRoom = reference("default_room", Rooms).nullable() + + enum class Type { ONCE, REGULAR } + enum class RegistrationType { ALL, GROUP, USER } + enum class Status { SCHEDULE, SKIP_ONCE, DISABLED } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/db/table/GateLogins.kt b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/GateLogins.kt new file mode 100644 index 0000000..7c1e02f --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/GateLogins.kt @@ -0,0 +1,10 @@ +package ru.sicamp.sicamphelper.db.table + +import org.jetbrains.exposed.sql.Table + +object GateLogins : Table("gate_logins") { + val gateLogin = varchar("gate_login", 255) + val user = reference("user", Users) + + override val primaryKey: PrimaryKey = PrimaryKey(gateLogin) +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/db/table/GroupRegistrations.kt b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/GroupRegistrations.kt new file mode 100644 index 0000000..b261597 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/GroupRegistrations.kt @@ -0,0 +1,8 @@ +package ru.sicamp.sicamphelper.db.table + +import org.jetbrains.exposed.dao.id.LongIdTable + +object GroupRegistrations : LongIdTable("group_registrations") { + val group = reference("group", Groups) + val schedule = reference("schedule", Schedules) +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Groups.kt b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Groups.kt new file mode 100644 index 0000000..1edb4b9 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Groups.kt @@ -0,0 +1,8 @@ +package ru.sicamp.sicamphelper.db.table + +import org.jetbrains.exposed.dao.id.LongIdTable + +object Groups : LongIdTable("groups") { + val name = varchar("name", 255) + val tgLink = varchar("tg_link", 255) +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Rooms.kt b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Rooms.kt new file mode 100644 index 0000000..d973e3f --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Rooms.kt @@ -0,0 +1,11 @@ +package ru.sicamp.sicamphelper.db.table + +import org.jetbrains.exposed.dao.id.LongIdTable + +object Rooms : LongIdTable("rooms") { + val name = varchar("name", 255) + val type = enumerationByName("type", 255) + val description = varchar("description", 255).nullable() + + enum class Type { DORMITORY, AUDIENCE, CANTEEN, COMMON_SPACE } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Schedules.kt b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Schedules.kt new file mode 100644 index 0000000..e50b3de --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Schedules.kt @@ -0,0 +1,16 @@ +package ru.sicamp.sicamphelper.db.table + +import org.jetbrains.exposed.dao.id.LongIdTable +import org.jetbrains.exposed.sql.javatime.datetime +import ru.sicamp.sicamphelper.CUSTOM_TEXT_SIZE + +object Schedules : LongIdTable("schedules") { + val start = datetime("start").index("datetime_index") + val end = datetime("end") + val description = varchar("description", CUSTOM_TEXT_SIZE).nullable() + val responsible = reference("responsible", Users) + val event = reference("event", Events).index("event_index") + val room = reference("room", Rooms).nullable() + + val eventStartIdx = index("event_start_idx", isUnique = true, event, start) +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Tables.kt b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Tables.kt new file mode 100644 index 0000000..0fec3ae --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Tables.kt @@ -0,0 +1,22 @@ +package ru.sicamp.sicamphelper.db.table + +import org.jetbrains.exposed.sql.Table + +enum class Tables(val tableName: String, val table: Table) { + ACCOMMODATIONS("accommodations", Accommodations), + ASSIGNMENTS("assignments", Assignments), + CONFIGS("configs", Configs), + EVENTS("events", Events), + GATE_LOGINS("gate_logins", GateLogins), + GROUP_REGISTRATIONS("group_registrations", GroupRegistrations), + GROUPS("groups", Groups), + ROOMS("rooms", Rooms), + SCHEDULES("schedules", Schedules), + USER_REGISTRATIONS("user_registrations", UserRegistrations), + USERS("users", Users), + WARNINGS("warnings", Warnings); + + companion object { + fun tableByName(name: String): Tables? = values().find { it.tableName == name } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/db/table/UserRegistrations.kt b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/UserRegistrations.kt new file mode 100644 index 0000000..6186283 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/UserRegistrations.kt @@ -0,0 +1,8 @@ +package ru.sicamp.sicamphelper.db.table + +import org.jetbrains.exposed.dao.id.LongIdTable + +object UserRegistrations : LongIdTable("user_registrations") { + val user = reference("user", Users) + val schedule = reference("schedule", Schedules) +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Users.kt b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Users.kt new file mode 100644 index 0000000..c82e981 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Users.kt @@ -0,0 +1,31 @@ +package ru.sicamp.sicamphelper.db.table + +import org.jetbrains.exposed.dao.id.LongIdTable +import org.jetbrains.exposed.sql.javatime.datetime + +object Users : LongIdTable("users") { + val name = varchar("name", 255) + val supervisor = reference("supervisor", Users).nullable() + val role = enumerationByName("role", 255) + val group = reference("group", Groups) + val tgId = long("tg_id").uniqueIndex("tg_id_index").nullable() + val tgUsername = varchar("tg_username", 255).nullable() + val sex = enumerationByName("sex", 255) + val age = integer("age") + + val login = varchar("login", 255) + val password = varchar("password", 255) + val enabled = bool("enabled") + val locked = bool("locked") + val accountExpiration = datetime("account_expiration") + val credentialsExpiration = datetime("credentials_expiration") + //TODO add photos + + enum class Role(val level: Int) { STUDENT(0), TEACHER(1), SENIOR_TEACHER(2), PRINCIPAL(3), ADMIN(4); + companion object { + fun upperRoles(role: Role): List = values().filter { it.level <= role.level } + fun lowerRoles(role: Role): List = values().filter { it.level >= role.level } + } + } + enum class Sex { MALE, FEMALE, OTHER } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Warnings.kt b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Warnings.kt new file mode 100644 index 0000000..181d73c --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/db/table/Warnings.kt @@ -0,0 +1,9 @@ +package ru.sicamp.sicamphelper.db.table + +import org.jetbrains.exposed.dao.id.LongIdTable + +object Warnings : LongIdTable("warnings") { + val user = reference("user", Users) + val issuer = reference("issuer", Users) + val description = varchar("description", 255) +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/helper/ScheduleHelper.kt b/src/main/kotlin/ru/sicamp/sicamphelper/helper/ScheduleHelper.kt new file mode 100644 index 0000000..ae22c17 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/helper/ScheduleHelper.kt @@ -0,0 +1,64 @@ +package ru.sicamp.sicamphelper.helper + +import org.jetbrains.exposed.dao.with +import org.springframework.stereotype.Component +import ru.sicamp.sicamphelper.ConfigNames +import ru.sicamp.sicamphelper.db.entity.Event +import ru.sicamp.sicamphelper.db.entity.Room +import ru.sicamp.sicamphelper.db.entity.Schedule +import ru.sicamp.sicamphelper.db.entity.User +import ru.sicamp.sicamphelper.service.ConfigService +import ru.sicamp.sicamphelper.util.InTransaction + +@Component +class ScheduleHelper( + private val configService: ConfigService +) { + @InTransaction + fun updateSchedulesByEvent(newEvent: Event, oldEvent: Event? = null): List { + val oldScheduleUpdates = oldEvent?.let { old -> + old.schedules.notForUpdate().mapIndexedNotNull { index, schedule -> + ScheduleUpdate( + description = schedule.description, + responsible = schedule.responsible.takeIf { schedule.responsible != old.responsible }, + room = schedule.room.takeIf { schedule.room != old.defaultRoom } + ).takeIf { !it.isEmpty() }?.let { + index to it + } + }.toMap() + } + + val startDate = newEvent.eventDate ?: configService.getDate(ConfigNames.CAMP_START) + val dates = generateSequence(seed = startDate) { prevDate -> + val nextDate = prevDate + newEvent.repeat + if (nextDate <= configService.getDate(ConfigNames.CAMP_END)) { + nextDate + } else { + null + } + } + return dates.mapIndexed { index, date -> + val scheduleUpdate = oldScheduleUpdates?.get(index) + Schedule.new { + dateTime = date.atTime(newEvent.eventTime) + responsible = scheduleUpdate?.responsible ?: newEvent.responsible + event = newEvent + room = scheduleUpdate?.room ?: newEvent.defaultRoom + } + } + .toList() + .with( + Schedule::event, + Schedule::responsible, + Schedule::room + ) + } + + private data class ScheduleUpdate( + val description: String?, + val responsible: User?, + val room: Room? + ) { + fun isEmpty() = description != null || responsible != null || room != null + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/model/data/Change.kt b/src/main/kotlin/ru/sicamp/sicamphelper/model/data/Change.kt new file mode 100644 index 0000000..8df6d25 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/model/data/Change.kt @@ -0,0 +1,49 @@ +/* +package ru.sicamp.sicamphelper.model.data + +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.json.Json +import mu.KLogging +import org.jetbrains.exposed.sql.Column +import ru.sicamp.sicamphelper.db.table.Tables + +class Change( + change: String, + private val deserializationStrategy: DeserializationStrategy +) { + val column: Column = extractColumn(change) + val newValue: T = extractValue(change) + + */ +/**Extract change from string + * @param change format: "table_name.column_name=newValue" *//* + + @Suppress("", "UNCHECKED_CAST") + private fun extractColumn(change: String): Column { + if (!format.matches(change)) { + throw BadFormatException(change) + } + + val (columnAndTable, _) = change.split("=") + val (tableName, columnName) = columnAndTable.split(".") + + val table = Tables.tableByName(tableName)?.table ?: throw BadFormatException(change) + return table.columns.find { it.name == columnName } as Column? ?: throw BadFormatException(change) + } + + class BadFormatException(change: String) : Exception("Bad format exception for change: $change") + + private fun extractValue(change: String): T { + if (!format.matches(change)) { + throw BadFormatException(change) + } + + val (_, valueString) = change.split("=") + + return Json.decodeFromString(deserializationStrategy, valueString) + } + + private val format = "[\\w_]+\\.[\\w_]+=.+".toRegex() + + companion object : KLogging() +}*/ diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/model/data/GroupInput.kt b/src/main/kotlin/ru/sicamp/sicamphelper/model/data/GroupInput.kt new file mode 100644 index 0000000..7640204 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/model/data/GroupInput.kt @@ -0,0 +1,18 @@ +package ru.sicamp.sicamphelper.model.data + +class GroupInput( + input: String +) { + val type = Type.typeByPrefix(input.firstOrNull()) + + val id: Long? = if (type == Type.ID) input.tail().toString().toLong() else null + val name: String? = if (type == Type.NAME) input.tail().toString() else null + + enum class Type(val prefix: Char) { + NAME('@'), ID('#'); + + companion object { + fun typeByPrefix(char: Char?) = values().find { it.prefix == char } + } + } +} diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/model/data/UserInput.kt b/src/main/kotlin/ru/sicamp/sicamphelper/model/data/UserInput.kt new file mode 100644 index 0000000..a9003dd --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/model/data/UserInput.kt @@ -0,0 +1,24 @@ +package ru.sicamp.sicamphelper.model.data + +/**Class extracts user from user input. Also used to represent user in views. Format: @tgUsername|$gateLogin|#userId */ +class UserInput( + input: String +) { + val type: Type? = Type.typeByPrefix(input.firstOrNull()) + + val tgUsername = if (type == Type.TG_USERNAME) input.tail().toString() else null + val gateLogin = if (type == Type.GATE_LOGIN) input.tail().toString() else null + val userId = if (type == Type.USER_ID) input.tail().toString().toLong() else null + + enum class Type(val prefix: Char) { + TG_USERNAME('@'), GATE_LOGIN('$'), USER_ID('#'); + + companion object { + fun typeByPrefix(char: Char?) = values().find { it.prefix == char } + } + } + + companion object { + fun fromTgUser(tgUser: org.telegram.telegrambots.meta.api.objects.User) = UserInput(Type.TG_USERNAME.prefix + tgUser.userName) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/model/data/Utils.kt b/src/main/kotlin/ru/sicamp/sicamphelper/model/data/Utils.kt new file mode 100644 index 0000000..b75272a --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/model/data/Utils.kt @@ -0,0 +1,3 @@ +package ru.sicamp.sicamphelper.model.data + +fun String.tail() = subSequence(1..lastIndex) \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/model/dto/EventDto.kt b/src/main/kotlin/ru/sicamp/sicamphelper/model/dto/EventDto.kt new file mode 100644 index 0000000..b6a5112 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/model/dto/EventDto.kt @@ -0,0 +1,20 @@ +package ru.sicamp.sicamphelper.model.dto + +import ru.sicamp.sicamphelper.db.table.Events +import java.time.Duration +import java.time.LocalDate +import java.time.LocalTime + +data class EventDto( + val name: String, + val registrationType: Events.RegistrationType, + val group: Long?, + val eventDate: LocalDate?, + val eventTime: LocalTime, + val repeat: Duration?, + val assigneeCount: Int?, + val status: Events.Status, + val description: String, + val responsible: Long, + val defaultRoom: Long?, +) diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/model/dto/ScheduleDto.kt b/src/main/kotlin/ru/sicamp/sicamphelper/model/dto/ScheduleDto.kt new file mode 100644 index 0000000..0a1686f --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/model/dto/ScheduleDto.kt @@ -0,0 +1,11 @@ +package ru.sicamp.sicamphelper.model.dto + +import java.time.LocalDateTime + +data class ScheduleDto( + val id: Long?, + val dateTime: LocalDateTime?, + val roomId: Long?, + val scheduleDescription: String?, + val responsibleId: Long?, +) diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/model/dto/UserDto.kt b/src/main/kotlin/ru/sicamp/sicamphelper/model/dto/UserDto.kt new file mode 100644 index 0000000..60f62c6 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/model/dto/UserDto.kt @@ -0,0 +1,15 @@ +package ru.sicamp.sicamphelper.model.dto + +import ru.sicamp.sicamphelper.db.table.Users + +data class UserDto( + val login: String, + val password: String?, + val role: Users.Role, + val name: String, + val sex: Users.Sex, + val age: Int, + val tgId: Long? = null, + val tgUsername: String? = null, + val enabled: Boolean = false, +) diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/model/metadata/Cursor.kt b/src/main/kotlin/ru/sicamp/sicamphelper/model/metadata/Cursor.kt new file mode 100644 index 0000000..39714c5 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/model/metadata/Cursor.kt @@ -0,0 +1,5 @@ +package ru.sicamp.sicamphelper.model.metadata + +data class Cursor( + val data: String +) \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/model/metadata/Error.kt b/src/main/kotlin/ru/sicamp/sicamphelper/model/metadata/Error.kt new file mode 100644 index 0000000..d2f96d3 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/model/metadata/Error.kt @@ -0,0 +1,6 @@ +package ru.sicamp.sicamphelper.model.metadata + +data class Error( + val code: Int, + val message: String +) diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/model/metadata/MetaData.kt b/src/main/kotlin/ru/sicamp/sicamphelper/model/metadata/MetaData.kt new file mode 100644 index 0000000..bf55feb --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/model/metadata/MetaData.kt @@ -0,0 +1,6 @@ +package ru.sicamp.sicamphelper.model.metadata + +data class MetaData( + val error: Error?, + val cursor: Cursor? +) \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/model/metadata/RequestSource.kt b/src/main/kotlin/ru/sicamp/sicamphelper/model/metadata/RequestSource.kt new file mode 100644 index 0000000..b1fb877 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/model/metadata/RequestSource.kt @@ -0,0 +1,3 @@ +package ru.sicamp.sicamphelper.model.metadata + +enum class RequestSource { TELEGRAM, WEB } \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/model/request/Request.kt b/src/main/kotlin/ru/sicamp/sicamphelper/model/request/Request.kt new file mode 100644 index 0000000..ec4ce9a --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/model/request/Request.kt @@ -0,0 +1,3 @@ +package ru.sicamp.sicamphelper.model.request + +interface Request diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/model/request/RequestWrapper.kt b/src/main/kotlin/ru/sicamp/sicamphelper/model/request/RequestWrapper.kt new file mode 100644 index 0000000..5150f3f --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/model/request/RequestWrapper.kt @@ -0,0 +1,38 @@ +package ru.sicamp.sicamphelper.model.request + +import ru.sicamp.sicamphelper.model.metadata.Cursor +import ru.sicamp.sicamphelper.model.metadata.Error +import ru.sicamp.sicamphelper.model.metadata.MetaData +import ru.sicamp.sicamphelper.model.metadata.RequestSource +import ru.sicamp.sicamphelper.model.response.Response +import ru.sicamp.sicamphelper.model.response.ResponseWrapper +import ru.sicamp.sicamphelper.db.entity.User + +class RequestWrapper( + val issuer: User, + val source: RequestSource, + val cursor: Cursor? = null, + val body: REQ +) { + fun success(response: RES, newCursor: Cursor? = null): ResponseWrapper = + ResponseWrapper( + issuer = issuer, + source = source, + metaData = MetaData( + cursor = newCursor, + error = null + ), + response = response + ) + + fun fail(statusCode: Int = 500, message: String = "Unknown Error"): ResponseWrapper = + ResponseWrapper( + issuer = issuer, + source = source, + metaData = MetaData( + error = Error(statusCode, message), + cursor = cursor, + ), + response = null + ) +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/model/request/info/InfoRequest.kt b/src/main/kotlin/ru/sicamp/sicamphelper/model/request/info/InfoRequest.kt new file mode 100644 index 0000000..5c9b2ac --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/model/request/info/InfoRequest.kt @@ -0,0 +1,8 @@ +package ru.sicamp.sicamphelper.model.request.info + +import ru.sicamp.sicamphelper.model.data.UserInput +import ru.sicamp.sicamphelper.model.request.Request + +data class InfoRequest( + val userInput: UserInput? +) : Request diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/model/request/schedule/ScheduleGroupRequest.kt b/src/main/kotlin/ru/sicamp/sicamphelper/model/request/schedule/ScheduleGroupRequest.kt new file mode 100644 index 0000000..308fe04 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/model/request/schedule/ScheduleGroupRequest.kt @@ -0,0 +1,10 @@ +package ru.sicamp.sicamphelper.model.request.schedule + +import ru.sicamp.sicamphelper.model.data.GroupInput +import ru.sicamp.sicamphelper.model.request.Request +import java.time.LocalDate + +data class ScheduleGroupRequest( + val group: GroupInput?, + val date: LocalDate? +) : Request \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/model/request/schedule/ScheduleRequest.kt b/src/main/kotlin/ru/sicamp/sicamphelper/model/request/schedule/ScheduleRequest.kt new file mode 100644 index 0000000..ea20133 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/model/request/schedule/ScheduleRequest.kt @@ -0,0 +1,10 @@ +package ru.sicamp.sicamphelper.model.request.schedule + +import ru.sicamp.sicamphelper.model.data.UserInput +import ru.sicamp.sicamphelper.model.request.Request +import java.time.LocalDate + +data class ScheduleRequest( + val userInput: UserInput?, + val date: LocalDate? +) : Request \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/model/response/Response.kt b/src/main/kotlin/ru/sicamp/sicamphelper/model/response/Response.kt new file mode 100644 index 0000000..a4f112a --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/model/response/Response.kt @@ -0,0 +1,3 @@ +package ru.sicamp.sicamphelper.model.response + +interface Response diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/model/response/ResponseWrapper.kt b/src/main/kotlin/ru/sicamp/sicamphelper/model/response/ResponseWrapper.kt new file mode 100644 index 0000000..5503434 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/model/response/ResponseWrapper.kt @@ -0,0 +1,23 @@ +package ru.sicamp.sicamphelper.model.response + +import org.telegram.telegrambots.meta.api.methods.send.SendMessage +import ru.sicamp.sicamphelper.model.metadata.MetaData +import ru.sicamp.sicamphelper.model.metadata.RequestSource +import ru.sicamp.sicamphelper.db.entity.User + +data class ResponseWrapper( + val issuer: User, + val source: RequestSource, + val metaData: MetaData?, + val response: RES? +) { + fun getTelegramMessage(): SendMessage? { + return if (response is TelegramResponse && source == RequestSource.TELEGRAM) { + issuer.tgId?.let { + response.buildMessage().chatId(it).build() + } ?: error("Not found chatId for user with id=${issuer.id.value}") + } else { + null + } + } +} diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/model/response/TelegramResponse.kt b/src/main/kotlin/ru/sicamp/sicamphelper/model/response/TelegramResponse.kt new file mode 100644 index 0000000..d7e34f0 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/model/response/TelegramResponse.kt @@ -0,0 +1,30 @@ +package ru.sicamp.sicamphelper.model.response + +import org.telegram.telegrambots.meta.api.methods.send.SendMessage +import org.telegram.telegrambots.meta.api.objects.MessageEntity +import org.telegram.telegrambots.meta.api.objects.replykeyboard.ReplyKeyboard +import ru.sicamp.sicamphelper.util.ifNotEmpty +import ru.sicamp.sicamphelper.util.ifNotNull + +abstract class TelegramResponse : Response { + protected abstract val text: String + protected open val parseMode: String? = null + protected open val disableWebPagePreview: Boolean? = null + protected open val disableNotification: Boolean? = null + protected open val replyToMessageId: Int? = null + protected open val replyMarkup: ReplyKeyboard? = null + protected open val entities: List = emptyList() + protected open val allowSendingWithoutReply: Boolean? = null + protected open val protectContent: Boolean = true + + fun buildMessage() = SendMessage.builder() + .text(text) + .ifNotNull(parseMode) { parseMode(parseMode) } + .ifNotNull(disableWebPagePreview) { disableWebPagePreview(disableWebPagePreview) } + .ifNotNull(disableNotification) { disableNotification(disableNotification) } + .ifNotNull(replyToMessageId) { replyToMessageId(replyToMessageId) } + .ifNotNull(replyMarkup) { replyMarkup(replyMarkup) } + .ifNotEmpty(entities) { entities(entities) } + .ifNotNull(allowSendingWithoutReply) { allowSendingWithoutReply(allowSendingWithoutReply) } + .protectContent(protectContent)!! +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/model/response/info/InfoResponse.kt b/src/main/kotlin/ru/sicamp/sicamphelper/model/response/info/InfoResponse.kt new file mode 100644 index 0000000..13c40f2 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/model/response/info/InfoResponse.kt @@ -0,0 +1,13 @@ +package ru.sicamp.sicamphelper.model.response.info + +import com.fasterxml.jackson.annotation.JsonIgnore +import ru.sicamp.sicamphelper.db.entity.User +import ru.sicamp.sicamphelper.model.response.TelegramResponse + +data class InfoResponse( + val user: User +) : TelegramResponse() { + override val text: String + @JsonIgnore + get() = user.toString() +} diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/model/response/schedule/ScheduleResponse.kt b/src/main/kotlin/ru/sicamp/sicamphelper/model/response/schedule/ScheduleResponse.kt new file mode 100644 index 0000000..4edfd40 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/model/response/schedule/ScheduleResponse.kt @@ -0,0 +1,13 @@ +package ru.sicamp.sicamphelper.model.response.schedule + +import com.fasterxml.jackson.annotation.JsonIgnore +import ru.sicamp.sicamphelper.db.entity.Schedule +import ru.sicamp.sicamphelper.model.response.TelegramResponse + +data class ScheduleResponse( + val schedules: List, +) : TelegramResponse() { + override val text: String + @JsonIgnore + get() = schedules.toString() +} diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/repository/EventRepository.kt b/src/main/kotlin/ru/sicamp/sicamphelper/repository/EventRepository.kt new file mode 100644 index 0000000..691b0bf --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/repository/EventRepository.kt @@ -0,0 +1,88 @@ +package ru.sicamp.sicamphelper.repository + +import mu.KLogging +import org.jetbrains.exposed.dao.with +import org.jetbrains.exposed.sql.batchInsert +import org.jetbrains.exposed.sql.transactions.transaction +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Repository +import ru.sicamp.sicamphelper.db.entity.* +import ru.sicamp.sicamphelper.db.table.Events +import ru.sicamp.sicamphelper.db.table.Groups +import ru.sicamp.sicamphelper.db.table.Rooms +import ru.sicamp.sicamphelper.db.table.Users +import ru.sicamp.sicamphelper.helper.ScheduleHelper +import ru.sicamp.sicamphelper.model.dto.EventDto +import ru.sicamp.sicamphelper.util.InTransaction +import ru.sicamp.sicamphelper.util.checkIds +import java.time.LocalDate + +@Repository +class EventRepository( + @Value("\${exposed.batch-size}") + private val batchSize: Int, + private val scheduleHelper: ScheduleHelper, +) { + suspend fun insertEvent(new: EventDto) = transaction { + val event = insertSingleEvent(new) + scheduleHelper.updateSchedulesByEvent(event) + } + + suspend fun insertEventsBatch(batch: List) = transaction { + batch.chunked(batchSize).flatMap { insertEvents(it) } + } + + @InTransaction + private fun insertEvents(batch: List): List { + val userIds = batch.map { it.responsible } + checkIds(Users, userIds) + val groupIds = batch.mapNotNull { it.group } + checkIds(Groups, groupIds) + val roomIds = batch.mapNotNull { it.defaultRoom } + checkIds(Rooms, roomIds) + val events = Events.batchInsert(batch) { eventDto -> + this[Events.name] = eventDto.name + this[Events.registrationType] = eventDto.registrationType + this[Events.group] = eventDto.group + this[Events.eventDate] = eventDto.eventDate + this[Events.eventTime] = eventDto.eventTime + this[Events.repeat] = eventDto.repeat + this[Events.assigneeCount] = eventDto.assigneeCount + this[Events.status] = eventDto.status + this[Events.description] = eventDto.description + this[Events.responsible] = eventDto.responsible + this[Events.defaultRoom] = eventDto.defaultRoom + } + .map { Event.wrapRow(it) } + .with( + Event::responsible, + Event::group, + Event::defaultRoom + ) + + return events.flatMap { + scheduleHelper.updateSchedulesByEvent(it) + } + } + + @InTransaction + private fun insertSingleEvent(new: EventDto) = Event.new { + name = new.name + registrationType = new.registrationType + group = new.group?.let { + Group.findById(it) ?: error("Not found group with id=$it") + } + eventDate = new.eventDate + eventTime = new.eventTime + repeat = new.repeat + assigneeCount = new.assigneeCount + status = new.status + description = new.description + responsible = User.findById(new.responsible) ?: error("Not found user with id=${new.responsible}") + defaultRoom = new.defaultRoom?.let { + Room.findById(it) ?: error("Not found room with id=${new.defaultRoom}") + } + } + + companion object : KLogging() +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/repository/ScheduleRepository.kt b/src/main/kotlin/ru/sicamp/sicamphelper/repository/ScheduleRepository.kt new file mode 100644 index 0000000..010aeee --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/repository/ScheduleRepository.kt @@ -0,0 +1,164 @@ +package ru.sicamp.sicamphelper.repository + +import org.jetbrains.exposed.dao.with +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.javatime.date +import org.jetbrains.exposed.sql.transactions.transaction +import org.springframework.stereotype.Repository +import ru.sicamp.sicamphelper.db.entity.* +import ru.sicamp.sicamphelper.db.table.* +import ru.sicamp.sicamphelper.model.dto.ScheduleDto +import ru.sicamp.sicamphelper.util.InTransaction +import java.time.LocalDate + +@Repository +class ScheduleRepository { + suspend fun updateScheduleById(new: ScheduleDto) = transaction { + if (new.id == null) { + error("Id is required for update, but was no no id was provided in scheduleDto: $new") + } + val schedule = Schedule.findById(new.id) ?: error("Not found schedule by id=${new.id}") + + schedule.apply { + new.dateTime?.let { dateTime = it } + new.roomId?.let { room = Room.findById(it) } + new.scheduleDescription?.let { description = it } + new.responsibleId?.let { responsible = User.findById(it) ?: error("Not found user with id=$it") } + } + } + + suspend fun scheduleByGroupId(groupId: Long, date: LocalDate) = transaction { + val groupSelector = Op.build { GroupRegistrations.group eq groupId } + + return@transaction schedulesByGroupSelector(groupSelector, date) + } + + suspend fun scheduleByGroupName(name: String, date: LocalDate) = transaction { + val groupId = Group.find { + Groups.name eq name + }.firstOrNull()?.id ?: return@transaction null + + val groupSelector = Op.build { GroupRegistrations.id eq groupId } + + return@transaction schedulesByGroupSelector(groupSelector, date) + } + + suspend fun scheduleByGateLogin(gateLogin: String, date: LocalDate) = transaction { + val user = User.find { + Users.login eq gateLogin + }.firstOrNull() ?: return@transaction null + return@transaction internalScheduleByUser(user, date) + } + + suspend fun scheduleByTgUsername(tgUsername: String, date: LocalDate) = transaction { + val user = User.find { + Users.tgUsername eq tgUsername + }.firstOrNull() ?: return@transaction null + return@transaction internalScheduleByUser(user, date) + } + + suspend fun scheduleByUserId(userId: Long, date: LocalDate) = transaction { + val user = User.findById(userId) ?: return@transaction null + return@transaction internalScheduleByUser(user, date) + } + + suspend fun scheduleByUser(user: User, date: LocalDate) = transaction { + return@transaction internalScheduleByUser(user, date) + } + + @InTransaction + private fun internalScheduleByUser(user: User, date: LocalDate): List { + val userSelector = Op.build { UserRegistrations.id eq user.id } + val userSchedules = schedulesByUserSelector(userSelector, date) + + val groupSelector = Op.build { GroupRegistrations.group eq user.group.id } + val groupSchedules = schedulesByGroupSelector(groupSelector, date) + + return userSchedules.plus(groupSchedules) + } + + @InTransaction + private fun schedulesByUserSelector( + userSelector: Op, + date: LocalDate + ) = UserRegistrations + .join( + Schedules, + joinType = JoinType.INNER, + additionalConstraint = { + UserRegistrations.schedule eq Schedules.id + } + ) + .joinEvents() + .joinRooms() + .slice(selectedColumns) + .select { + userSelector and + (Schedules.start.date().date() eq date) + } + .map { + Schedule.wrapRow(it) + } + .with( + Schedule::room, + Schedule::event, + Schedule::responsible + ) + + /*.map { + Room.wrapRow(it) + Event.wrapRow(it) + Schedule.wrapRow(it) + }*/ + + private fun schedulesByGroupSelector( + groupSelector: Op, + date: LocalDate + ) = GroupRegistrations + .join( + Schedules, + joinType = JoinType.INNER, + additionalConstraint = { + GroupRegistrations.schedule eq Schedules.id + } + ) + .joinEvents() + .joinRooms() + .select { + groupSelector and (Schedules.start.date().date() eq date) + } + .map { + Event.wrapRow(it) + Room.wrapRow(it) + Schedule.wrapRow(it) + } + + private fun ColumnSet.joinEvents() = join( + Events, + joinType = JoinType.INNER, + additionalConstraint = { + Schedules.event eq Events.id + } + ) + + private fun ColumnSet.joinRooms() = join( + Rooms, + joinType = JoinType.INNER, + additionalConstraint = { + Schedules.room eq Rooms.id + } + ) + + private val selectedColumns = listOf( + Schedules.id, + Schedules.start, + Schedules.room, + Events.id, + Events.description, + Events.responsible, + Rooms.id, + Rooms.type, + Rooms.name, + Rooms.description + ) +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/security/WebSecurityConfig.kt b/src/main/kotlin/ru/sicamp/sicamphelper/security/WebSecurityConfig.kt new file mode 100644 index 0000000..2a117a8 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/security/WebSecurityConfig.kt @@ -0,0 +1,29 @@ +package ru.sicamp.sicamphelper.security + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.security.web.SecurityFilterChain + +@Configuration +@EnableWebSecurity +class WebSecurityConfig { + @Bean + fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder() + + @Bean + fun securityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain = httpSecurity + .authorizeHttpRequests { requests -> + requests.requestMatchers("/**").permitAll() + } + .formLogin { configurer -> + configurer.loginPage("/login") + } + .logout { + it.permitAll() + } + .build() +} diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/service/ConfigService.kt b/src/main/kotlin/ru/sicamp/sicamphelper/service/ConfigService.kt new file mode 100644 index 0000000..0fd1cd7 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/service/ConfigService.kt @@ -0,0 +1,16 @@ +package ru.sicamp.sicamphelper.service + +import com.fasterxml.jackson.databind.ObjectMapper +import org.jetbrains.exposed.sql.transactions.transaction +import org.springframework.stereotype.Service +import ru.sicamp.sicamphelper.db.entity.Config +import java.time.LocalDate + +@Service +class ConfigService( + private val objectMapper: ObjectMapper +) { + fun getDate(name: String): LocalDate? = transaction { + return@transaction objectMapper.readValue(Config.findById(name)?.value, LocalDate::class.java) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/service/EventService.kt b/src/main/kotlin/ru/sicamp/sicamphelper/service/EventService.kt new file mode 100644 index 0000000..743ffdc --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/service/EventService.kt @@ -0,0 +1,7 @@ +package ru.sicamp.sicamphelper.service + +import org.springframework.stereotype.Service + +@Service +class EventService { +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/service/GroupService.kt b/src/main/kotlin/ru/sicamp/sicamphelper/service/GroupService.kt new file mode 100644 index 0000000..5b34ac6 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/service/GroupService.kt @@ -0,0 +1,15 @@ +package ru.sicamp.sicamphelper.service + +import org.jetbrains.exposed.sql.transactions.transaction +import org.springframework.stereotype.Service +import ru.sicamp.sicamphelper.db.entity.Group +import ru.sicamp.sicamphelper.db.table.Groups + +@Service +class GroupService( + +) { + fun findByName(groupName: String) = transaction { + Group.find { Groups.name eq groupName }.firstOrNull() + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/service/InfoService.kt b/src/main/kotlin/ru/sicamp/sicamphelper/service/InfoService.kt new file mode 100644 index 0000000..68999f3 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/service/InfoService.kt @@ -0,0 +1,34 @@ +package ru.sicamp.sicamphelper.service + +import org.jetbrains.exposed.sql.transactions.transaction +import org.springframework.stereotype.Service +import ru.sicamp.sicamphelper.db.entity.User +import ru.sicamp.sicamphelper.db.table.Users +import ru.sicamp.sicamphelper.model.data.UserInput +import ru.sicamp.sicamphelper.model.request.RequestWrapper +import ru.sicamp.sicamphelper.model.request.info.InfoRequest +import ru.sicamp.sicamphelper.model.response.ResponseWrapper +import ru.sicamp.sicamphelper.model.response.info.InfoResponse + +@Service +class InfoService { + suspend fun getInfo(requestWrapper: RequestWrapper): ResponseWrapper { + val request = requestWrapper.body + val userInput = request.userInput + val type = userInput?.type + val user = transaction { + when (type) { + UserInput.Type.USER_ID -> User.findById(userInput.userId ?: return@transaction null) + UserInput.Type.TG_USERNAME -> User.find { Users.tgUsername eq userInput.tgUsername }.firstOrNull() + UserInput.Type.GATE_LOGIN -> userInput.gateLogin?.let { User.find { Users.login eq it }.firstOrNull() } + null -> requestWrapper.issuer + } + } + + return if (user != null) { + requestWrapper.success(InfoResponse(user)) + } else { + requestWrapper.fail(statusCode = 404, message = "Not found user") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/service/ScheduleService.kt b/src/main/kotlin/ru/sicamp/sicamphelper/service/ScheduleService.kt new file mode 100644 index 0000000..1927a5d --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/service/ScheduleService.kt @@ -0,0 +1,69 @@ +package ru.sicamp.sicamphelper.service + +import mu.KLogging +import org.springframework.stereotype.Service +import ru.sicamp.sicamphelper.model.data.GroupInput +import ru.sicamp.sicamphelper.model.data.UserInput +import ru.sicamp.sicamphelper.model.request.RequestWrapper +import ru.sicamp.sicamphelper.model.request.schedule.ScheduleGroupRequest +import ru.sicamp.sicamphelper.model.request.schedule.ScheduleRequest +import ru.sicamp.sicamphelper.model.response.ResponseWrapper +import ru.sicamp.sicamphelper.model.response.schedule.ScheduleResponse +import ru.sicamp.sicamphelper.repository.ScheduleRepository +import java.time.LocalDate + +@Service +class ScheduleService( + private val scheduleRepository: ScheduleRepository, +) { + suspend fun getUserSchedule(requestWrapper: RequestWrapper): ResponseWrapper { + val request = requestWrapper.body + val userInput = request.userInput + val date = request.date ?: LocalDate.now() + val schedules = try { + when (userInput?.type) { + UserInput.Type.TG_USERNAME -> userInput.tgUsername?.let { + scheduleRepository.scheduleByTgUsername(it, date) + } + UserInput.Type.GATE_LOGIN -> userInput.gateLogin?.let { + scheduleRepository.scheduleByGateLogin(it, date) + } + UserInput.Type.USER_ID -> userInput.userId?.let { + scheduleRepository.scheduleByUserId(it, date) + } + null -> scheduleRepository.scheduleByUser(requestWrapper.issuer, date) + } ?: return requestWrapper.fail(404, message = "Not found schedules. Bad input parameters") + } catch (e: Exception) { + logger.error { e } + return requestWrapper.fail(500, message = "Error while trying to fetch schedules for user $userInput") + } + + return requestWrapper.success(ScheduleResponse(schedules)) + } + + suspend fun getSchedulesByGroup(requestWrapper: RequestWrapper): ResponseWrapper { + val request = requestWrapper.body + val date = request.date ?: LocalDate.now() + val type = request.group?.type + val schedules = try { + when (type) { + GroupInput.Type.ID -> request.group.id?.let { + scheduleRepository.scheduleByGroupId(it, date) + } + GroupInput.Type.NAME -> request.group.name?.let { + scheduleRepository.scheduleByGroupName(it, date) + } + null -> requestWrapper.issuer.group.id.value.let { + scheduleRepository.scheduleByGroupId(it, date) + } + } ?: return requestWrapper.fail(404, message = "Not found schedules. Bad input parameters") + } catch (e: Exception) { + logger.error { e } + return requestWrapper.fail(500, "Error while trying to fetch schedules for group ${request.group}") + } + + return requestWrapper.success(ScheduleResponse(schedules)) + } + + companion object : KLogging() +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/service/UserService.kt b/src/main/kotlin/ru/sicamp/sicamphelper/service/UserService.kt new file mode 100644 index 0000000..c79ff57 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/service/UserService.kt @@ -0,0 +1,53 @@ +package ru.sicamp.sicamphelper.service + +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.transactions.transaction +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Service +import ru.sicamp.sicamphelper.api.controller.UsersController +import ru.sicamp.sicamphelper.db.entity.User +import ru.sicamp.sicamphelper.db.table.Users +import ru.sicamp.sicamphelper.model.dto.UserDto +import java.time.Duration +import java.time.LocalDateTime + +@Service +class UserService( + private val passwordEncoder: PasswordEncoder +) : UserDetailsService { + private val userExpirationTime = Duration.ofDays(60) + private val credentialsExpirationTime = Duration.ofDays(30) + + fun findUserByTgUser(user: org.telegram.telegrambots.meta.api.objects.User): User? = transaction { + User.find { + (Users.tgId eq user.id) and + (Users.tgUsername eq user.userName) + }.firstOrNull() + } + + override fun loadUserByUsername(username: String?): User = transaction { + username?.let { + User.find { Users.login eq it }.firstOrNull() + } ?: error("Not found user with username $username") + } + + fun createUser(userDto: UserDto): User { + val now = LocalDateTime.now() + UsersController.logger.info("Creating new user at $now") + return User.new { + login = userDto.login + _password = passwordEncoder.encode(userDto.password) + role = userDto.role + name = userDto.name + sex = userDto.sex + age = userDto.age + tgId = userDto.tgId + tgUsername = userDto.tgUsername + locked = false + enabled = userDto.enabled + accountExpiration = now + userExpirationTime + credentialsExpiration = now + credentialsExpirationTime + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/service/Utils.kt b/src/main/kotlin/ru/sicamp/sicamphelper/service/Utils.kt new file mode 100644 index 0000000..88261b8 --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/service/Utils.kt @@ -0,0 +1,4 @@ +package ru.sicamp.sicamphelper.service + +import mu.KLogger + diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/util/ExposedUtils.kt b/src/main/kotlin/ru/sicamp/sicamphelper/util/ExposedUtils.kt new file mode 100644 index 0000000..0fcb3ba --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/util/ExposedUtils.kt @@ -0,0 +1,12 @@ +package ru.sicamp.sicamphelper.util + +import org.jetbrains.exposed.dao.id.LongIdTable +import org.jetbrains.exposed.sql.select + +@InTransaction +fun checkIds(table: LongIdTable, ids: List) { + val entityCount = table.select { table.id inList ids }.count() + if (entityCount != ids.size.toLong()) { + error("Not found some users from id list. Supposed to find ${ids.size}, found: $entityCount. Ids: $ids") + } +} diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/util/Extractor.kt b/src/main/kotlin/ru/sicamp/sicamphelper/util/Extractor.kt new file mode 100644 index 0000000..9b23cec --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/util/Extractor.kt @@ -0,0 +1,7 @@ +package ru.sicamp.sicamphelper.util + +import ru.sicamp.sicamphelper.model.request.Request + +interface Extractor { + fun extract(arguments: Array?): E +} \ No newline at end of file diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/util/InTransaction.kt b/src/main/kotlin/ru/sicamp/sicamphelper/util/InTransaction.kt new file mode 100644 index 0000000..a52225a --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/util/InTransaction.kt @@ -0,0 +1,6 @@ +package ru.sicamp.sicamphelper.util + +/**This function should only be called inside exposed transaction*/ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.SOURCE) +annotation class InTransaction() diff --git a/src/main/kotlin/ru/sicamp/sicamphelper/util/Utils.kt b/src/main/kotlin/ru/sicamp/sicamphelper/util/Utils.kt new file mode 100644 index 0000000..4f5a3fa --- /dev/null +++ b/src/main/kotlin/ru/sicamp/sicamphelper/util/Utils.kt @@ -0,0 +1,5 @@ +package ru.sicamp.sicamphelper.util + +fun R.ifNotNull(obj: T?, block: R.(T) -> R): R = if (obj != null) block(obj) else this + +fun R.ifNotEmpty(list: List, block: R.(List) -> R): R = if (list.isNotEmpty()) block(list) else this \ No newline at end of file diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000..abf2604 --- /dev/null +++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,18 @@ +{ + "properties": [ + { + "name": "exposed.batch-size", + "type": "java.lang.String", + "description": "Description for exposed.batch-size." + }, + { + "name": "bot.username", + "type": "java.lang.String", + "description": "Description for bot.username." + }, + { + "name": "bot.token", + "type": "java.lang.String", + "description": "Description for bot.token." + } + ] } \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..170de12 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,19 @@ +# Настройка для телеграм апи +bot: + username: SicampHelperBot + token: [your bot token here] + +server: + port: 8081 + +# Настройка для СУБД +spring: + datasource: + url: jdbc:postgresql://localhost:5432/sicamp_helper + username: postgres + password: postgres + thymeleaf: + enabled: false + +exposed: + batch-size: 1000 diff --git a/src/test/kotlin/ru/sicamp/sicamphelper/SicampHelperApplicationTests.kt b/src/test/kotlin/ru/sicamp/sicamphelper/SicampHelperApplicationTests.kt new file mode 100644 index 0000000..d4a47ff --- /dev/null +++ b/src/test/kotlin/ru/sicamp/sicamphelper/SicampHelperApplicationTests.kt @@ -0,0 +1,13 @@ +package ru.sicamp.sicamphelper + +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class SicampHelperApplicationTests { + + @Test + fun contextLoads() { + } + +}