diff --git a/build.gradle b/build.gradle index c5e9b34..97fff2d 100644 --- a/build.gradle +++ b/build.gradle @@ -99,6 +99,10 @@ kotlin { implementation "com.soywiz:klock-jvm:$klockVersion" implementation "com.soywiz:klock-locale-jvm:$klockVersion" + //implementation 'com.twitter:hbc-core:2.2.0' + //implementation 'com.twitter:hbc-twitter4j:2.2.0' + api 'org.twitter4j:twitter4j-stream:4.0.1' + implementation 'com.github.uchuhimo:konf:master-SNAPSHOT' implementation 'com.vladsch.flexmark:flexmark-all:0.42.10' api 'io.github.microutils:kotlin-logging:1.6.23' diff --git a/src/jsMain/resources/style/_color.scss b/src/jsMain/resources/style/_color.scss index 2e1c177..d00ab4c 100644 --- a/src/jsMain/resources/style/_color.scss +++ b/src/jsMain/resources/style/_color.scss @@ -1,6 +1,5 @@ $background-primary-color: #fff; -$background-secondary-color: #f5f5f5; -$background-card-color: #fff; +$background-secondary-color: #fcfcfc; $text-primary-color: #333; $text-secondary-color: rgba($text-primary-color, 0.5); @@ -16,7 +15,7 @@ $input-border-color: #888; $table-border-color: rgba($text-primary-color, 0.1); $table-header-color: rgba($text-primary-color, 0.06); -$shadow-color: rgba(#000, 0.3); +$shadow-color: rgba($text-primary-color, 0.8); $icon-color-focused: rgba($text-primary-color, 0.87); $icon-color: rgba($text-primary-color, 0.54); @@ -31,7 +30,6 @@ $lever-enabled-color: $primary-color; :root { --background-primary-color: $background-primary-color; --background-secondary-color: $background-secondary-color; - --background-card-color: $background-card-color; --text-primary-color: $text-primary-color; --text-secondary-color: $text-secondary-color; diff --git a/src/jsMain/resources/style/board.scss b/src/jsMain/resources/style/board.scss deleted file mode 100644 index 8b13789..0000000 --- a/src/jsMain/resources/style/board.scss +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/jsMain/resources/style/components/_board.scss b/src/jsMain/resources/style/components/_board.scss index bff8f29..4718443 100644 --- a/src/jsMain/resources/style/components/_board.scss +++ b/src/jsMain/resources/style/components/_board.scss @@ -1,124 +1,73 @@ @import "../config"; .board-header { - line-height: 3rem; - flex-grow: 1; - font-family: "Bungee", sans-serif; - font-weight: normal; - font-size: 1.1rem; - padding-left: 0.3rem; -} + line-height: 6rem; + display: flex; + padding: 0 2rem; -.board-card { - background-color: var(--background-card-color); - box-shadow: 0 0.1rem 0.2rem var(--shadow-color); - border-radius: $border-radius; - overflow: hidden; + & > div { + flex-grow: 1; + font-family: "Bungee", sans-serif; + font-weight: normal; + font-size: 1.1rem; + text-align: center; + + &:first-child { + text-align: left; + } + &:last-child { + text-align: right; + } + } } .board { - padding: 1rem 0.4rem 2rem; + padding: 1rem 1rem 2rem; display: flex; overflow: hidden; - height: calc(100vh - 0.5rem); + height: calc(100vh - 6rem); & > div { flex-grow: 1; flex-basis: 0; - padding: 0 0.4rem; - - &:nth-child(1) { - flex-grow: 4; - - padding-left: 0.15rem; - padding-right: 0.15rem; - } - - &:nth-child(2) { - flex-grow: 3; - } - - &:nth-child(3) { - flex-grow: 3; - } } } -.board-twitter { - .board-card { - height: calc(100% - 1.3rem); - - & > * { - margin-top: -1px !important; - } - } -} - -.board-post { - margin-bottom: 0.5rem; - - .post-name { - top: 0.5rem; - } - - padding-top: 2.5rem; - padding-bottom: 0.5rem; -} - -.board-schedule-box { - display: flex; - flex-wrap: wrap; -} - .board-schedule { + width: 100%; position: relative; - flex-grow: 1; - flex-basis: 0; - min-width: 15rem; - padding: 0.6rem; - margin: 0 0.25rem 0.5rem; + padding: 1rem 1rem; &:not(:last-child) { border-bottom: solid 1px var(--table-border-color) } } -.board-schedule-bottom { +.board-schedule-room { display: block; padding-left: 1rem; - padding-right: 0.5rem; - line-height: 1rem; - clear: both; + line-height: 2rem; +} - & > span { - - &:first-child { - float: left; - } - &:last-child { - float: right; - } - } +.board-schedule-time { + position: absolute; + top: 1rem; + right: 1rem; + line-height: 2rem; } .board-schedule-color { background-color: var(--primary-color); position: absolute; - top: 1.25rem; - left: 0.6rem; - width: 0.8rem; - height: 0.8rem; - border-radius: 100%; + top: 3.25rem; + left: 1rem; + width: 0.5rem; + height: 1.5rem; } .board-schedule-name { display: block; padding-left: 1rem; - line-height: 1.3rem; - - font-family: 'Montserrat', sans-serif; - font-weight: 600; - padding-top: 0.35rem; - padding-bottom: 0.35rem; + line-height: 2rem; } \ No newline at end of file diff --git a/src/jsMain/resources/style/components/_overview.scss b/src/jsMain/resources/style/components/_overview.scss index 8ab9330..30d7a96 100644 --- a/src/jsMain/resources/style/components/_overview.scss +++ b/src/jsMain/resources/style/components/_overview.scss @@ -12,10 +12,12 @@ } .overview-side { - min-width: 30%; + min-width: 20%; } .overview-twitter { + height: 20rem; + background-color: #b3e6f9; } .post { @@ -32,8 +34,6 @@ color: var(--primary-color); line-height: 2rem; padding: 0 1rem; - font-weight: bold; - font-family: 'Montserrat', sans-serif; &:empty::before { display: block; diff --git a/src/jsMain/resources/style/dark.scss b/src/jsMain/resources/style/dark.scss index 2d79406..4d7bf52 100644 --- a/src/jsMain/resources/style/dark.scss +++ b/src/jsMain/resources/style/dark.scss @@ -2,12 +2,11 @@ $background-primary-color: #2d2d2d; $background-secondary-color: #373737; -$background-card-color: rgba($text-primary-color, 0.06); $text-primary-color: #fff; $text-secondary-color: rgba($text-primary-color, 0.5); -$primary-color: #ef5350; +$primary-color: #dd213d; $primary-text-color: #fff; $error-color: #F00; @@ -18,7 +17,7 @@ $input-border-color: #888; $table-border-color: rgba($text-primary-color, 0.1); $table-header-color: rgba($text-primary-color, 0.06); -$shadow-color: rgba(#000, 0.3); +$shadow-color: rgba($text-primary-color, 0.8); $icon-color-focused: rgba($text-primary-color, 1.0); $icon-color: rgba($text-primary-color, 0.7); diff --git a/src/jsMain/resources/style/princess.scss b/src/jsMain/resources/style/princess.scss index bbd22d0..28720dd 100644 --- a/src/jsMain/resources/style/princess.scss +++ b/src/jsMain/resources/style/princess.scss @@ -2,7 +2,6 @@ $background-primary-color: #ffc3e1; $background-secondary-color: #ffa5d2; -$background-card-color: rgba($text-primary-color, 0.06); $text-primary-color: #333; $text-secondary-color: rgba($text-primary-color, 0.5); @@ -18,7 +17,7 @@ $input-border-color: #888; $table-border-color: rgba($text-primary-color, 0.1); $table-header-color: rgba($text-primary-color, 0.06); -$shadow-color: rgba(#000, 0.3); +$shadow-color: rgba($text-primary-color, 0.8); $icon-color-focused: rgba($text-primary-color, 0.87); $icon-color: rgba($text-primary-color, 0.54); diff --git a/src/jsMain/resources/style/style.scss b/src/jsMain/resources/style/style.scss index 66b9dbd..021ae19 100644 --- a/src/jsMain/resources/style/style.scss +++ b/src/jsMain/resources/style/style.scss @@ -14,8 +14,8 @@ body, html { color: var(--text-primary-color); background: var(--background-secondary-color); - font-family: 'Raleway', 'Montserrat', Roboto, Arial, sans-serif; - font-weight: 500; + font-family: 'Montserrat', Roboto, Arial, sans-serif; + font-weight: 600; width: 100%; height: 100%; diff --git a/src/jvmMain/kotlin/de/kif/backend/Configuration.kt b/src/jvmMain/kotlin/de/kif/backend/Configuration.kt index efcb9f7..53b4395 100644 --- a/src/jvmMain/kotlin/de/kif/backend/Configuration.kt +++ b/src/jvmMain/kotlin/de/kif/backend/Configuration.kt @@ -91,11 +91,13 @@ object Configuration { } private object TwitterSpec : ConfigSpec("twitter") { - val timeline by required("timeline") + val username by required("username") + val password by required("password") } object Twitter { - val timeline by c(TwitterSpec.timeline) + val username by c(TwitterSpec.username) + val password by c(TwitterSpec.password) } init { diff --git a/src/jvmMain/kotlin/de/kif/backend/Twitter.kt b/src/jvmMain/kotlin/de/kif/backend/Twitter.kt new file mode 100644 index 0000000..dd09637 --- /dev/null +++ b/src/jvmMain/kotlin/de/kif/backend/Twitter.kt @@ -0,0 +1,142 @@ +package de.kif.backend + +import twitter4j.* +import twitter4j.conf.ConfigurationBuilder + + +/* +fun twitter() { + val msgQueue = LinkedBlockingQueue(100000) + val eventQueue = LinkedBlockingQueue(1000) + + val hosts = HttpHosts(Constants.STREAM_HOST) + + val filterEndpoint = StatusesFilterEndpoint() + filterEndpoint.trackTerms(listOf("kif")) + + println(Configuration.Twitter.username) + println(Configuration.Twitter.password) + + val authentication = BasicAuth( + Configuration.Twitter.username, + Configuration.Twitter.password + ) + + val builder = ClientBuilder() + .name("kif-portal") + .hosts(hosts) + .authentication(authentication) + .endpoint(filterEndpoint) + .processor(StringDelimitedProcessor(msgQueue)) + .eventMessageQueue(eventQueue) + + val client = builder.build() + + val listener = object: StatusStreamHandler { + override fun onUnknownMessageType(msg: String?) { + println("onUnknownMessageType") + println(msg) + } + + override fun onDisconnectMessage(message: DisconnectMessage?) { + println("onDisconnectMessage") + println(message?.disconnectReason) + } + + override fun onStallWarningMessage(warning: StallWarningMessage?) { + println("onStallWarningMessage") + println(warning?.message) + } + + override fun onTrackLimitationNotice(numberOfLimitedStatuses: Int) { + println("onTrackLimitationNotice") + println(numberOfLimitedStatuses) + } + + override fun onStallWarning(warning: StallWarning?) { + println("onStallWarning") + println(warning?.message) + } + + override fun onException(ex: Exception?) { + println("onException") + ex?.printStackTrace() + } + + override fun onDeletionNotice(statusDeletionNotice: StatusDeletionNotice?) { + println("onDeletionNotice") + println(statusDeletionNotice?.statusId) + } + + override fun onStatus(status: Status?) { + println("onStatus") + println(status?.text) + } + + override fun onScrubGeo(userId: Long, upToStatusId: Long) { + println("onScrubGeo") + } + + } + + val t4jClient = Twitter4jStatusClient( + client, + msgQueue, + listOf(listener), + Executors.newFixedThreadPool(4) + ) + + t4jClient.connect() + + t4jClient.process() + + while (true) { + + } +} + */ + +fun twitter() { + val cb = ConfigurationBuilder() + cb.setDebugEnabled(true) + .setUser(Configuration.Twitter.username) + .setPassword(Configuration.Twitter.password) + + val listener = object : StatusListener { + + override fun onTrackLimitationNotice(numberOfLimitedStatuses: Int) { + println("onTrackLimitationNotice") + println(numberOfLimitedStatuses) + } + + override fun onStallWarning(warning: StallWarning?) { + println("onStallWarning") + println(warning?.message) + } + + override fun onException(ex: Exception?) { + println("onException") + ex?.printStackTrace() + } + + override fun onDeletionNotice(statusDeletionNotice: StatusDeletionNotice?) { + println("onDeletionNotice") + println(statusDeletionNotice?.statusId) + } + + override fun onStatus(status: Status?) { + println("onStatus") + println(status?.text) + } + + override fun onScrubGeo(userId: Long, upToStatusId: Long) { + println("onScrubGeo") + } + } + + val twitterStream = TwitterStreamFactory(cb.build()).instance + + addTwitterStreamListener(twitterStream, listener) + + twitterStream.sample() +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Account.kt b/src/jvmMain/kotlin/de/kif/backend/route/Account.kt index d44c97a..e651308 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Account.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Account.kt @@ -2,15 +2,18 @@ package de.kif.backend.route import de.kif.backend.authenticate import de.kif.backend.authenticateOrRedirect +import de.kif.backend.util.Backup import de.kif.backend.repository.TrackRepository import de.kif.backend.repository.WorkGroupRepository import de.kif.backend.route.api.error -import de.kif.backend.util.Backup import de.kif.backend.util.WikiImporter +import de.kif.backend.view.MainTemplate +import de.kif.backend.view.MenuTemplate import de.kif.backend.view.respondMain import de.kif.common.RepositoryType import de.kif.common.model.Permission import io.ktor.application.call +import io.ktor.html.respondHtmlTemplate import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode import io.ktor.http.content.PartData @@ -32,6 +35,10 @@ private val logger = KotlinLogging.logger {} fun Route.account() { get("/account") { authenticateOrRedirect { user -> + + val tracks = TrackRepository.all() + val wikiSections = WikiImporter.loadSections() + respondMain { content { h1 { +"Account" } @@ -47,26 +54,6 @@ fun Route.account() { } } - a(href = "/account/backup", classes = "form-btn") { - +"Backup" - } - - if (user.checkPermission(Permission.ADMIN)) { - a(href = "/account/import", classes = "form-btn") { - +"Import wiki" - } - } - } - } - } - } - - get("/account/backup") { - authenticateOrRedirect { user -> - respondMain { - content { - h1 { +"Backup" } - div("account-backup") { if (user.checkPermission(Permission.ROOM)) { a("/account/backup/rooms.json", classes = "form-btn") { @@ -112,11 +99,9 @@ fun Route.account() { } if (user.checkPermission(Permission.ADMIN)) { - h1 { +"Restore" } - div("account-import") { form( - action = "/account/restore", + action = "/account/import", method = FormMethod.post, encType = FormEncType.multipartFormData ) { @@ -161,25 +146,12 @@ fun Route.account() { } } } - } - } - } - } - - get("/account/import") { - authenticateOrRedirect(Permission.ADMIN) { user -> - val tracks = TrackRepository.all() - val wikiSections = WikiImporter.loadSections() - - respondMain { - content { - h1 { +"Import wiki" } if (user.checkPermission(Permission.ADMIN)) { div("account-import-wiki") { span { +"Import work group data from the kif wiki" } - form(action = "/account/import", method = FormMethod.post) { + form(action = "/account/import-wiki", method = FormMethod.post) { for ((index, section) in wikiSections.withIndex()) { div("form-group") { label { @@ -305,7 +277,7 @@ fun Route.account() { } } - post("/account/restore") { + post("/account/import") { authenticate(Permission.ADMIN) { var reset = false var import = "" @@ -336,7 +308,7 @@ fun Route.account() { } } - post("/account/import") { + post("/account/import-wiki") { authenticate(Permission.ADMIN) { val params = call.receiveParameters().toMap().mapValues { (_, list) -> list.firstOrNull() diff --git a/src/jvmMain/kotlin/de/kif/backend/route/Board.kt b/src/jvmMain/kotlin/de/kif/backend/route/Board.kt index dc9fdad..59e9f5b 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/Board.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Board.kt @@ -4,6 +4,7 @@ import de.kif.backend.Configuration import de.kif.backend.repository.PostRepository import de.kif.backend.repository.ScheduleRepository import de.kif.backend.view.respondMain +import de.kif.common.formatDateTime import de.kif.common.model.Schedule import io.ktor.routing.Route import io.ktor.routing.get @@ -11,11 +12,10 @@ import kotlinx.css.CSSBuilder import kotlinx.css.Color import kotlinx.html.div import kotlinx.html.span -import kotlinx.html.unsafe import java.util.* fun Route.board() { - get("/brett") { + get("/board") { val postList = PostRepository.all().asReversed() val scheduleList = ScheduleRepository.all().map { it to it.getAbsoluteStartTime() * 60 @@ -24,9 +24,8 @@ fun Route.board() { val referenceTime = Configuration.Schedule.referenceDate.time / 1000 val now = referenceTime - (Date().time / 1000) - respondMain(true, true) { theme -> + respondMain(true, true) { content { - /* div("board-header") { div { +"KIF 47.0" @@ -35,96 +34,46 @@ fun Route.board() { +formatDateTime(Date().time) } } - */ div("board") { div("board-schedules") { attributes["data-reference"] = referenceTime.toString() - div("board-header") { - +"AKs" - } + for ((schedule, time) in scheduleList) { + div("board-schedule") { + attributes["data-id"] = schedule.id.toString() - div("board-schedule-box") { - for ((schedule, time) in scheduleList) { - div("board-card board-schedule") { - attributes["data-id"] = schedule.id.toString() + span("board-schedule-room") { + +schedule.room.name + } + span("board-schedule-time") { + attributes["data-time"] = time.toString() + attributes["data-duration"] = schedule.workGroup.length.toString() - span("board-schedule-color") { - attributes["style"] = CSSBuilder().apply { - val c = schedule.workGroup.track?.color - if (c != null) { - backgroundColor = Color(c.toString()) - } - }.toString() - } - span("board-schedule-name") { - +schedule.workGroup.name - } - - div("board-schedule-bottom") { - span("board-schedule-time") { - attributes["data-time"] = time.toString() - attributes["data-duration"] = schedule.workGroup.length.toString() - - - val startTime = (time % MINUTES_OF_DAY).let { - if (it < 0) it + MINUTES_OF_DAY else it - } - val sm = (startTime % 60).toString().padStart(2, '0') - val sh = (startTime / 60).toString().padStart(2, '0') - val startTimeString = "$sh:$sm" - - val endTime = ((time + schedule.workGroup.length) % MINUTES_OF_DAY).let { - if (it < 0) it + MINUTES_OF_DAY else it - } - val em = (endTime % 60).toString().padStart(2, '0') - val eh = (endTime / 60).toString().padStart(2, '0') - val endTimeString = "$eh:$em" - - +"$startTimeString - $endTimeString" + +Schedule.timeDifferenceToString(time + now) + } + span("board-schedule-color") { + attributes["style"] = CSSBuilder().apply { + val c = schedule.workGroup.track?.color + if (c != null) { + backgroundColor = Color(c.toString()) } - - span("board-schedule-room") { - +schedule.room.name - } - } + }.toString() + } + span("board-schedule-name") { + +schedule.workGroup.name } } } } div("board-posts") { - div("board-header") { - +"News" - } for (post in postList) { - createPost(post, false, "board-card board-post") + createPost(post, false, "board-post overview-post") } } div("board-twitter") { - div("board-header") { - +"Tweets" - } - div("board-card") { - unsafe { - raw( - """ - - - """.trimIndent() - ) - } - } + } } } diff --git a/src/jvmMain/kotlin/de/kif/backend/route/News.kt b/src/jvmMain/kotlin/de/kif/backend/route/Overview.kt similarity index 96% rename from src/jvmMain/kotlin/de/kif/backend/route/News.kt rename to src/jvmMain/kotlin/de/kif/backend/route/Overview.kt index a993680..e280ca4 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/News.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/Overview.kt @@ -77,7 +77,7 @@ fun Route.overview() { val postList = PostRepository.all().asReversed() - respondMain { theme -> + respondMain { content { div("overview") { div("overview-main") { @@ -94,21 +94,7 @@ fun Route.overview() { } } div("overview-twitter") { - unsafe { - raw(""" - - - """.trimIndent()) - } + +"The Twitter Wall" } } } diff --git a/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt b/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt index a0ce39c..52bfbe5 100644 --- a/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt +++ b/src/jvmMain/kotlin/de/kif/backend/view/MainTemplate.kt @@ -2,6 +2,7 @@ package de.kif.backend.view import de.kif.backend.PortalSession import de.kif.backend.Resources +import de.kif.backend.authenticate import de.kif.common.model.User import io.ktor.application.ApplicationCall import io.ktor.application.call @@ -33,7 +34,7 @@ class MainTemplate( link(href = "/static/external/material-icons.css", type = LinkType.textCss, rel = LinkRel.stylesheet) link(href = "/static/external/font/Montserrat.css", type = LinkType.textCss, rel = LinkRel.stylesheet) link( - href = "https://fonts.googleapis.com/css?family=Bungee|Oswald|Raleway", + href = "https://fonts.googleapis.com/css?family=Bungee|Oswald", type = LinkType.textCss, rel = LinkRel.stylesheet ) @@ -49,9 +50,6 @@ class MainTemplate( Theme.PRINCESS -> { link(href = "/static/style/princess.css", type = LinkType.textCss, rel = LinkRel.stylesheet) } - Theme.BRETT -> { - link(href = "/static/style/board.css", type = LinkType.textCss, rel = LinkRel.stylesheet) - } } script(src = "/static/require.min.js") {} @@ -81,7 +79,8 @@ class MainTemplate( div("footer-credit") { } div("footer-theme") { - for ((it, name) in Theme.displayThemes) { + for (it in Theme.values()) { + val name = it.name.toLowerCase() a("?theme=${it.name}", classes = if (theme == it) "selected" else "") { id = "theme-$name" +name.capitalize() @@ -95,19 +94,14 @@ class MainTemplate( } } -enum class Theme(val display: Boolean, val dark: Boolean, val primaryColor: String) { - LIGHT(true, false, "#B11D33"), - DARK(true, true, "#ef5350"), - PRINCESS(true, false, "#B11D33"), - BRETT(false, false, "#B11D33"); +enum class Theme { + LIGHT, DARK, PRINCESS; companion object { - private val lookup = values().toList().associateBy { it.name } - - val displayThemes = values().filter { it.display }.map { it to it.name.toLowerCase() } + private val loopup = values().toList().associateBy { it.name } fun lookup(name: String?): Theme { - return lookup[(name ?: return LIGHT).toUpperCase()] ?: LIGHT + return loopup[(name ?: return LIGHT).toUpperCase()] ?: LIGHT } } } @@ -115,7 +109,7 @@ enum class Theme(val display: Boolean, val dark: Boolean, val primaryColor: Stri suspend fun PipelineContext.respondMain( noMenu: Boolean = false, stretch: Boolean = false, - body: MainTemplate.(Theme) -> Unit + body: MainTemplate.() -> Unit ) { val param = call.request.queryParameters["theme"] val url = call.request.uri.substring(1) @@ -130,17 +124,15 @@ suspend fun PipelineContext.respondMain( ) call.respondRedirect(call.request.path()) } else { - val theme = Theme.lookup(call.request.cookies["theme"]) call.respondHtmlTemplate( MainTemplate( - theme, + Theme.lookup(call.request.cookies["theme"]), url, user, noMenu, stretch - ) - ) { - body(theme) - } + ), + body = body + ) } } diff --git a/src/jvmMain/kotlin/twitter4j/AddListener.kt b/src/jvmMain/kotlin/twitter4j/AddListener.kt new file mode 100644 index 0000000..567207f --- /dev/null +++ b/src/jvmMain/kotlin/twitter4j/AddListener.kt @@ -0,0 +1,5 @@ +package twitter4j + +fun addTwitterStreamListener(stream: TwitterStream, listener: StatusListener) { + stream.addListener(listener) +}