diff --git a/build.gradle b/build.gradle index d6a397d..c5e9b34 100644 --- a/build.gradle +++ b/build.gradle @@ -83,6 +83,7 @@ kotlin { implementation "io.ktor:ktor-server-netty:$ktor_version" implementation "io.ktor:ktor-auth:$ktor_version" implementation "io.ktor:ktor-server-sessions:$ktor_version" + implementation "io.ktor:ktor-websockets:$ktor_version" implementation "io.ktor:ktor-jackson:$ktor_version" implementation 'org.jetbrains:kotlin-css-jvm:1.0.0-pre.70-kotlin-1.3.21' diff --git a/src/commonMain/kotlin/de/kif/common/ConstraintChecking.kt b/src/commonMain/kotlin/de/kif/common/ConstraintChecking.kt index 8f3071c..78e2ee8 100644 --- a/src/commonMain/kotlin/de/kif/common/ConstraintChecking.kt +++ b/src/commonMain/kotlin/de/kif/common/ConstraintChecking.kt @@ -48,8 +48,8 @@ fun checkConstraints( if ( schedule != s && leader in s.workGroup.leader && - start < s.getAbsoluteEndTime() && - s.getAbsoluteStartTime() < end + start <= s.getAbsoluteEndTime() && + s.getAbsoluteStartTime() <= end ) { errors += ConstraintError("Work group with leader $leader cannot be at same time with ${s.workGroup.name}!") } diff --git a/src/commonMain/kotlin/de/kif/common/Message.kt b/src/commonMain/kotlin/de/kif/common/Message.kt index e48a65b..e239280 100644 --- a/src/commonMain/kotlin/de/kif/common/Message.kt +++ b/src/commonMain/kotlin/de/kif/common/Message.kt @@ -1,38 +1,35 @@ package de.kif.common -import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule -@Serializable -data class MessageBox( - val timestamp: Long, - val valid: Boolean, - val messages: List -) - @Serializable data class Message( val type: MessageType, val repository: RepositoryType, val id: Long -) +) { + + fun stringify(): String { + return json.stringify(serializer(), this) + } + + companion object { + private val jsonContext = SerializersModule {} + + val json = Json(context = jsonContext) + + fun parse(data: String): Message { + return json.parse(serializer(), data) + } + } +} enum class MessageType { CREATE, UPDATE, DELETE } enum class RepositoryType { - ROOM, SCHEDULE, TRACK, USER, WORK_GROUP, POST, ANNOUNCEMENT -} - -object Serialization { - private val jsonContext = SerializersModule {} - - val json = Json(context = jsonContext) - - fun stringify(serializer: SerializationStrategy, obj: T) = json.stringify(serializer, obj) - fun parse(serializer: DeserializationStrategy, str: String) = json.parse(serializer, str) + ROOM, SCHEDULE, TRACK, USER, WORK_GROUP, POST } diff --git a/src/commonMain/kotlin/de/kif/common/Search.kt b/src/commonMain/kotlin/de/kif/common/Search.kt index 184518d..fadef97 100644 --- a/src/commonMain/kotlin/de/kif/common/Search.kt +++ b/src/commonMain/kotlin/de/kif/common/Search.kt @@ -10,12 +10,12 @@ data class SearchElement( ) { fun stringify(): String { - return Serialization.stringify(serializer(), this) + return Message.json.stringify(serializer(), this) } companion object { fun parse(data: String): SearchElement { - return Serialization.parse(serializer(), data) + return Message.json.parse(serializer(), data) } } } diff --git a/src/jsMain/kotlin/de/kif/frontend/WebSocketClient.kt b/src/jsMain/kotlin/de/kif/frontend/WebSocketClient.kt index 1a72f25..62464af 100644 --- a/src/jsMain/kotlin/de/kif/frontend/WebSocketClient.kt +++ b/src/jsMain/kotlin/de/kif/frontend/WebSocketClient.kt @@ -1,92 +1,68 @@ package de.kif.frontend -import de.kif.common.MessageBox +import de.kif.common.Message import de.kif.common.MessageType import de.kif.common.RepositoryType import de.kif.frontend.repository.* -import de.westermann.kwebview.clearInterval -import de.westermann.kwebview.createHtmlView -import de.westermann.kwebview.interval -import kotlinx.serialization.DynamicObjectParser -import org.w3c.dom.get -import org.w3c.xhr.XMLHttpRequest -import kotlin.browser.document +import de.westermann.kwebview.async +import org.w3c.dom.MessageEvent +import org.w3c.dom.WebSocket +import org.w3c.dom.events.Event import kotlin.browser.window -class WebSocketClient { +class WebSocketClient() { private val prefix = js("prefix") - private val url = "$prefix/api/updates" - private val body = document.body ?: createHtmlView() - private val parser = DynamicObjectParser() - private var timestamp = body.dataset["timestamp"]?.toLongOrNull() ?: 0L - private var intervalId: Int? = null + private val useSsl = "https" in window.location.protocol + private val wsProtocol = if (useSsl) "wss" else "ws" + private val url = "$wsProtocol://${window.location.host}$prefix/websocket" - private fun reload() { - val id = intervalId ?: return - clearInterval(id) - intervalId = null + private lateinit var ws: WebSocket + private var reconnect = false - window.location.reload() + @Suppress("UNUSED_PARAMETER") + private fun onOpen(event: Event) { + console.log("Connected!") + if (reconnect) { + window.location.reload() + } } - private fun onMessage(messageBox: MessageBox) { - body.classList.remove("offline") + private fun onMessage(messageEvent: MessageEvent) { + val message = Message.parse(messageEvent.data?.toString() ?: "") - if (messageBox.valid) { - timestamp = messageBox.timestamp - - for (message in messageBox.messages) { - for (handler in messageHandlers) { - if (handler.repository == message.repository) { - when (message.type) { - MessageType.CREATE -> handler.onCreate(message.id) - MessageType.UPDATE -> handler.onUpdate(message.id) - MessageType.DELETE -> handler.onDelete(message.id) - } - } + for (handler in messageHandlers) { + if (handler.repository == message.repository) { + when (message.type) { + MessageType.CREATE -> handler.onCreate(message.id) + MessageType.UPDATE -> handler.onUpdate(message.id) + MessageType.DELETE -> handler.onDelete(message.id) } } - } else { - reload() } } - private fun onError(code: Int) { - if (!body.classList.contains("offline")) { - console.log("Offline reason: $code") + @Suppress("UNUSED_PARAMETER") + private fun onClose(event: Event) { + console.log("Disconnected!") + reconnect = true + async(1000) { + connect() } - body.classList.add("offline") } - private fun request() { - val xmlHttpRequest = XMLHttpRequest() + @Suppress("UNUSED_PARAMETER") + private fun onError(event: Event) { + console.log("An error occurred!") + } - xmlHttpRequest.onreadystatechange = { - try { - if (xmlHttpRequest.readyState == 4.toShort()) { - if (xmlHttpRequest.status == 200.toShort() || xmlHttpRequest.status == 304.toShort()) { - val json = JSON.parse(xmlHttpRequest.responseText) + private fun connect() { + ws = WebSocket(url) - if (json.OK) { - val message = parser.parse(json.data, MessageBox.serializer()) - onMessage(message) - } else { - onError(-1) - } - - } else { - onError(xmlHttpRequest.status.toInt()) - } - } - } catch (e: Exception) { - console.error(e) - onError(-2) - } - } - xmlHttpRequest.open("GET", "$url?timestamp=$timestamp", true) - xmlHttpRequest.overrideMimeType("application/json") - xmlHttpRequest.send() + ws.onopen = this::onOpen + ws.onmessage = this::onMessage + ws.onclose = this::onClose + ws.onerror = this::onError } private val messageHandlers: List = listOf( @@ -95,14 +71,11 @@ class WebSocketClient { TrackRepository.handler, UserRepository.handler, WorkGroupRepository.handler, - PostRepository.handler, - AnnouncementRepository.handler + PostRepository.handler ) init { - intervalId = interval(500) { - request() - } + connect() } } diff --git a/src/jsMain/kotlin/de/kif/frontend/main.kt b/src/jsMain/kotlin/de/kif/frontend/main.kt index 7f10c8c..ed186cf 100644 --- a/src/jsMain/kotlin/de/kif/frontend/main.kt +++ b/src/jsMain/kotlin/de/kif/frontend/main.kt @@ -2,7 +2,6 @@ package de.kif.frontend import de.kif.frontend.views.board.initBoard import de.kif.frontend.views.calendar.initCalendar -import de.kif.frontend.views.initAnnouncement import de.kif.frontend.views.table.initTableLayout import de.kif.frontend.views.initWorkGroupConstraints import de.kif.frontend.views.overview.initOverviewMain @@ -35,7 +34,4 @@ fun main() = init { if (document.getElementsByClassName("board").length > 0) { initBoard() } - if (document.getElementsByClassName("announcement").length > 0) { - initAnnouncement() - } } diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/Ajax.kt b/src/jsMain/kotlin/de/kif/frontend/repository/Ajax.kt index 1f5446b..a08991d 100644 --- a/src/jsMain/kotlin/de/kif/frontend/repository/Ajax.kt +++ b/src/jsMain/kotlin/de/kif/frontend/repository/Ajax.kt @@ -8,15 +8,10 @@ import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine import kotlin.js.Promise -interface JsonResponse { - val OK: Boolean - val data: dynamic -} - suspend fun repositoryGet( url: String ): dynamic { - val promise = Promise { resolve, reject -> + val promise = Promise { resolve, reject -> val xhttp = XMLHttpRequest() xhttp.onreadystatechange = { @@ -52,7 +47,7 @@ suspend fun repositoryPost( url: String, data: String? = null ): dynamic { - val promise = Promise { resolve, reject -> + val promise = Promise { resolve, reject -> val xhttp = XMLHttpRequest() xhttp.onreadystatechange = { diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/AnnouncementRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/AnnouncementRepository.kt deleted file mode 100644 index 59a1965..0000000 --- a/src/jsMain/kotlin/de/kif/frontend/repository/AnnouncementRepository.kt +++ /dev/null @@ -1,40 +0,0 @@ -package de.kif.frontend.repository - -import de.kif.common.Message -import de.kif.common.Repository -import de.kif.common.RepositoryType -import de.kif.common.Serialization -import de.kif.common.model.Room -import de.kif.frontend.MessageHandler -import de.westermann.kobserve.event.EventHandler -import kotlinx.serialization.DynamicObjectParser -import kotlinx.serialization.list -import kotlinx.serialization.serializer - -object AnnouncementRepository { - - private val prefix = js("prefix") - - val onUpdate = EventHandler() - - private val parser = DynamicObjectParser() - - suspend fun getAnnouncement(): String { - val json = repositoryGet("$prefix/api/announcement") ?: return "" - return parser.parse(json, String.serializer()) - } - - suspend fun setAnnouncement(value: String){ - return repositoryPost("$prefix/api/announcement", Serialization.stringify(String.serializer(), value)) - ?: throw IllegalStateException("Cannot set announcement!") - } - - val handler = object : MessageHandler(RepositoryType.ROOM) { - - override fun onCreate(id: Long) {} - - override fun onUpdate(id: Long) = onUpdate.emit(Unit) - - override fun onDelete(id: Long) {} - } -} diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/PostRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/PostRepository.kt index 13c459e..aa56570 100644 --- a/src/jsMain/kotlin/de/kif/frontend/repository/PostRepository.kt +++ b/src/jsMain/kotlin/de/kif/frontend/repository/PostRepository.kt @@ -3,7 +3,6 @@ package de.kif.frontend.repository import de.kif.common.Message import de.kif.common.Repository import de.kif.common.RepositoryType -import de.kif.common.Serialization import de.kif.common.model.Post import de.kif.frontend.MessageHandler import de.westermann.kobserve.event.EventHandler @@ -26,13 +25,13 @@ object PostRepository : Repository { } override suspend fun create(model: Post): Long { - return repositoryPost("$prefix/api/posts", Serialization.stringify(Post.serializer(), model)) + return repositoryPost("$prefix/api/posts", Message.json.stringify(Post.serializer(), model)) ?: throw IllegalStateException("Cannot create model!") } override suspend fun update(model: Post) { if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") - repositoryPost("$prefix/api/post/${model.id}", Serialization.stringify(Post.serializer(), model)) + repositoryPost("$prefix/api/post/${model.id}", Message.json.stringify(Post.serializer(), model)) } override suspend fun delete(id: Long) { diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/RoomRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/RoomRepository.kt index 9d933f3..1dc88ee 100644 --- a/src/jsMain/kotlin/de/kif/frontend/repository/RoomRepository.kt +++ b/src/jsMain/kotlin/de/kif/frontend/repository/RoomRepository.kt @@ -3,7 +3,6 @@ package de.kif.frontend.repository import de.kif.common.Message import de.kif.common.Repository import de.kif.common.RepositoryType -import de.kif.common.Serialization import de.kif.common.model.Room import de.kif.frontend.MessageHandler import de.westermann.kobserve.event.EventHandler @@ -26,13 +25,13 @@ object RoomRepository : Repository { } override suspend fun create(model: Room): Long { - return repositoryPost("$prefix/api/rooms", Serialization.stringify(Room.serializer(), model)) + return repositoryPost("$prefix/api/rooms", Message.json.stringify(Room.serializer(), model)) ?: throw IllegalStateException("Cannot create model!") } override suspend fun update(model: Room) { if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") - repositoryPost("$prefix/api/room/${model.id}", Serialization.stringify(Room.serializer(), model)) + repositoryPost("$prefix/api/room/${model.id}", Message.json.stringify(Room.serializer(), model)) } override suspend fun delete(id: Long) { diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/ScheduleRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/ScheduleRepository.kt index 3da61bf..60a2b73 100644 --- a/src/jsMain/kotlin/de/kif/frontend/repository/ScheduleRepository.kt +++ b/src/jsMain/kotlin/de/kif/frontend/repository/ScheduleRepository.kt @@ -1,6 +1,9 @@ package de.kif.frontend.repository -import de.kif.common.* +import de.kif.common.ConstraintMap +import de.kif.common.Message +import de.kif.common.Repository +import de.kif.common.RepositoryType import de.kif.common.model.Schedule import de.kif.frontend.MessageHandler import de.westermann.kobserve.event.EventHandler @@ -28,13 +31,13 @@ object ScheduleRepository : Repository { } override suspend fun create(model: Schedule): Long { - return repositoryPost("$prefix/api/schedules", Serialization.stringify(Schedule.serializer(), model)) + return repositoryPost("$prefix/api/schedules", Message.json.stringify(Schedule.serializer(), model)) ?: throw IllegalStateException("Cannot create model!") } override suspend fun update(model: Schedule) { if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") - repositoryPost("$prefix/api/schedule/${model.id}", Serialization.stringify(Schedule.serializer(), model)) + repositoryPost("$prefix/api/schedule/${model.id}", Message.json.stringify(Schedule.serializer(), model)) } override suspend fun delete(id: Long) { diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/TrackRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/TrackRepository.kt index 754cdef..cf5dcae 100644 --- a/src/jsMain/kotlin/de/kif/frontend/repository/TrackRepository.kt +++ b/src/jsMain/kotlin/de/kif/frontend/repository/TrackRepository.kt @@ -3,7 +3,6 @@ package de.kif.frontend.repository import de.kif.common.Message import de.kif.common.Repository import de.kif.common.RepositoryType -import de.kif.common.Serialization import de.kif.common.model.Track import de.kif.frontend.MessageHandler import de.westermann.kobserve.event.EventHandler @@ -26,13 +25,13 @@ object TrackRepository : Repository { } override suspend fun create(model: Track): Long { - return repositoryPost("$prefix/api/tracks", Serialization.stringify(Track.serializer(), model)) + return repositoryPost("$prefix/api/tracks", Message.json.stringify(Track.serializer(), model)) ?: throw IllegalStateException("Cannot create model!") } override suspend fun update(model: Track) { if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") - repositoryPost("$prefix/api/track/${model.id}", Serialization.stringify(Track.serializer(), model)) + repositoryPost("$prefix/api/track/${model.id}", Message.json.stringify(Track.serializer(), model)) } override suspend fun delete(id: Long) { diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/UserRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/UserRepository.kt index 90c0438..b5a42db 100644 --- a/src/jsMain/kotlin/de/kif/frontend/repository/UserRepository.kt +++ b/src/jsMain/kotlin/de/kif/frontend/repository/UserRepository.kt @@ -3,7 +3,6 @@ package de.kif.frontend.repository import de.kif.common.Message import de.kif.common.Repository import de.kif.common.RepositoryType -import de.kif.common.Serialization import de.kif.common.model.User import de.kif.frontend.MessageHandler import de.westermann.kobserve.event.EventHandler @@ -26,13 +25,13 @@ object UserRepository : Repository { } override suspend fun create(model: User): Long { - return repositoryPost("$prefix/api/users", Serialization.stringify(User.serializer(), model)) + return repositoryPost("$prefix/api/users", Message.json.stringify(User.serializer(), model)) ?: throw IllegalStateException("Cannot create model!") } override suspend fun update(model: User) { if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") - repositoryPost("$prefix/api/user/${model.id}", Serialization.stringify(User.serializer(), model)) + repositoryPost("$prefix/api/user/${model.id}", Message.json.stringify(User.serializer(), model)) } override suspend fun delete(id: Long) { diff --git a/src/jsMain/kotlin/de/kif/frontend/repository/WorkGroupRepository.kt b/src/jsMain/kotlin/de/kif/frontend/repository/WorkGroupRepository.kt index 6714c71..18c240c 100644 --- a/src/jsMain/kotlin/de/kif/frontend/repository/WorkGroupRepository.kt +++ b/src/jsMain/kotlin/de/kif/frontend/repository/WorkGroupRepository.kt @@ -3,7 +3,6 @@ package de.kif.frontend.repository import de.kif.common.Message import de.kif.common.Repository import de.kif.common.RepositoryType -import de.kif.common.Serialization import de.kif.common.model.WorkGroup import de.kif.frontend.MessageHandler import de.westermann.kobserve.event.EventHandler @@ -26,13 +25,13 @@ object WorkGroupRepository : Repository { } override suspend fun create(model: WorkGroup): Long { - return repositoryPost("$prefix/api/workgroups", Serialization.stringify(WorkGroup.serializer(), model)) + return repositoryPost("$prefix/api/workgroups", Message.json.stringify(WorkGroup.serializer(), model)) ?: throw IllegalStateException("Cannot create model!") } override suspend fun update(model: WorkGroup) { if (model.id == null) throw IllegalStateException("Cannot update model which was not created!") - repositoryPost("$prefix/api/workgroup/${model.id}", Serialization.stringify(WorkGroup.serializer(), model)) + repositoryPost("$prefix/api/workgroup/${model.id}", Message.json.stringify(WorkGroup.serializer(), model)) } override suspend fun delete(id: Long) { diff --git a/src/jsMain/kotlin/de/kif/frontend/views/Announcements.kt b/src/jsMain/kotlin/de/kif/frontend/views/Announcements.kt deleted file mode 100644 index 4c65cc8..0000000 --- a/src/jsMain/kotlin/de/kif/frontend/views/Announcements.kt +++ /dev/null @@ -1,21 +0,0 @@ -package de.kif.frontend.views - -import de.kif.frontend.launch -import de.kif.frontend.repository.AnnouncementRepository -import org.w3c.dom.HTMLElement -import org.w3c.dom.get -import kotlin.browser.document - -fun initAnnouncement() { - val announcement = document.getElementsByClassName("announcement")[0] as? HTMLElement ?: return - val span = announcement.children[0] as? HTMLElement ?: return - - AnnouncementRepository.onUpdate { - launch { - val text = AnnouncementRepository.getAnnouncement() - - announcement.classList.toggle("announcement-blank", text.isBlank()) - span.textContent = text - } - } -} \ No newline at end of file diff --git a/src/jsMain/resources/style/components/_board.scss b/src/jsMain/resources/style/components/_board.scss index 22ffc32..4abf210 100644 --- a/src/jsMain/resources/style/components/_board.scss +++ b/src/jsMain/resources/style/components/_board.scss @@ -58,7 +58,7 @@ width: 100%; height: 100%; border-bottom: solid 1px var(--table-border-color); - box-shadow: 0 0 4px var(--shadow-color); + box-shadow: 0 0 4px black; top: -1rem; padding-top: 1.2rem; } @@ -205,10 +205,3 @@ margin-top: -1px !important; } } - -.board-announcement { - position: fixed; - bottom: 3rem; - z-index: 10; - margin-bottom: 0 !important; -} diff --git a/src/jsMain/resources/style/style.scss b/src/jsMain/resources/style/style.scss index 4f12582..a55129c 100644 --- a/src/jsMain/resources/style/style.scss +++ b/src/jsMain/resources/style/style.scss @@ -160,42 +160,3 @@ a { } } } - -.offline-banner { - position: fixed; - bottom: 3rem; - left: 0; - right: 0; - height: 3rem; - line-height: 3rem; - text-align: center; - font-size: 1.2rem; - background-color: var(--primary-color); - color: var(--primary-text-color); - z-index: 12; - box-shadow: 0 1px 4px var(--shadow-color); - display: none; -} - -body.offline { - .offline-banner { - display: block; - } -} - -.announcement { - height: 3rem; - line-height: 3rem; - text-align: center; - width: 100%; - font-size: 1.2rem; - background-color: var(--primary-color); - color: var(--primary-text-color); - box-shadow: 0 1px 4px var(--shadow-color); - margin-bottom: 1rem; - display: block; - - &.announcement-blank { - display: none; - } -} diff --git a/src/jvmMain/kotlin/de/kif/backend/Application.kt b/src/jvmMain/kotlin/de/kif/backend/Application.kt index e7efee4..008d8b5 100644 --- a/src/jvmMain/kotlin/de/kif/backend/Application.kt +++ b/src/jvmMain/kotlin/de/kif/backend/Application.kt @@ -15,6 +15,7 @@ import io.ktor.jackson.jackson import io.ktor.response.respond import io.ktor.routing.route import io.ktor.routing.routing +import io.ktor.websocket.WebSockets import org.slf4j.event.Level import java.nio.file.Paths @@ -30,7 +31,7 @@ fun Application.main() { install(ConditionalHeaders) install(Compression) install(DataConversion) - install(AutoHeadResponse) + install(WebSockets) install(StatusPages) { exception { @@ -90,7 +91,6 @@ fun Application.main() { workGroupApi() constraintsApi() postApi() - announcementApi() // Web socket push notifications pushService() diff --git a/src/jvmMain/kotlin/de/kif/backend/Configuration.kt b/src/jvmMain/kotlin/de/kif/backend/Configuration.kt index eb7d7e3..21ffe5e 100644 --- a/src/jvmMain/kotlin/de/kif/backend/Configuration.kt +++ b/src/jvmMain/kotlin/de/kif/backend/Configuration.kt @@ -42,7 +42,6 @@ object Configuration { val sessions by required() val uploads by required() val database by required() - val announcement by required() } object Path { @@ -57,9 +56,6 @@ object Configuration { val database by c(PathSpec.database) val databasePath: java.nio.file.Path by lazy { Paths.get(database).toAbsolutePath() } - - val announcement by c(PathSpec.announcement) - val announcementPath: java.nio.file.Path by lazy { Paths.get(announcement).toAbsolutePath() } } private object ScheduleSpec : ConfigSpec("schedule") { diff --git a/src/jvmMain/kotlin/de/kif/backend/repository/AnnouncementRepository.kt b/src/jvmMain/kotlin/de/kif/backend/repository/AnnouncementRepository.kt deleted file mode 100644 index af0a571..0000000 --- a/src/jvmMain/kotlin/de/kif/backend/repository/AnnouncementRepository.kt +++ /dev/null @@ -1,53 +0,0 @@ -package de.kif.backend.repository - -import de.kif.backend.Configuration -import de.kif.backend.util.PushService -import de.kif.common.MessageType -import de.kif.common.RepositoryType -import de.westermann.kobserve.event.EventHandler -import kotlinx.coroutines.runBlocking - -object AnnouncementRepository { - - val onUpdate = EventHandler() - - private val file = Configuration.Path.announcementPath.toFile() - - private var announcement = "" - - fun setAnnouncement(value: String) { - val str = value.replace("\n", " ").trim() - if (announcement == str) return - - announcement = str - try { - file.writeText(str) - } catch (e: Exception) { - - } - - onUpdate.emit(Unit) - } - - fun getAnnouncement(): String { - return announcement - } - - fun registerPushService() { - onUpdate { - runBlocking { - PushService.notify(MessageType.UPDATE, RepositoryType.ANNOUNCEMENT, 0L) - } - } - } - - init { - try { - if (!file.exists()) { - file.createNewFile() - } - announcement = file.readText().trim() - } catch (e: Exception) { - } - } -} diff --git a/src/jvmMain/kotlin/de/kif/backend/repository/WorkGroupRepository.kt b/src/jvmMain/kotlin/de/kif/backend/repository/WorkGroupRepository.kt index 46a1330..a4ef155 100644 --- a/src/jvmMain/kotlin/de/kif/backend/repository/WorkGroupRepository.kt +++ b/src/jvmMain/kotlin/de/kif/backend/repository/WorkGroupRepository.kt @@ -3,7 +3,10 @@ package de.kif.backend.repository import de.kif.backend.database.DbWorkGroup import de.kif.backend.database.dbQuery import de.kif.backend.util.PushService -import de.kif.common.* +import de.kif.common.Message +import de.kif.common.MessageType +import de.kif.common.Repository +import de.kif.common.RepositoryType import de.kif.common.model.Constraint import de.kif.common.model.WorkGroup import de.westermann.kobserve.event.EventHandler @@ -33,8 +36,8 @@ object WorkGroupRepository : Repository { val accessible = row[DbWorkGroup.accessible] val length = row[DbWorkGroup.length] val language = row[DbWorkGroup.language] - val leader = Serialization.parse(String.serializer().list, row[DbWorkGroup.leader]) - val constraints = Serialization.parse(Constraint.serializer().list, row[DbWorkGroup.constraints]) + val leader = Message.json.parse(String.serializer().list, row[DbWorkGroup.leader]) + val constraints = Message.json.parse(Constraint.serializer().list, row[DbWorkGroup.constraints]) val createdAt = row[DbWorkGroup.createdAt] val updatedAt = row[DbWorkGroup.updatedAt] @@ -89,8 +92,8 @@ object WorkGroupRepository : Repository { it[accessible] = model.accessible it[length] = model.length it[language] = model.language - it[leader] = Serialization.stringify(String.serializer().list, model.leader) - it[constraints] = Serialization.stringify(Constraint.serializer().list, model.constraints) + it[leader] = Message.json.stringify(String.serializer().list, model.leader) + it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints) it[createdAt] = now it[updatedAt] = now @@ -121,8 +124,8 @@ object WorkGroupRepository : Repository { it[accessible] = model.accessible it[length] = model.length it[language] = model.language - it[leader] = Serialization.stringify(String.serializer().list, model.leader) - it[constraints] = Serialization.stringify(Constraint.serializer().list, model.constraints) + it[leader] = Message.json.stringify(String.serializer().list, model.leader) + it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints) it[updatedAt] = now } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Board.kt b/src/jvmMain/kotlin/de/kif/backend/route/Board.kt index 1407793..14479d0 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Board.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Board.kt @@ -1,8 +1,6 @@ package de.kif.backend.route import de.kif.backend.Configuration -import de.kif.backend.prefix -import de.kif.backend.repository.AnnouncementRepository import de.kif.backend.repository.RoomRepository import de.kif.backend.repository.ScheduleRepository import de.kif.backend.view.respondMain @@ -89,18 +87,6 @@ fun Route.board() { val nowLocale = now.time + timeOffset respondMain(true, true) { theme -> content { - - val announcement = AnnouncementRepository.getAnnouncement() - var classes = "board-announcement announcement" - if (announcement.isBlank()) { - classes += " announcement-blank" - } - div(classes) { - span { - +announcement - } - } - div("board") { div("board-header") { div("board-running") { @@ -156,7 +142,7 @@ fun Route.board() { } } div("board-logo") { - img("KIF 47.0", "$prefix/static/images/logo.svg") + img("KIF 47.0", "/static/images/logo.svg") div("board-header-date") { attributes["data-now"] = (now.time - timeOffset).toString() } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/News.kt b/src/jvmMain/kotlin/de/kif/backend/route/News.kt index 4d3662b..ccb2aa6 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/News.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/News.kt @@ -1,7 +1,6 @@ package de.kif.backend.route import de.kif.backend.* -import de.kif.backend.repository.AnnouncementRepository import de.kif.backend.repository.PostRepository import de.kif.backend.util.markdownToHtml import de.kif.backend.view.respondMain @@ -14,13 +13,11 @@ import io.ktor.http.content.PartData import io.ktor.http.content.forEachPart import io.ktor.http.content.streamProvider import io.ktor.request.receiveMultipart -import io.ktor.request.receiveParameters import io.ktor.response.respond import io.ktor.response.respondRedirect import io.ktor.routing.Route import io.ktor.routing.get import io.ktor.routing.post -import io.ktor.util.toMap import kotlinx.html.* import java.io.File @@ -78,12 +75,9 @@ fun Route.overview() { content { if (editable) { div("overview-new") { - a("$prefix/post/new", classes = "form-btn") { + a("post/new", classes = "form-btn") { +"Neuer Eintrag" } - a("$prefix/announcement", classes = "form-btn") { - +"Ankündigungsbanner bearbeiten" - } } } div("overview") { @@ -139,7 +133,7 @@ fun Route.overview() { } get("/post/{id}") { - authenticateOrRedirect(Permission.POST) { + authenticateOrRedirect(Permission.POST) { user -> val postId = call.parameters["id"]?.toLongOrNull() ?: return@get val editPost = PostRepository.get(postId) ?: return@get respondMain { @@ -297,7 +291,7 @@ fun Route.overview() { } post("/post/{id}") { - authenticateOrRedirect(Permission.POST) { + authenticateOrRedirect(Permission.POST) { user -> val postId = call.parameters["id"]?.toLongOrNull() ?: return@post var imageUploadName: String? = null @@ -367,7 +361,7 @@ fun Route.overview() { } get("/post/new") { - authenticateOrRedirect(Permission.POST) { + authenticateOrRedirect(Permission.POST) { user -> respondMain { content { h1 { +"Beitrag erstellen" } @@ -497,7 +491,7 @@ fun Route.overview() { } post("/post/new") { - authenticateOrRedirect(Permission.POST) { + authenticateOrRedirect(Permission.POST) { user -> var imageUploadName: String? = null val params = mutableMapOf() @@ -543,7 +537,7 @@ fun Route.overview() { } get("/post/{id}/delete") { - authenticateOrRedirect(Permission.POST) { + authenticateOrRedirect(Permission.POST) { user -> val postId = call.parameters["id"]?.toLongOrNull() ?: return@get PostRepository.delete(postId) @@ -551,57 +545,4 @@ fun Route.overview() { call.respondRedirect("$prefix/") } } - - - get("/announcement") { - authenticateOrRedirect(Permission.POST) { - respondMain { - content { - h1 { +"Ankündigungsbanner bearbeiten" } - form(method = FormMethod.post) { - div("form-group") { - label { - htmlFor = "announcement" - +"Ankündigung" - } - input( - name = "announcement", - classes = "form-control" - ) { - id = "announcement" - placeholder = "Ankündigung" - value = AnnouncementRepository.getAnnouncement() - } - } - - div("form-group") { - a("$prefix/") { - button(classes = "form-btn") { - +"Abbrechen" - } - } - button(type = ButtonType.submit, classes = "form-btn btn-primary") { - +"Speichern" - } - } - } - } - } - } - } - - - post("/announcement") { - authenticateOrRedirect(Permission.POST) { - val params = call.receiveParameters().toMap().mapValues { (_, list) -> - list.firstOrNull() - } - - val announcement = params["announcement"] ?: return@post - - AnnouncementRepository.setAnnouncement(announcement) - - call.respondRedirect("$prefix/") - } - } } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Room.kt b/src/jvmMain/kotlin/de/kif/backend/route/Room.kt index 9d4cc55..ec8cad1 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Room.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Room.kt @@ -29,7 +29,7 @@ import de.kif.backend.prefix fun Route.room() { get("/rooms") { - authenticateOrRedirect(Permission.ROOM) { + authenticateOrRedirect(Permission.ROOM) { user -> val search = call.parameters["search"] ?: "" val list = RoomRepository.all() @@ -106,7 +106,7 @@ fun Route.room() { } get("/room/{id}") { - authenticateOrRedirect(Permission.ROOM) { + authenticateOrRedirect(Permission.ROOM) { user -> val roomId = call.parameters["id"]?.toLongOrNull() ?: return@get val editRoom = RoomRepository.get(roomId) ?: return@get respondMain { @@ -260,7 +260,7 @@ fun Route.room() { } post("/room/{id}") { - authenticateOrRedirect(Permission.ROOM) { + authenticateOrRedirect(Permission.ROOM) { user -> val roomId = call.parameters["id"]?.toLongOrNull() ?: return@post val params = call.receiveParameters().toMap().mapValues { (_, list) -> list.firstOrNull() @@ -283,7 +283,7 @@ fun Route.room() { } get("/room/new") { - authenticateOrRedirect(Permission.ROOM) { + authenticateOrRedirect(Permission.ROOM) { user -> respondMain { content { h1 { +"Raum erstellen" } @@ -430,7 +430,7 @@ fun Route.room() { } post("/room/new") { - authenticateOrRedirect(Permission.ROOM) { + authenticateOrRedirect(Permission.ROOM) { user -> val params = call.receiveParameters().toMap().mapValues { (_, list) -> list.firstOrNull() } @@ -453,7 +453,7 @@ fun Route.room() { } get("/room/{id}/delete") { - authenticateOrRedirect(Permission.ROOM) { + authenticateOrRedirect(Permission.ROOM) { user -> val roomId = call.parameters["id"]?.toLongOrNull() ?: return@get RoomRepository.delete(roomId) diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Track.kt b/src/jvmMain/kotlin/de/kif/backend/route/Track.kt index 15764fe..81b9d35 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Track.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Track.kt @@ -83,7 +83,7 @@ fun DIV.colorPicker(color: Color?) { fun Route.track() { get("/tracks") { - authenticateOrRedirect(Permission.WORK_GROUP) { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> val search = call.parameters["search"] ?: "" val list = TrackRepository.all() respondMain { @@ -145,7 +145,7 @@ fun Route.track() { } get("/track/{id}") { - authenticateOrRedirect(Permission.WORK_GROUP) { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> val trackId = call.parameters["id"]?.toLongOrNull() ?: return@get val editTrack = TrackRepository.get(trackId) ?: return@get respondMain { @@ -192,7 +192,7 @@ fun Route.track() { } post("/track/{id}") { - authenticateOrRedirect(Permission.WORK_GROUP) { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> val trackId = call.parameters["id"]?.toLongOrNull() ?: return@post val params = call.receiveParameters().toMap().mapValues { (_, list) -> list.firstOrNull() @@ -212,7 +212,7 @@ fun Route.track() { } get("/track/new") { - authenticateOrRedirect(Permission.WORK_GROUP) { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> respondMain { content { h1 { +"Track erstellen" } @@ -251,7 +251,7 @@ fun Route.track() { } post("/track/new") { - authenticateOrRedirect(Permission.WORK_GROUP) { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> val params = call.receiveParameters().toMap().mapValues { (_, list) -> list.firstOrNull() } @@ -270,7 +270,7 @@ fun Route.track() { } get("track/{id}/delete") { - authenticateOrRedirect(Permission.WORK_GROUP) { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> val trackId = call.parameters["id"]?.toLongOrNull() ?: return@get TrackRepository.delete(trackId) diff --git a/src/jvmMain/kotlin/de/kif/backend/route/User.kt b/src/jvmMain/kotlin/de/kif/backend/route/User.kt index 2f589f3..a301f1d 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/User.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/User.kt @@ -26,8 +26,8 @@ import kotlin.collections.component2 import kotlin.collections.set fun Route.user() { - get("/users") { - authenticateOrRedirect(Permission.USER) { + get("/users") { param -> + authenticateOrRedirect(Permission.USER) { user -> val search = call.parameters["search"] ?: "" val list = UserRepository.all() respondMain { diff --git a/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt b/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt index 9c90782..0957c4b 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt @@ -25,7 +25,7 @@ private const val separator = "###" fun Route.workGroup() { get("workgroups") { - authenticateOrRedirect(Permission.WORK_GROUP) { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> val search = call.parameters["search"] ?: "" val list = WorkGroupRepository.all() respondMain { @@ -146,7 +146,7 @@ fun Route.workGroup() { } get("/workgroup/{id}") { - authenticateOrRedirect(Permission.WORK_GROUP) { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> val workGroupId = call.parameters["id"]?.toLongOrNull() ?: return@get val editWorkGroup = WorkGroupRepository.get(workGroupId) ?: return@get val tracks = TrackRepository.all() @@ -532,7 +532,7 @@ fun Route.workGroup() { } post("/workgroup/{id}") { - authenticateOrRedirect(Permission.WORK_GROUP) { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> val workGroupId = call.parameters["id"]?.toLongOrNull() ?: return@post val params = call.receiveParameters().toMap().mapValues { (_, list) -> list.firstOrNull() @@ -574,7 +574,7 @@ fun Route.workGroup() { } get("/workgroup/new") { - authenticateOrRedirect(Permission.WORK_GROUP) { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> val tracks = TrackRepository.all() respondMain { content { @@ -823,7 +823,7 @@ fun Route.workGroup() { } post("/workgroup/new") { - authenticateOrRedirect(Permission.WORK_GROUP) { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> val params = call.receiveParameters().toMap().mapValues { (_, list) -> list.firstOrNull() } @@ -872,7 +872,7 @@ fun Route.workGroup() { } get("/workgroup/{id}/delete") { - authenticateOrRedirect(Permission.WORK_GROUP) { + authenticateOrRedirect(Permission.WORK_GROUP) { user -> val workGroupId = call.parameters["id"]?.toLongOrNull() ?: return@get WorkGroupRepository.delete(workGroupId) diff --git a/src/jvmMain/kotlin/de/kif/backend/route/api/Announcement.kt b/src/jvmMain/kotlin/de/kif/backend/route/api/Announcement.kt deleted file mode 100644 index 938e94e..0000000 --- a/src/jvmMain/kotlin/de/kif/backend/route/api/Announcement.kt +++ /dev/null @@ -1,39 +0,0 @@ -package de.kif.backend.route.api - -import de.kif.backend.authenticate -import de.kif.backend.repository.AnnouncementRepository -import de.kif.backend.repository.RoomRepository -import de.kif.common.model.Permission -import de.kif.common.model.Room -import io.ktor.application.call -import io.ktor.http.HttpStatusCode -import io.ktor.request.receive -import io.ktor.routing.Route -import io.ktor.routing.get -import io.ktor.routing.post - -fun Route.announcementApi() { - get("/api/announcement") { - try { - call.success(AnnouncementRepository.getAnnouncement()) - } catch (_: Exception) { - call.error(HttpStatusCode.InternalServerError) - } - } - - post("/api/announcement") { - try { - authenticate(Permission.POST) { - val announcement = call.receive() - - AnnouncementRepository.setAnnouncement(announcement) - - call.success() - } onFailure { - call.error(HttpStatusCode.Unauthorized) - } - } catch (_: Exception) { - call.error(HttpStatusCode.InternalServerError) - } - } -} diff --git a/src/jvmMain/kotlin/de/kif/backend/util/Backup.kt b/src/jvmMain/kotlin/de/kif/backend/util/Backup.kt index 21c0c8d..b9f758f 100644 --- a/src/jvmMain/kotlin/de/kif/backend/util/Backup.kt +++ b/src/jvmMain/kotlin/de/kif/backend/util/Backup.kt @@ -4,7 +4,6 @@ import de.kif.backend.database.Connection import de.kif.backend.repository.* import de.kif.common.Message import de.kif.common.RepositoryType -import de.kif.common.Serialization import de.kif.common.model.* import kotlinx.serialization.Serializable @@ -40,16 +39,15 @@ data class Backup( RepositoryType.USER -> backup.copy(users = UserRepository.all()) RepositoryType.WORK_GROUP -> backup.copy(workGroups = WorkGroupRepository.all()) RepositoryType.POST -> backup.copy(posts = PostRepository.all()) - else -> backup } } - return Serialization.stringify(serializer(), backup) + return Message.json.stringify(serializer(), backup) } @Suppress("UNUSED_VARIABLE") suspend fun import(data: String) { - val backup = Serialization.parse(serializer(), data) + val backup = Message.json.parse(serializer(), data) backup.users.forEach { UserRepository.create(it); println("Import user ${it.username}") } backup.posts.forEach { PostRepository.create(it); println("Import post") } diff --git a/src/jvmMain/kotlin/de/kif/backend/util/PushService.kt b/src/jvmMain/kotlin/de/kif/backend/util/PushService.kt index 78c7127..12f07e3 100644 --- a/src/jvmMain/kotlin/de/kif/backend/util/PushService.kt +++ b/src/jvmMain/kotlin/de/kif/backend/util/PushService.kt @@ -1,60 +1,48 @@ package de.kif.backend.util +import de.kif.backend.prefix import de.kif.backend.repository.* -import de.kif.backend.route.api.error -import de.kif.backend.route.api.success -import de.kif.common.Message -import de.kif.common.MessageBox -import de.kif.common.MessageType +import de.kif.common.* import de.kif.common.RepositoryType -import io.ktor.application.call -import io.ktor.http.HttpStatusCode +import io.ktor.http.cio.websocket.Frame +import io.ktor.http.cio.websocket.readText import io.ktor.routing.Route -import io.ktor.routing.get +import io.ktor.websocket.WebSocketServerSession +import io.ktor.websocket.webSocket +import kotlinx.coroutines.channels.ClosedReceiveChannelException object PushService { - var leastValidTimestamp = System.currentTimeMillis() + var clients: List = emptyList() - /** - * Save the message with the current timestamp - */ - fun notify(type: MessageType, repository: RepositoryType, id: Long) { - val timestamp = System.currentTimeMillis() - val message = Message(type, repository, id) - } - - /** - * Get all messages created after the given timestamp. - * The return Box has the current timestamp. If the leastValidTimestamp is less then the current timestamp set the - * valid flag to false and return an empty message list. - */ - fun getMessages(timestamp: Long?): MessageBox { - return MessageBox( - System.currentTimeMillis(), - timestamp != null && timestamp > leastValidTimestamp, - emptyList() - ) - } - - /** - * Delete all messages before the given timestamp - */ - fun gc(timestamp: Long) { - leastValidTimestamp = timestamp + suspend fun notify(type: MessageType, repository: RepositoryType, id: Long) { + try { + val data = Message(type, repository, id).stringify() + for (client in clients) { + client.outgoing.send(Frame.Text(data)) + } + } catch (e: Exception) { + e.printStackTrace() + } } } fun Route.pushService() { - get("/api/updates") { + webSocket("/websocket") { + PushService.clients += this + try { - val timestamp = call.request.queryParameters["timestamp"]?.toLongOrNull() - - val messageBox = PushService.getMessages(timestamp) - - call.success(messageBox) - } catch (_: Exception) { - call.error(HttpStatusCode.InternalServerError) + while (true) { + val text = (incoming.receive() as Frame.Text).readText() + println("onMessage($text)") + outgoing.send(Frame.Text(text)) + } + } catch (_: ClosedReceiveChannelException) { + PushService.clients -= this + } catch (e: Throwable) { + println("onFailure ${closeReason.await()}") + e.printStackTrace() + PushService.clients -= this } } @@ -64,5 +52,4 @@ fun Route.pushService() { UserRepository.registerPushService() WorkGroupRepository.registerPushService() PostRepository.registerPushService() - AnnouncementRepository.registerPushService() } diff --git a/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt b/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt index 3f8271e..f96a52e 100644 --- a/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt +++ b/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt @@ -3,7 +3,6 @@ package de.kif.backend.view import de.kif.backend.PortalSession import de.kif.backend.Resources import de.kif.backend.prefix -import de.kif.backend.repository.AnnouncementRepository import de.kif.common.model.User import io.ktor.application.ApplicationCall import io.ktor.application.call @@ -74,25 +73,10 @@ class MainTemplate( } } body { - attributes["data-timestamp"] = currentTimeMillis().toString() - if (!noMenu) { insert(MenuTemplate(url, user)) {} } - if (!noMenu) { - val announcement = AnnouncementRepository.getAnnouncement() - var classes = "announcement" - if (announcement.isBlank()) { - classes += " announcement-blank" - } - div(classes) { - span { - +announcement - } - } - } - val containerClasses = if (stretch) "container-full" else "container" div(containerClasses) { div("main") { @@ -100,10 +84,6 @@ class MainTemplate( } } - div("offline-banner") { - span { +"Die Verbindung zum Server ist unterbrochen" } - } - if (!noMenu) { div("footer") { div("container") { diff --git a/src/jvmMain/resources/portal.toml b/src/jvmMain/resources/portal.toml index 28f8096..02fc07d 100644 --- a/src/jvmMain/resources/portal.toml +++ b/src/jvmMain/resources/portal.toml @@ -9,7 +9,6 @@ web = "web" sessions = "data/sessions" uploads = "data/uploads" database = "data/portal.db" -announcement = "data/announcement.txt" [schedule] reference = "1970-01-01"