Initial commit

This commit is contained in:
slon565 2023-07-22 08:09:39 +04:00
commit 0686dd7c93
83 changed files with 2178 additions and 0 deletions

37
.gitignore vendored Normal file
View file

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

80
build.gradle.kts Normal file
View file

@ -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<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "17"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

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

240
gradlew vendored Normal file
View file

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

91
gradlew.bat vendored Normal file
View file

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

1
settings.gradle.kts Normal file
View file

@ -0,0 +1 @@
rootProject.name = "sicamp-helper"

View file

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

View file

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

View file

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

View file

@ -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<String>) {
runApplication<SicampHelperApplication>(*args)
}

View file

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

View file

@ -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<Command<*, *>>,
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()
}*/

View file

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

View file

@ -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<REQ : Request, RES : TelegramResponse>(
private val command: Commands
) : BotCommand(command.name, command.description), Extractor<REQ> {
protected abstract val userService: UserService
protected abstract suspend fun executeRequest(request: RequestWrapper<REQ>): ResponseWrapper<RES>
override fun execute(absSender: AbsSender?, user: User?, chat: Chat?, arguments: Array<out String>?) {
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()
}

View file

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

View file

@ -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<REQ : Request, RES : TelegramResponse>(command: Commands) : Command<REQ, RES>(command)

View file

@ -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<REQ : Request, RES : TelegramResponse>(command: Commands) : Command<REQ, RES>(command)

View file

@ -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<out String>.getValueOrNull(index: Int) = getOrNull(index)?.let { if (it == EMPTY_TOKEN) null else it }

View file

@ -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<InfoRequest, InfoResponse>(Commands.INFO) {
override suspend fun executeRequest(request: RequestWrapper<InfoRequest>) = infoService.getInfo(request)
override fun extract(
arguments: Array<out String>?
) = InfoRequest(
userInput = arguments?.getValueOrNull(0)?.let {
UserInput(it)
}
)
}

View file

@ -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<ScheduleRequest, ScheduleResponse>(Commands.SCHEDULE) {
override suspend fun executeRequest(
request: RequestWrapper<ScheduleRequest>
): ResponseWrapper<ScheduleResponse> = scheduleService.getUserSchedule(request)
override fun extract(
arguments: Array<out String>?
) = ScheduleRequest(
date = arguments?.getValueOrNull(0).toLocalDate(),
userInput = arguments?.getValueOrNull(1)?.let { UserInput(it) }
)
companion object : KLogging()
}

View file

@ -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<ScheduleGroupRequest, ScheduleResponse>(Commands.SCHEDULE_GROUP) {
override suspend fun executeRequest(request: RequestWrapper<ScheduleGroupRequest>): ResponseWrapper<ScheduleResponse> {
return scheduleService.getSchedulesByGroup(request)
}
override fun extract(arguments: Array<out String>?): ScheduleGroupRequest =
ScheduleGroupRequest(
date = arguments?.getValueOrNull(0).toLocalDate(),
group = arguments?.getValueOrNull(1)?.let { GroupInput(it) }
)
}

View file

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

View file

@ -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<String>): Entity<String>(id) {
companion object : EntityClass<String, Config>(Configs)
val value by Configs.value
}

View file

@ -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<Long>): LongEntity(id) {
companion object : LongEntityClass<Event>(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
}

View file

@ -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<Long>) : LongEntity(id) {
companion object : LongEntityClass<Group>(Groups)
var name by Groups.name
var tgLink by Groups.tgLink
}

View file

@ -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<Long>): LongEntity(id) {
companion object : LongEntityClass<GroupRegistration>(GroupRegistrations)
var schedule by Schedule referencedOn GroupRegistrations.schedule
var group by Group referencedOn GroupRegistrations.group
}

View file

@ -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<Long>) : LongEntity(id) {
companion object : LongEntityClass<Room>(Rooms)
var name by Rooms.name
var type by Rooms.type
var description by Rooms.description
}

View file

@ -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<Long>) : LongEntity(id) {
companion object : LongEntityClass<Schedule>(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
}

View file

@ -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<Long>) : LongEntity(id), UserDetails {
companion object : LongEntityClass<User>(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<out GrantedAuthority> = 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}"
}
}

View file

@ -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<Long>): LongEntity(id) {
companion object : LongEntityClass<UserRegistration>(UserRegistrations)
var schedule by Schedule referencedOn UserRegistrations.schedule
var user by User referencedOn UserRegistrations.user
}

View file

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

View file

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

View file

@ -0,0 +1,10 @@
package ru.sicamp.sicamphelper.db.table
import org.jetbrains.exposed.dao.id.IdTable
object Configs : IdTable<String>("configs") {
override val id = varchar("id", 255).entityId()
val value = varchar("value", 255)
override val primaryKey: PrimaryKey = PrimaryKey(id)
}

View file

@ -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>("type", 255)
val registrationType = enumerationByName<RegistrationType>("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>("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 }
}

View file

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

View file

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

View file

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

View file

@ -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>("type", 255)
val description = varchar("description", 255).nullable()
enum class Type { DORMITORY, AUDIENCE, CANTEEN, COMMON_SPACE }
}

View file

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

View file

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

View file

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

View file

@ -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>("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>("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<Role> = values().filter { it.level <= role.level }
fun lowerRoles(role: Role): List<Role> = values().filter { it.level >= role.level }
}
}
enum class Sex { MALE, FEMALE, OTHER }
}

View file

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

View file

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

View file

@ -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<T : Any>(
change: String,
private val deserializationStrategy: DeserializationStrategy<T>
) {
val column: Column<T> = 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<T> {
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<T>? ?: 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()
}*/

View file

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

View file

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

View file

@ -0,0 +1,3 @@
package ru.sicamp.sicamphelper.model.data
fun String.tail() = subSequence(1..lastIndex)

View file

@ -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?,
)

View file

@ -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?,
)

View file

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

View file

@ -0,0 +1,5 @@
package ru.sicamp.sicamphelper.model.metadata
data class Cursor(
val data: String
)

View file

@ -0,0 +1,6 @@
package ru.sicamp.sicamphelper.model.metadata
data class Error(
val code: Int,
val message: String
)

View file

@ -0,0 +1,6 @@
package ru.sicamp.sicamphelper.model.metadata
data class MetaData(
val error: Error?,
val cursor: Cursor?
)

View file

@ -0,0 +1,3 @@
package ru.sicamp.sicamphelper.model.metadata
enum class RequestSource { TELEGRAM, WEB }

View file

@ -0,0 +1,3 @@
package ru.sicamp.sicamphelper.model.request
interface Request

View file

@ -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<REQ : Request>(
val issuer: User,
val source: RequestSource,
val cursor: Cursor? = null,
val body: REQ
) {
fun <RES : Response> success(response: RES, newCursor: Cursor? = null): ResponseWrapper<RES> =
ResponseWrapper(
issuer = issuer,
source = source,
metaData = MetaData(
cursor = newCursor,
error = null
),
response = response
)
fun <RES : Response> fail(statusCode: Int = 500, message: String = "Unknown Error"): ResponseWrapper<RES> =
ResponseWrapper(
issuer = issuer,
source = source,
metaData = MetaData(
error = Error(statusCode, message),
cursor = cursor,
),
response = null
)
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,3 @@
package ru.sicamp.sicamphelper.model.response
interface Response

View file

@ -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<RES: Response>(
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
}
}
}

View file

@ -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<MessageEntity> = 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)!!
}

View file

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

View file

@ -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<Schedule>,
) : TelegramResponse() {
override val text: String
@JsonIgnore
get() = schedules.toString()
}

View file

@ -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<EventDto>) = transaction {
batch.chunked(batchSize).flatMap { insertEvents(it) }
}
@InTransaction
private fun insertEvents(batch: List<EventDto>): List<Schedule> {
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()
}

View file

@ -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<Schedule> {
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<Boolean>,
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<Boolean>,
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
)
}

View file

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

View file

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

View file

@ -0,0 +1,7 @@
package ru.sicamp.sicamphelper.service
import org.springframework.stereotype.Service
@Service
class EventService {
}

View file

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

View file

@ -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<InfoRequest>): ResponseWrapper<InfoResponse> {
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")
}
}
}

View file

@ -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<ScheduleRequest>): ResponseWrapper<ScheduleResponse> {
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<ScheduleGroupRequest>): ResponseWrapper<ScheduleResponse> {
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()
}

View file

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

View file

@ -0,0 +1,4 @@
package ru.sicamp.sicamphelper.service
import mu.KLogger

View file

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

View file

@ -0,0 +1,7 @@
package ru.sicamp.sicamphelper.util
import ru.sicamp.sicamphelper.model.request.Request
interface Extractor<E : Request> {
fun extract(arguments: Array<out String>?): E
}

View file

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

View file

@ -0,0 +1,5 @@
package ru.sicamp.sicamphelper.util
fun <R, T> R.ifNotNull(obj: T?, block: R.(T) -> R): R = if (obj != null) block(obj) else this
fun <R, T> R.ifNotEmpty(list: List<T>, block: R.(List<T>) -> R): R = if (list.isNotEmpty()) block(list) else this

View file

@ -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."
}
] }

View file

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

View file

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