Compare commits
	
		
			33 Commits
		
	
	
		
			1.2.2
			...
			6f4cb9fd66
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6f4cb9fd66 | |||
| 1f1374f112 | |||
| b783fb6c4f | |||
| 65dc8ea5d2 | |||
| 97efbd0877 | |||
| c62f576ace | |||
| 8ac78d29b1 | |||
| 024f2b04ce | |||
| bf71d62dc5 | |||
| 7dce2c6cfd | |||
| a1dc5656b8 | |||
| dd4c5259d2 | |||
| 884aab08ed | |||
| c64c8779e3 | |||
| 1d614a06c4 | |||
| 3f10c8afaa | |||
| 9de1e295dd | |||
| 6287d4582d | |||
| 7dfa0fc6c4 | |||
| a53b2b8fc1 | |||
| 36972c9322 | |||
| 7bf2920d17 | |||
| c30306c163 | |||
| 46c9a61124 | |||
| 36acf1a00a | |||
| f9029bf1c3 | |||
| fe72c02562 | |||
| 8d9fcd3d7c | |||
| ec7a0a7a64 | |||
| efd8f9f9f5 | |||
| e2dce9fab3 | |||
| bbac0d3688 | |||
| 6114077591 | 
| @ -1,9 +0,0 @@ | |||||||
| kind: pipeline |  | ||||||
| name: default |  | ||||||
|  |  | ||||||
| steps: |  | ||||||
| - name: test |  | ||||||
|   image: gradle:jdk11 |  | ||||||
|   commands: |  | ||||||
|   - gradle test |  | ||||||
|  |  | ||||||
							
								
								
									
										22
									
								
								.woodpecker.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.woodpecker.yml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | |||||||
|  | pipeline: | ||||||
|  |   test: | ||||||
|  |     image: gradle:jdk11 | ||||||
|  |     commands: | ||||||
|  |       - gradle test | ||||||
|  |   build: | ||||||
|  |     image: gradle:jdk11 | ||||||
|  |     commands: | ||||||
|  |       - gradle bootJar | ||||||
|  |     when: | ||||||
|  |       event: | ||||||
|  |         - tag | ||||||
|  |   docker: | ||||||
|  |     image: plugins/docker | ||||||
|  |     repo: mosadxyz/tcor | ||||||
|  |     tags: latest | ||||||
|  |     secrets: | ||||||
|  |       - username: docker_username | ||||||
|  |         password: docker_password | ||||||
|  |     when: | ||||||
|  |       event: | ||||||
|  |         - tag | ||||||
							
								
								
									
										8
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | |||||||
|  | FROM adoptopenjdk/openjdk11:alpine-jre | ||||||
|  | RUN addgroup -S spring && adduser -S spring -G spring | ||||||
|  | #RUN groupadd -r spring && useradd -r -g spring spring # for openjdk:xx builds | ||||||
|  | USER spring:spring | ||||||
|  | ARG JAR_FILE=build/libs/*.jar | ||||||
|  | COPY ${JAR_FILE} thecitadelofricks.jar | ||||||
|  | ENTRYPOINT ["java","-Djavax.net.ssl.trustStore=/tcor/cacerts", "-Djavax.net.ssl.trustStorePassword=changeit", "-jar","/thecitadelofricks.jar"] | ||||||
|  | VOLUME /tcor | ||||||
| @ -1,4 +1,5 @@ | |||||||
| [](https://drone.mosad.xyz/Seil0/TheCitadelofRicks) |  | ||||||
|  | [](https://ci.mosad.xyz/Seil0/TheCitadelofRicks) | ||||||
| [](https://git.mosad.xyz/Seil0/TheCitadelofRicks/releases) | [](https://git.mosad.xyz/Seil0/TheCitadelofRicks/releases) | ||||||
| [](https://www.gnu.org/licenses/gpl-3.0) | [](https://www.gnu.org/licenses/gpl-3.0) | ||||||
| # TheCitadelofRicks | # TheCitadelofRicks | ||||||
|  | |||||||
							
								
								
									
										33
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								build.gradle
									
									
									
									
									
								
							| @ -1,26 +1,25 @@ | |||||||
| plugins { | plugins { | ||||||
|     id 'org.jetbrains.kotlin.jvm' version '1.3.70' |     id 'org.jetbrains.kotlin.jvm' version '1.5.31' | ||||||
|     id 'org.jetbrains.kotlin.plugin.spring' version '1.3.70' |     id 'org.jetbrains.kotlin.plugin.spring' version '1.5.31' | ||||||
|     id 'org.springframework.boot' version '2.2.5.RELEASE' |     id 'org.springframework.boot' version '2.5.5' | ||||||
|     id 'io.spring.dependency-management' version '1.0.9.RELEASE' |     id 'io.spring.dependency-management' version '1.0.11.RELEASE' | ||||||
| } | } | ||||||
|  |  | ||||||
| group 'org.mosad' | group 'org.mosad' | ||||||
| version '1.2.2' | version '1.2.7' | ||||||
|  |  | ||||||
| repositories { | repositories { | ||||||
|     jcenter() |     mavenCentral() | ||||||
| } | } | ||||||
|  |  | ||||||
| dependencies { | dependencies { | ||||||
|     implementation 'org.springframework.boot:spring-boot-starter-web' |     implementation 'org.springframework.boot:spring-boot-starter-web' | ||||||
|     implementation 'org.jetbrains.kotlin:kotlin-reflect' |  | ||||||
|     implementation 'org.jetbrains.kotlin:kotlin-stdlib' |     implementation 'org.jetbrains.kotlin:kotlin-stdlib' | ||||||
|     implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3' |     implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' | ||||||
|     implementation 'org.jsoup:jsoup:1.12.2' |     implementation 'org.jsoup:jsoup:1.14.3' | ||||||
|     implementation 'com.google.code.gson:gson:2.8.6' |     implementation 'com.google.code.gson:gson:2.8.8' | ||||||
|  |  | ||||||
|     testImplementation 'org.junit.jupiter:junit-jupiter:5.6.0' |     testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1' | ||||||
| } | } | ||||||
|  |  | ||||||
| test { | test { | ||||||
| @ -31,9 +30,17 @@ test { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | def jvmTargetVersion = "11" | ||||||
| compileKotlin { | compileKotlin { | ||||||
|     kotlinOptions.jvmTarget = "11" |     kotlinOptions.jvmTarget = jvmTargetVersion | ||||||
|  |     kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" | ||||||
|  | } | ||||||
|  | compileJava { | ||||||
|  |     targetCompatibility = jvmTargetVersion | ||||||
| } | } | ||||||
| compileTestKotlin { | compileTestKotlin { | ||||||
|     kotlinOptions.jvmTarget = "11" |     kotlinOptions.jvmTarget = jvmTargetVersion | ||||||
|  | } | ||||||
|  | compileTestJava { | ||||||
|  |     targetCompatibility = jvmTargetVersion | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										
											BIN
										
									
								
								gradle/wrapper/gradle-wrapper.jar
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								gradle/wrapper/gradle-wrapper.jar
									
									
									
									
										vendored
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							| @ -1,5 +1,5 @@ | |||||||
| distributionBase=GRADLE_USER_HOME | distributionBase=GRADLE_USER_HOME | ||||||
| distributionPath=wrapper/dists | distributionPath=wrapper/dists | ||||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip | ||||||
| zipStoreBase=GRADLE_USER_HOME | zipStoreBase=GRADLE_USER_HOME | ||||||
| zipStorePath=wrapper/dists | zipStorePath=wrapper/dists | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								gradlew
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								gradlew
									
									
									
									
										vendored
									
									
								
							| @ -82,6 +82,7 @@ esac | |||||||
|  |  | ||||||
| CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar | ||||||
|  |  | ||||||
|  |  | ||||||
| # Determine the Java command to use to start the JVM. | # Determine the Java command to use to start the JVM. | ||||||
| if [ -n "$JAVA_HOME" ] ; then | if [ -n "$JAVA_HOME" ] ; then | ||||||
|     if [ -x "$JAVA_HOME/jre/sh/java" ] ; then |     if [ -x "$JAVA_HOME/jre/sh/java" ] ; then | ||||||
| @ -129,6 +130,7 @@ fi | |||||||
| if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then | ||||||
|     APP_HOME=`cygpath --path --mixed "$APP_HOME"` |     APP_HOME=`cygpath --path --mixed "$APP_HOME"` | ||||||
|     CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` |     CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` | ||||||
|  |      | ||||||
|     JAVACMD=`cygpath --unix "$JAVACMD"` |     JAVACMD=`cygpath --unix "$JAVACMD"` | ||||||
|  |  | ||||||
|     # We build the pattern for arguments to be converted via cygpath |     # We build the pattern for arguments to be converted via cygpath | ||||||
|  | |||||||
							
								
								
									
										4
									
								
								gradlew.bat
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								gradlew.bat
									
									
									
									
										vendored
									
									
								
							| @ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=. | |||||||
| set APP_BASE_NAME=%~n0 | set APP_BASE_NAME=%~n0 | ||||||
| set APP_HOME=%DIRNAME% | 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. | @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" | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" | ||||||
|  |  | ||||||
| @ -81,6 +84,7 @@ set CMD_LINE_ARGS=%* | |||||||
|  |  | ||||||
| set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar | ||||||
|  |  | ||||||
|  |  | ||||||
| @rem Execute Gradle | @rem Execute Gradle | ||||||
| "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% | ||||||
|  |  | ||||||
|  | |||||||
| @ -40,6 +40,7 @@ import org.springframework.web.bind.annotation.RequestParam | |||||||
| import org.springframework.web.bind.annotation.RestController | import org.springframework.web.bind.annotation.RestController | ||||||
| import java.time.LocalDateTime | import java.time.LocalDateTime | ||||||
| import java.util.* | import java.util.* | ||||||
|  | import kotlin.collections.ArrayList | ||||||
|  |  | ||||||
| @RestController | @RestController | ||||||
| class APIController { | class APIController { | ||||||
| @ -47,8 +48,8 @@ class APIController { | |||||||
|     private val logger: Logger = LoggerFactory.getLogger(APIController::class.java) |     private val logger: Logger = LoggerFactory.getLogger(APIController::class.java) | ||||||
|  |  | ||||||
|     companion object { |     companion object { | ||||||
|         const val apiVersion = "1.1.4" |         const val apiVersion = "1.2.0" | ||||||
|         const val softwareVersion = "1.2.2" |         const val softwareVersion = "1.2.8" | ||||||
|         val startTime = System.currentTimeMillis() / 1000 |         val startTime = System.currentTimeMillis() / 1000 | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @ -57,18 +58,12 @@ class APIController { | |||||||
|         CacheController() |         CacheController() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // TODO remove this with API version 2.0.0 |  | ||||||
|     @Deprecated("courses is replaced by courseList", replaceWith = ReplaceWith("courseList()")) |  | ||||||
|     @RequestMapping("/courses") |  | ||||||
|     fun courses(): CoursesList { |  | ||||||
|         return courseList() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @RequestMapping("/courseList") |     @RequestMapping("/courseList") | ||||||
|     fun courseList(): CoursesList { |     fun courseList(): CoursesListRet { | ||||||
|         logger.info("courseList request at ${LocalDateTime.now()}!") |         logger.info("courseList request at ${LocalDateTime.now()}!") | ||||||
|         updateCourseListRequests() |         updateCourseListRequests() | ||||||
|         return courseList |  | ||||||
|  |         return CoursesListRet(courseList.meta, ArrayList(courseList.courses.values)) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @RequestMapping("/mensamenu") |     @RequestMapping("/mensamenu") | ||||||
| @ -80,7 +75,7 @@ class APIController { | |||||||
|  |  | ||||||
|     @RequestMapping("/timetable") |     @RequestMapping("/timetable") | ||||||
|     fun timetable( |     fun timetable( | ||||||
|         @RequestParam(value = "courseName", defaultValue = "AI4") courseName: String, |         @RequestParam(value = "course", defaultValue = "AI4") courseName: String, | ||||||
|         @RequestParam(value = "week", defaultValue = "0") week: Int |         @RequestParam(value = "week", defaultValue = "0") week: Int | ||||||
|     ): TimetableCourseWeek { |     ): TimetableCourseWeek { | ||||||
|         logger.info("timetable request at ${LocalDateTime.now()}!") |         logger.info("timetable request at ${LocalDateTime.now()}!") | ||||||
| @ -88,20 +83,20 @@ class APIController { | |||||||
|         return getTimetable(courseName, week) |         return getTimetable(courseName, week) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @RequestMapping("/lessonSubjectList") |     @RequestMapping("/subjectList") | ||||||
|     fun lessonSubjectList( |     fun lessonSubjectList( | ||||||
|         @RequestParam(value = "courseName", defaultValue = "AI4") courseName: String, |         @RequestParam(value = "course", defaultValue = "AI4") courseName: String, | ||||||
|         @RequestParam(value = "week", defaultValue = "0") week: Int |         @RequestParam(value = "week", defaultValue = "0") week: Int | ||||||
|     ): HashSet<String> { |     ): HashSet<String> { | ||||||
|         logger.info("lessonSubjectList request at ${LocalDateTime.now()}!") |         logger.info("subjectList request at ${LocalDateTime.now()}!") | ||||||
|         updateTimetableRequests(courseName) |         updateTimetableRequests(courseName) | ||||||
|         return getLessonSubjectList(courseName, week) |         return getLessonSubjectList(courseName, week) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @RequestMapping("/lessons") |     @RequestMapping("/lessons") | ||||||
|     fun lesson( |     fun lesson( | ||||||
|         @RequestParam(value = "courseName", defaultValue = "AI4") courseName: String, |         @RequestParam(value = "course", defaultValue = "AI4") courseName: String, | ||||||
|         @RequestParam(value = "lessonSubject", defaultValue = "Mathematik 4") lessonSubject: String, |         @RequestParam(value = "subject", defaultValue = "Mathematik 4") lessonSubject: String, | ||||||
|         @RequestParam(value = "week", defaultValue = "0") week: Int |         @RequestParam(value = "week", defaultValue = "0") week: Int | ||||||
|     ): ArrayList<Lesson> { |     ): ArrayList<Lesson> { | ||||||
|         logger.info("lesson request at ${LocalDateTime.now()}!") |         logger.info("lesson request at ${LocalDateTime.now()}!") | ||||||
| @ -121,4 +116,25 @@ class APIController { | |||||||
|         return 200 |         return 200 | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Deprecated section | ||||||
|  |      */ | ||||||
|  |  | ||||||
|  |     // TODO remove this with API version 2.0.0 | ||||||
|  |     @Deprecated("courses is replaced by courseList", replaceWith = ReplaceWith("courseList()")) | ||||||
|  |     @RequestMapping("/courses") | ||||||
|  |     fun courses(): CoursesListRet { | ||||||
|  |         return courseList() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // TODO remove this with API version 2.0.0 | ||||||
|  |     @Deprecated("the parameter courseName is deprecated please use course", ReplaceWith("timetable(courseName, week)")) | ||||||
|  |     @RequestMapping("/timetable", params= ["courseName", "week"]) | ||||||
|  |     fun timetableDep( | ||||||
|  |         @RequestParam(value = "courseName", defaultValue = "AI4") courseName: String, | ||||||
|  |         @RequestParam(value = "week", defaultValue = "0") week: Int | ||||||
|  |     ): TimetableCourseWeek { | ||||||
|  |        return timetable(courseName, week) | ||||||
|  |     } | ||||||
|  |  | ||||||
| } | } | ||||||
| @ -24,13 +24,15 @@ package org.mosad.thecitadelofricks | |||||||
|  |  | ||||||
| import java.time.LocalDateTime | import java.time.LocalDateTime | ||||||
| import java.util.* | import java.util.* | ||||||
|  | import kotlin.collections.HashMap | ||||||
|  |  | ||||||
| // data classes for the course part | // data classes for the course part | ||||||
| data class Course(val courseName: String, val courseLink: String) | data class Course(val courseName: String, val courseLink: String) | ||||||
|  |  | ||||||
| data class CoursesMeta(val updateTime: Long, val totalCourses: Int) | data class CoursesMeta(val updateTime: Long = 0, val totalCourses: Int = 0) | ||||||
|  |  | ||||||
| data class CoursesList(val meta: CoursesMeta, val courses: ArrayList<Course>) | data class CoursesList(val meta: CoursesMeta = CoursesMeta(), val courses: SortedMap<String, Course>) | ||||||
|  | data class CoursesListRet(val meta: CoursesMeta = CoursesMeta(), val courses: ArrayList<Course> = ArrayList()) | ||||||
|  |  | ||||||
| // data classes for the Mensa part | // data classes for the Mensa part | ||||||
| data class Meal(val day: String, val heading: String, val parts: ArrayList<String>, val additives: String) | data class Meal(val day: String, val heading: String, val parts: ArrayList<String>, val additives: String) | ||||||
| @ -56,13 +58,12 @@ data class TimetableDay(val timeslots: Array<ArrayList<Lesson>> = Array(6) { Arr | |||||||
|  |  | ||||||
| data class TimetableWeek(val days: Array<TimetableDay> = Array(6) { TimetableDay() }) | data class TimetableWeek(val days: Array<TimetableDay> = Array(6) { TimetableDay() }) | ||||||
|  |  | ||||||
| data class TimetableCourseMeta(var updateTime: Long = 0, val courseName: String = "", val weekIndex: Int = 0, val weekNumberYear: Int = 0, val link: String = "") | data class TimetableCourseMeta(var updateTime: Long = 0, val courseName: String = "", val weekIndex: Int = 0, var weekNumberYear: Int = 0, val link: String = "") | ||||||
|  |  | ||||||
| data class TimetableCourseWeek(val meta: TimetableCourseMeta = TimetableCourseMeta(), var timetable: TimetableWeek = TimetableWeek()) | data class TimetableCourseWeek(val meta: TimetableCourseMeta = TimetableCourseMeta(), var timetable: TimetableWeek = TimetableWeek()) | ||||||
|  |  | ||||||
|  |  | ||||||
| // data classes for the status part | // data classes for the status part | ||||||
| data class TimetableCounter(var courseName: String, var requests: Int) |  | ||||||
|  |  | ||||||
| data class Status( | data class Status( | ||||||
|     val time: LocalDateTime, |     val time: LocalDateTime, | ||||||
| @ -72,7 +73,7 @@ data class Status( | |||||||
|     val totalRequests: Int, |     val totalRequests: Int, | ||||||
|     val mensaMenuRequests: Int, |     val mensaMenuRequests: Int, | ||||||
|     val courseListRequests: Int, |     val courseListRequests: Int, | ||||||
|     val timetableRequests: ArrayList<TimetableCounter>, |     val timetableRequests: HashMap<String, Int>, | ||||||
|     val timetableListSize: Int, |     val timetableListSize: Int, | ||||||
|     val coursesLastUpdate: Date, |     val coursesLastUpdate: Date, | ||||||
|     val mensaLastUpdate: Date, |     val mensaLastUpdate: Date, | ||||||
|  | |||||||
| @ -23,24 +23,23 @@ | |||||||
| package org.mosad.thecitadelofricks.controller | package org.mosad.thecitadelofricks.controller | ||||||
|  |  | ||||||
| import com.google.gson.Gson | import com.google.gson.Gson | ||||||
| import kotlinx.coroutines.GlobalScope | import kotlinx.coroutines.* | ||||||
| import kotlinx.coroutines.async | import org.jsoup.Jsoup | ||||||
| import kotlinx.coroutines.launch |  | ||||||
| import kotlinx.coroutines.runBlocking |  | ||||||
| import org.mosad.thecitadelofricks.* | import org.mosad.thecitadelofricks.* | ||||||
| import org.mosad.thecitadelofricks.hsoparser.CourseListParser | import org.mosad.thecitadelofricks.hsoparser.CourseListParser | ||||||
| import org.mosad.thecitadelofricks.hsoparser.MensaParser | import org.mosad.thecitadelofricks.hsoparser.MensaParser | ||||||
| import org.mosad.thecitadelofricks.hsoparser.TimetableParser | import org.mosad.thecitadelofricks.hsoparser.TimetableParser | ||||||
| import org.slf4j.Logger | import org.slf4j.Logger | ||||||
| import org.slf4j.LoggerFactory | import org.slf4j.LoggerFactory | ||||||
| import java.io.BufferedWriter | import java.io.* | ||||||
| import java.io.File |  | ||||||
| import java.io.FileWriter |  | ||||||
| import java.util.* | import java.util.* | ||||||
| import java.util.concurrent.Executors | import java.util.concurrent.Executors | ||||||
| import kotlin.collections.ArrayList | import kotlin.collections.ArrayList | ||||||
|  | import kotlin.collections.HashMap | ||||||
| import kotlin.collections.HashSet | import kotlin.collections.HashSet | ||||||
| import kotlin.concurrent.scheduleAtFixedRate | import kotlin.concurrent.scheduleAtFixedRate | ||||||
|  | import kotlin.time.Duration | ||||||
|  | import kotlin.time.ExperimentalTime | ||||||
|  |  | ||||||
| class CacheController { | class CacheController { | ||||||
|  |  | ||||||
| @ -49,12 +48,12 @@ class CacheController { | |||||||
|         scheduledUpdates() |         scheduledUpdates() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     companion object{ |     companion object { | ||||||
|         private val logger: Logger = LoggerFactory.getLogger(CacheController::class.java) |         private val logger: Logger = LoggerFactory.getLogger(CacheController::class.java) | ||||||
|  |  | ||||||
|         var courseList = CoursesList(CoursesMeta(0, 0), ArrayList()) |         var courseList = CoursesList(CoursesMeta(), sortedMapOf()) | ||||||
|         var mensaMenu = MensaMenu(MensaMeta(0,""), MensaWeek(), MensaWeek()) |         var mensaMenu = MensaMenu(MensaMeta(0, ""), MensaWeek(), MensaWeek()) | ||||||
|         var timetableList = ArrayList<TimetableCourseWeek>() // this list contains all timetables |         var timetableList = HashMap<String, TimetableCourseWeek>() // this list contains all timetables | ||||||
|  |  | ||||||
|         /** |         /** | ||||||
|          * get a timetable, since they may not be cached, we need to make sure it's cached, otherwise download |          * get a timetable, since they may not be cached, we need to make sure it's cached, otherwise download | ||||||
| @ -62,31 +61,53 @@ class CacheController { | |||||||
|          * @param weekIndex request week number (current week = 0) |          * @param weekIndex request week number (current week = 0) | ||||||
|          * @return timetable of the course (Type: [TimetableCourseWeek]) |          * @return timetable of the course (Type: [TimetableCourseWeek]) | ||||||
|          */ |          */ | ||||||
|         fun getTimetable(courseName: String, weekIndex: Int): TimetableCourseWeek = runBlocking { |         fun getTimetable(courseName: String, weekIndex: Int): TimetableCourseWeek { | ||||||
|             val currentTime = System.currentTimeMillis() / 1000 |  | ||||||
|             var timetable = TimetableWeek() |  | ||||||
|             var weekNumberYear = 0 |  | ||||||
|  |  | ||||||
|             // check if the timetable already exists and is up to date |             // TODO just for testing | ||||||
|             when (timetableList.stream().filter { x -> x.meta.courseName == courseName && x.meta.weekIndex == weekIndex }.findAny().orElse(null)) { |             if (courseName == "TEST_A" || courseName == "TEST_B") { | ||||||
|                 // there is no such course yet, create one |                 val currentTime = System.currentTimeMillis() / 1000 | ||||||
|                 null -> { |                 val timetableLink = "https://mosad.xyz" | ||||||
|                     val courseLink = courseList.courses.stream().filter { x -> x.courseName == courseName }.findFirst().orElse(null).courseLink |                 val weekNumberYear = 0 | ||||||
|                     val timetableLink = courseLink.replace("week=0","week=$weekIndex") |                 val instr = CacheController::class.java.getResourceAsStream("/html/Timetable_normal-week.html") | ||||||
|  |  | ||||||
|                     val jobTimetable = async { |                 val timetableParser = | ||||||
|                         timetable = TimetableParser().getTimeTable(timetableLink) |                     TimetableParser(htmlDoc = Jsoup.parse(instr, "UTF-8", "https://www.hs-offenburg.de/")) | ||||||
|                         weekNumberYear = TimetableParser().getWeekNumberYear(timetableLink) |                 val timetableTest = timetableParser.parseTimeTable() | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     jobTimetable.await() |                 return TimetableCourseWeek( | ||||||
|                     timetableList.add(TimetableCourseWeek(TimetableCourseMeta(currentTime, courseName, weekIndex, weekNumberYear, timetableLink), timetable)) |                     TimetableCourseMeta( | ||||||
|  |                         currentTime, | ||||||
|                     logger.info("added new timetable for $courseName, week $weekIndex") |                         courseName, | ||||||
|                 } |                         weekIndex, | ||||||
|  |                         weekNumberYear, | ||||||
|  |                         timetableLink | ||||||
|  |                     ), timetableTest ?: TimetableWeek() | ||||||
|  |                 ) | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             return@runBlocking timetableList.stream().filter { x -> x.meta.courseName == courseName && x.meta.weekIndex == weekIndex }.findAny().orElse(null) |             val key = "$courseName-$weekIndex" | ||||||
|  |             return if (timetableList.contains(key)) { | ||||||
|  |                 timetableList[key]!! | ||||||
|  |             } else { | ||||||
|  |                 val timetableLink = courseList.courses[courseName] | ||||||
|  |                     ?.courseLink | ||||||
|  |                     ?.replace("week=0", "week=$weekIndex") ?: "" | ||||||
|  |                 val currentTime = System.currentTimeMillis() / 1000 | ||||||
|  |  | ||||||
|  |                 val timetableParser = TimetableParser(timetableLink) | ||||||
|  |                 val weekNumberYear = timetableParser.parseWeekNumberYear() | ||||||
|  |                 val timetable = timetableParser.parseTimeTable() | ||||||
|  |  | ||||||
|  |                 TimetableCourseWeek( | ||||||
|  |                     TimetableCourseMeta( | ||||||
|  |                         currentTime, | ||||||
|  |                         courseName, | ||||||
|  |                         weekIndex, | ||||||
|  |                         weekNumberYear ?: 0, | ||||||
|  |                         timetableLink | ||||||
|  |                     ), timetable ?: TimetableWeek() | ||||||
|  |                 ).also { timetableList[key] = it } | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         /** |         /** | ||||||
| @ -130,19 +151,23 @@ class CacheController { | |||||||
|  |  | ||||||
|         /** |         /** | ||||||
|          * this function updates the courseList |          * this function updates the courseList | ||||||
|          * during the update process the old data will be returned for a API request |          * during the update process the old data will be returned for an API request | ||||||
|          */ |          */ | ||||||
|         private fun asyncUpdateCourseList() = GlobalScope.launch { |         private fun asyncUpdateCourseList() = GlobalScope.launch { | ||||||
|             CourseListParser().getCourseLinks(StartupController.courseListURL)?.let { |             CourseListParser().getCourseLinks(StartupController.courseListURL)?.let { | ||||||
|                 courseList = CoursesList(CoursesMeta(System.currentTimeMillis() / 1000, it.size), it) |                 courseList = CoursesList(CoursesMeta(System.currentTimeMillis() / 1000, it.size), it.toSortedMap()) | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             // TODO just for testing | ||||||
|  |             courseList.courses["TEST_A"] = Course("TEST_A", "https://mosad.xyz") | ||||||
|  |             courseList.courses["TEST_B"] = Course("TEST_B", "https://mosad.xyz") | ||||||
|  |  | ||||||
|             logger.info("Updated courses successful at ${Date(courseList.meta.updateTime * 1000)}") |             logger.info("Updated courses successful at ${Date(courseList.meta.updateTime * 1000)}") | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         /** |         /** | ||||||
|          * this function updates the mensa menu list |          * this function updates the mensa menu list | ||||||
|          * during the update process the old data will be returned for a API request |          * during the update process the old data will be returned for an API request | ||||||
|          */ |          */ | ||||||
|         private fun asyncUpdateMensa() = GlobalScope.launch { |         private fun asyncUpdateMensa() = GlobalScope.launch { | ||||||
|             val mensaCurrentWeek = MensaParser().getMensaMenu(StartupController.mensaMenuURL) |             val mensaCurrentWeek = MensaParser().getMensaMenu(StartupController.mensaMenuURL) | ||||||
| @ -158,7 +183,7 @@ class CacheController { | |||||||
|  |  | ||||||
|         /** |         /** | ||||||
|          * this function updates all existing timetables |          * this function updates all existing timetables | ||||||
|          * during the update process the old data will be returned for a API request |          * during the update process the old data will be returned for an API request | ||||||
|          * a FixedThreadPool is used to make parallel requests for faster updates |          * a FixedThreadPool is used to make parallel requests for faster updates | ||||||
|          */ |          */ | ||||||
|         private fun asyncUpdateTimetables() = GlobalScope.launch { |         private fun asyncUpdateTimetables() = GlobalScope.launch { | ||||||
| @ -170,10 +195,13 @@ class CacheController { | |||||||
|             try { |             try { | ||||||
|                 timetableList.forEach { timetableCourse -> |                 timetableList.forEach { timetableCourse -> | ||||||
|                     executor.execute { |                     executor.execute { | ||||||
|                         timetableCourse.timetable = TimetableParser().getTimeTable(timetableCourse.meta.link) |                         val timetableParser = TimetableParser(timetableCourse.value.meta.link) | ||||||
|                         timetableCourse.meta.updateTime = System.currentTimeMillis() / 1000 |                         timetableCourse.value.timetable = timetableParser.parseTimeTable() ?: return@execute | ||||||
|  |                         timetableCourse.value.meta.weekNumberYear = | ||||||
|  |                             timetableParser.parseWeekNumberYear() ?: return@execute | ||||||
|  |                         timetableCourse.value.meta.updateTime = System.currentTimeMillis() / 1000 | ||||||
|  |  | ||||||
|                         saveTimetableToCache(timetableCourse) // save the updated timetable to the cache directory |                         saveTimetableToCache(timetableCourse.value) // save the updated timetable to the cache directory | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|                 } |                 } | ||||||
| @ -190,12 +218,16 @@ class CacheController { | |||||||
|          * @param timetable a timetable of the type [TimetableCourseWeek] |          * @param timetable a timetable of the type [TimetableCourseWeek] | ||||||
|          */ |          */ | ||||||
|         private fun saveTimetableToCache(timetable: TimetableCourseWeek) { |         private fun saveTimetableToCache(timetable: TimetableCourseWeek) { | ||||||
|             println(timetable.timetable.toString()) |  | ||||||
|  |  | ||||||
|             val file = File(StartupController.dirTcorCache, "timetable-${timetable.meta.courseName}-${timetable.meta.weekIndex}.json") |             val file = File(StartupController.dirTcorCache, "timetable-${timetable.meta.courseName}-${timetable.meta.weekIndex}.json") | ||||||
|             val writer = BufferedWriter(FileWriter(file)) |             val writer = BufferedWriter(FileWriter(file)) | ||||||
|             writer.write(Gson().toJson(timetable)) |  | ||||||
|             writer.close() |             try { | ||||||
|  |                 writer.write(Gson().toJson(timetable)) | ||||||
|  |             } catch (e: Exception) { | ||||||
|  |                 logger.error("something went wrong while trying to write a cache file", e) | ||||||
|  |             } finally { | ||||||
|  |                 writer.close() | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         /** |         /** | ||||||
| @ -217,30 +249,39 @@ class CacheController { | |||||||
|          * update the CourseList every 24h, the Timetables every 3h and the Mensa Menu every hour |          * update the CourseList every 24h, the Timetables every 3h and the Mensa Menu every hour | ||||||
|          * doesn't account the change between winter and summer time! |          * doesn't account the change between winter and summer time! | ||||||
|          */ |          */ | ||||||
|  |         @OptIn(ExperimentalTime::class) | ||||||
|         private fun scheduledUpdates() { |         private fun scheduledUpdates() { | ||||||
|             val currentTime = System.currentTimeMillis() |             val currentTime = System.currentTimeMillis() | ||||||
|             val initDelay24h = (86400000 - ((currentTime + 3600000) % 86400000)) + 60000 |  | ||||||
|             val initDelay3h = (10800000 - ((currentTime + 3600000) % 10800000)) + 60000 |             val duration24h = Duration.hours(24).inWholeMilliseconds | ||||||
|             val initDelay1h = (3600000 - ((currentTime + 3600000) % 3600000)) + 60000 |             val duration3h = Duration.hours(3).inWholeMilliseconds | ||||||
|  |             val duration1h = Duration.hours(1).inWholeMilliseconds | ||||||
|  |             val duration1m = Duration.minutes(1).inWholeMilliseconds | ||||||
|  |  | ||||||
|  |             // Calculate the initial delay to make the update time independent of the start time | ||||||
|  |             fun calcInitDelay(period: Long) = (period - ((currentTime + duration1h) % period)) + duration1m | ||||||
|  |             val initDelay24h = calcInitDelay(duration24h) | ||||||
|  |             val initDelay3h = calcInitDelay(duration3h) | ||||||
|  |             val initDelay1h = calcInitDelay(duration1h) | ||||||
|  |  | ||||||
|             // update courseList every 24 hours (time in ms) |             // update courseList every 24 hours (time in ms) | ||||||
|             Timer().scheduleAtFixedRate(initDelay24h, 86400000) { |             Timer().scheduleAtFixedRate(initDelay24h, duration24h) { | ||||||
|                 asyncUpdateCourseList() |                 asyncUpdateCourseList() | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             // update all already existing timetables every 3 hours (time in ms) |             // update all already existing timetables every 3 hours (time in ms) | ||||||
|             Timer().scheduleAtFixedRate(initDelay3h, 10800000) { |             Timer().scheduleAtFixedRate(initDelay3h, duration3h) { | ||||||
|                 asyncUpdateTimetables() |                 asyncUpdateTimetables() | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             // update courses every hour (time in ms) |             // update courses every hour (time in ms) | ||||||
|             Timer().scheduleAtFixedRate(initDelay1h, 3600000) { |             Timer().scheduleAtFixedRate(initDelay1h, duration1h) { | ||||||
|                 asyncUpdateMensa() |                 asyncUpdateMensa() | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             // post to status.mosad.xyz every hour, if an API key is present |             // post to status.mosad.xyz every hour, if an API key is present | ||||||
|             if (StartupController.cachetAPIKey != "0") { |             if (StartupController.cachetAPIKey != "0") { | ||||||
|                 Timer().scheduleAtFixedRate(initDelay1h, 3600000) { |                 Timer().scheduleAtFixedRate(initDelay1h, duration1h) { | ||||||
|                     CachetAPIController.postTotalRequests() |                     CachetAPIController.postTotalRequests() | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  | |||||||
| @ -22,7 +22,7 @@ | |||||||
|  |  | ||||||
| package org.mosad.thecitadelofricks.controller | package org.mosad.thecitadelofricks.controller | ||||||
|  |  | ||||||
| import org.mosad.thecitadelofricks.controller.StatusController.Companion.getTotalRequests | import org.mosad.thecitadelofricks.controller.StatusController.Companion.totalRequests | ||||||
| import org.slf4j.Logger | import org.slf4j.Logger | ||||||
| import org.slf4j.LoggerFactory | import org.slf4j.LoggerFactory | ||||||
| import java.io.BufferedReader | import java.io.BufferedReader | ||||||
| @ -42,8 +42,8 @@ class CachetAPIController { | |||||||
|          fun postTotalRequests() { |          fun postTotalRequests() { | ||||||
|              try { |              try { | ||||||
|                  val url = URL("${StartupController.cachetBaseURL}/api/v1/metrics/1/points") |                  val url = URL("${StartupController.cachetBaseURL}/api/v1/metrics/1/points") | ||||||
|                  val jsonInputString = "{\"value\": ${getTotalRequests() -oldTotalRequests}, \"timestamp\": \"${(System.currentTimeMillis() / 1000)}\"}" |                  val jsonInputString = "{\"value\": ${totalRequests -oldTotalRequests}, \"timestamp\": \"${(System.currentTimeMillis() / 1000)}\"}" | ||||||
|                  oldTotalRequests = getTotalRequests() |                  oldTotalRequests = totalRequests | ||||||
|  |  | ||||||
|                  val con = url.openConnection() as HttpURLConnection |                  val con = url.openConnection() as HttpURLConnection | ||||||
|                  con.requestMethod = "POST" |                  con.requestMethod = "POST" | ||||||
|  | |||||||
| @ -44,7 +44,7 @@ class StartupController { | |||||||
|         var cachetAPIKey = "0" |         var cachetAPIKey = "0" | ||||||
|         var cachetBaseURL = "https://status.mosad.xyz" |         var cachetBaseURL = "https://status.mosad.xyz" | ||||||
|         var courseListURL = "https://www.hs-offenburg.de/studium/vorlesungsplaene/" |         var courseListURL = "https://www.hs-offenburg.de/studium/vorlesungsplaene/" | ||||||
|         var mensaMenuURL = "https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/" |         var mensaMenuURL = "https://www.swfr.de/essen-trinken/speiseplaene/mensa-offenburg/" | ||||||
|         var mensaName = "Offenburg" |         var mensaName = "Offenburg" | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @ -92,7 +92,7 @@ class StartupController { | |||||||
|         mensaMenuURL = try { |         mensaMenuURL = try { | ||||||
|             properties.getProperty("mensaMenuURL") |             properties.getProperty("mensaMenuURL") | ||||||
|         } catch (ex: Exception) { |         } catch (ex: Exception) { | ||||||
|             "https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/" |             "https://www.swfr.de/essen-trinken/speiseplaene/mensa-offenburg/" | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         mensaName = try { |         mensaName = try { | ||||||
| @ -113,7 +113,7 @@ class StartupController { | |||||||
|  |  | ||||||
|         properties.setProperty("cachetAPIKey", "0") |         properties.setProperty("cachetAPIKey", "0") | ||||||
|         properties.setProperty("cachetBaseURL", "https://status.mosad.xyz") |         properties.setProperty("cachetBaseURL", "https://status.mosad.xyz") | ||||||
|         properties.setProperty("mensaMenuURL", "https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/") |         properties.setProperty("mensaMenuURL", "https://www.swfr.de/essen-trinken/speiseplaene/mensa-offenburg/") | ||||||
|         properties.setProperty("mensaName", "Offenburg") |         properties.setProperty("mensaName", "Offenburg") | ||||||
|  |  | ||||||
|         val outputStream = FileOutputStream(fileConfig) |         val outputStream = FileOutputStream(fileConfig) | ||||||
| @ -134,7 +134,9 @@ class StartupController { | |||||||
|  |  | ||||||
|                 try { |                 try { | ||||||
|                     val timetableObject = JsonParser.parseString(bufferedReader.readLine()).asJsonObject |                     val timetableObject = JsonParser.parseString(bufferedReader.readLine()).asJsonObject | ||||||
|                     CacheController.timetableList.add(Gson().fromJson(timetableObject, TimetableCourseWeek().javaClass)) |                     val timetable = Gson().fromJson(timetableObject, TimetableCourseWeek().javaClass) | ||||||
|  |                     CacheController.timetableList["${timetable.meta.courseName}-${timetable.meta.weekIndex}"] = | ||||||
|  |                         timetable | ||||||
|                 } catch (ex: Exception) { |                 } catch (ex: Exception) { | ||||||
|                     logger.error("error while reading cache", ex) |                     logger.error("error while reading cache", ex) | ||||||
|                 } finally { |                 } finally { | ||||||
|  | |||||||
| @ -26,24 +26,27 @@ import org.mosad.thecitadelofricks.APIController.Companion.apiVersion | |||||||
| import org.mosad.thecitadelofricks.APIController.Companion.softwareVersion | import org.mosad.thecitadelofricks.APIController.Companion.softwareVersion | ||||||
| import org.mosad.thecitadelofricks.APIController.Companion.startTime | import org.mosad.thecitadelofricks.APIController.Companion.startTime | ||||||
| import org.mosad.thecitadelofricks.Status | import org.mosad.thecitadelofricks.Status | ||||||
| import org.mosad.thecitadelofricks.TimetableCounter |  | ||||||
| import org.slf4j.Logger | import org.slf4j.Logger | ||||||
| import org.slf4j.LoggerFactory | import org.slf4j.LoggerFactory | ||||||
| import java.net.HttpURLConnection | import java.net.HttpURLConnection | ||||||
| import java.net.URL | import java.net.URL | ||||||
| import java.time.LocalDateTime | import java.time.LocalDateTime | ||||||
| import java.util.* | import java.util.* | ||||||
| import kotlin.collections.ArrayList | import kotlin.collections.HashMap | ||||||
|  |  | ||||||
| class StatusController { | class StatusController { | ||||||
|  |  | ||||||
|     companion object { |     companion object { | ||||||
|         private val logger: Logger = LoggerFactory.getLogger(StatusController::class.java) |         private val logger: Logger = LoggerFactory.getLogger(StatusController::class.java) | ||||||
|  |  | ||||||
|         private var totalRequests = 0 |         var totalRequests = 0 | ||||||
|         private var mensaMenuRequests = 0 |             private set | ||||||
|         private var courseListRequests = 0 |         var mensaMenuRequests = 0 | ||||||
|         private var timetableRequests = ArrayList<TimetableCounter>() |             private set | ||||||
|  |         var courseListRequests = 0 | ||||||
|  |             private set | ||||||
|  |         var timetableRequests = HashMap<String, Int>() | ||||||
|  |             private set | ||||||
|  |  | ||||||
|         /** |         /** | ||||||
|          * if a mensamenu/courseList/timetable is requested update the specific and total request count |          * if a mensamenu/courseList/timetable is requested update the specific and total request count | ||||||
| @ -59,33 +62,10 @@ class StatusController { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         fun updateTimetableRequests(courseName: String) { |         fun updateTimetableRequests(courseName: String) { | ||||||
|             timetableRequests.stream().filter { it.courseName == courseName }.findFirst().ifPresentOrElse({ |             timetableRequests[courseName] = (timetableRequests[courseName] ?: 0) + 1 | ||||||
|                 it.requests++ |  | ||||||
|             }, { |  | ||||||
|                 timetableRequests.add(TimetableCounter(courseName, 1)) |  | ||||||
|             }) |  | ||||||
|             totalRequests++ |             totalRequests++ | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         /** |  | ||||||
|          * getters and setters |  | ||||||
|          */ |  | ||||||
|         fun getTotalRequests(): Int { |  | ||||||
|             return totalRequests |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         fun getMensaMenuRequests(): Int { |  | ||||||
|             return mensaMenuRequests |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         fun getCourseListRequests(): Int { |  | ||||||
|             return courseListRequests |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         fun getTimetableRequests(): ArrayList<TimetableCounter> { |  | ||||||
|             return timetableRequests |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         fun getStatus(): Status { |         fun getStatus(): Status { | ||||||
|             val currentTime = System.currentTimeMillis() / 1000 |             val currentTime = System.currentTimeMillis() / 1000 | ||||||
|             val minutes = (currentTime - startTime) % 3600 / 60 |             val minutes = (currentTime - startTime) % 3600 / 60 | ||||||
| @ -117,10 +97,10 @@ class StatusController { | |||||||
|                 "$days days, $hours:$minutes", |                 "$days days, $hours:$minutes", | ||||||
|                 apiVersion, |                 apiVersion, | ||||||
|                 softwareVersion, |                 softwareVersion, | ||||||
|                 getTotalRequests(), |                 totalRequests, | ||||||
|                 getMensaMenuRequests(), |                 mensaMenuRequests, | ||||||
|                 getCourseListRequests(), |                 courseListRequests, | ||||||
|                 getTimetableRequests(), |                 timetableRequests, | ||||||
|                 CacheController.timetableList.size, |                 CacheController.timetableList.size, | ||||||
|                 Date(CacheController.courseList.meta.updateTime * 1000), |                 Date(CacheController.courseList.meta.updateTime * 1000), | ||||||
|                 Date(CacheController.mensaMenu.meta.updateTime * 1000), |                 Date(CacheController.mensaMenu.meta.updateTime * 1000), | ||||||
|  | |||||||
| @ -29,26 +29,25 @@ import java.net.SocketTimeoutException | |||||||
|  |  | ||||||
| class CourseListParser { | class CourseListParser { | ||||||
|  |  | ||||||
|     var logger: org.slf4j.Logger = LoggerFactory.getLogger(MensaParser::class.java) |     private var logger: org.slf4j.Logger = LoggerFactory.getLogger(CourseListParser::class.java) | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * return a list of all courses at courseListURL |      * return a list of all courses at courseListURL | ||||||
|      * @param courseListURL the url to the course list page |      * @param courseListURL the url to the course list page | ||||||
|      * @return a ArrayList<Course> with all courses or null if the request was not successful |      * @return a ArrayList<Course> with all courses or null if the request was not successful | ||||||
|      */ |      */ | ||||||
|     fun getCourseLinks(courseListURL: String): ArrayList<Course>? { |     fun getCourseLinks(courseListURL: String): HashMap<String, Course>? { | ||||||
|         val courseLinkList = ArrayList<Course>() |         val courseLinkList = HashMap<String, Course>() | ||||||
|         try { |         try { | ||||||
|             val courseHTML = Jsoup.connect(courseListURL).get() |             val courseHTML = Jsoup.connect(courseListURL).get() | ||||||
|  |  | ||||||
|             courseHTML.select("ul.index-group").select("li.Class").select("a[href]").forEachIndexed { _, element -> |             courseHTML.select("ul.index-group").select("li.Class").select("a[href]").forEachIndexed { _, element -> | ||||||
|                 courseLinkList.add( |                 courseLinkList[element.text()] = Course( | ||||||
|                     Course( |                     element.text(), | ||||||
|                         element.text(), |                     element.attr("href").replace("http", "https") | ||||||
|                         element.attr("href").replace("http", "https") |  | ||||||
|                     ) |  | ||||||
|                 ) |                 ) | ||||||
|             } |             } | ||||||
|  |             logger.info("successfully retrieved course List") | ||||||
|         } catch (ex: SocketTimeoutException) { |         } catch (ex: SocketTimeoutException) { | ||||||
|             logger.warn("timeout from hs-offenburg.de, updating on next attempt!") |             logger.warn("timeout from hs-offenburg.de, updating on next attempt!") | ||||||
|             return null |             return null | ||||||
|  | |||||||
| @ -28,26 +28,37 @@ import org.mosad.thecitadelofricks.Lesson | |||||||
| import org.mosad.thecitadelofricks.TimetableWeek | import org.mosad.thecitadelofricks.TimetableWeek | ||||||
| import org.slf4j.LoggerFactory | import org.slf4j.LoggerFactory | ||||||
|  |  | ||||||
| class TimetableParser { | /** | ||||||
|  |  * @param timetableURL the URL of the timetable you want to get | ||||||
|  |  * @param htmlDoc the html document to use (the timetableURL will be ignored if this value is present) | ||||||
|  |  */ | ||||||
|  | class TimetableParser(timetableURL: String? = null, htmlDoc: Document? = null) { | ||||||
|     private var logger: org.slf4j.Logger = LoggerFactory.getLogger(TimetableParser::class.java) |     private var logger: org.slf4j.Logger = LoggerFactory.getLogger(TimetableParser::class.java) | ||||||
|     private val days = arrayOf("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday") |     private val days = arrayOf("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday") | ||||||
|  |  | ||||||
|  |     private val htmlDoc: Document? = | ||||||
|  |         htmlDoc | ||||||
|  |             ?: if (timetableURL == null) { | ||||||
|  |                 null | ||||||
|  |             } else { | ||||||
|  |                 try { | ||||||
|  |                     Jsoup.connect(timetableURL).get() | ||||||
|  |                 } catch (gex: Exception) { | ||||||
|  |                     logger.error("general TimetableParser error", gex) | ||||||
|  |                     null | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * get the timetable from the given url |      * parse the timetable from the previously given url | ||||||
|      * the timetable is organised per row not per column; |      * the timetable is organised per row not per column; | ||||||
|      * Mon 1, Tue 1, Wed 1, Thur 1, Fri 1, Sat 1, Mon 2 and so on |      * Mon 1, Tue 1, Wed 1, Thur 1, Fri 1, Sat 1, Mon 2 and so on | ||||||
|      * @param timetableURL the URL of the timetable you want to get |  | ||||||
|      */ |      */ | ||||||
|     fun getTimeTable(timetableURL: String): TimetableWeek { |     fun parseTimeTable(): TimetableWeek? { | ||||||
|         return try { |         if (htmlDoc == null) { | ||||||
|             parseTimeTable(Jsoup.connect(timetableURL).get()) |             return null | ||||||
|         } catch (gex: Exception) { |  | ||||||
|             logger.error("general TimetableParser error", gex) |  | ||||||
|             TimetableWeek() |  | ||||||
|         } |         } | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun parseTimeTable(htmlDoc: Document): TimetableWeek { |  | ||||||
|         val timetableWeek = TimetableWeek() |         val timetableWeek = TimetableWeek() | ||||||
|         val rows = htmlDoc.select("table.timetable").select("tr[scope=\"row\"]") |         val rows = htmlDoc.select("table.timetable").select("tr[scope=\"row\"]") | ||||||
|  |  | ||||||
| @ -61,7 +72,7 @@ class TimetableParser { | |||||||
|             var lessonIndexDay = 0 // the index of the lesson per timeslot |             var lessonIndexDay = 0 // the index of the lesson per timeslot | ||||||
|  |  | ||||||
|             // elements are now all lessons, including empty ones |             // elements are now all lessons, including empty ones | ||||||
|             row.select("td.lastcol, td[style]").forEach {element -> |             row.select("td.lastcol, td[style]").forEach { element -> | ||||||
|  |  | ||||||
|                 // if there is a lecture with rowspan="2", we need to shift everything by one to the left. This is stupid and ugly there needs to bee an API |                 // if there is a lecture with rowspan="2", we need to shift everything by one to the left. This is stupid and ugly there needs to bee an API | ||||||
|                 if ((sDay > -1 && sRow > -1) && (sDay == day && ((sRow + 1) == rowIndex))) { |                 if ((sDay > -1 && sRow > -1) && (sDay == day && ((sRow + 1) == rowIndex))) { | ||||||
| @ -104,8 +115,7 @@ class TimetableParser { | |||||||
|  |  | ||||||
|                 lessonIndexDay++ |                 lessonIndexDay++ | ||||||
|  |  | ||||||
|                 if (element.hasClass("lastcol")) |                 if (element.hasClass("lastcol")) { | ||||||
|                 { |  | ||||||
|                     day++ |                     day++ | ||||||
|                     lessonIndexDay = 0 |                     lessonIndexDay = 0 | ||||||
|                 } |                 } | ||||||
| @ -117,19 +127,13 @@ class TimetableParser { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * get the week number of the year for the timetable |      * parse the week number of the year for the timetable | ||||||
|      * @param timetableURL the URL of the timetable you want to get |  | ||||||
|      */ |      */ | ||||||
|     fun getWeekNumberYear(timetableURL: String): Int { |     fun parseWeekNumberYear(): Int? { | ||||||
|         return try { |         if (htmlDoc == null) { | ||||||
|             parseWeekNumberYear(Jsoup.connect(timetableURL).get()) |             return null | ||||||
|         } catch (gex: Exception) { |  | ||||||
|             logger.error("general TimetableParser error", gex) |  | ||||||
|             0 |  | ||||||
|         } |         } | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun parseWeekNumberYear(htmlDoc: Document): Int { |  | ||||||
|         return htmlDoc.select("h1.timetable-caption").text().substringAfter("- ") |         return htmlDoc.select("h1.timetable-caption").text().substringAfter("- ") | ||||||
|             .substringBefore(".").replace(" ", "").toInt() |             .substringBefore(".").replace(" ", "").toInt() | ||||||
|     } |     } | ||||||
| @ -181,4 +185,5 @@ class TimetableParser { | |||||||
|  |  | ||||||
|         println(" \n") |         println(" \n") | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
| @ -11,5 +11,5 @@ logging.level.org.springframework.web=INFO | |||||||
| # ---------------------------------------- | # ---------------------------------------- | ||||||
|  |  | ||||||
| # EMBEDDED SERVER CONFIGURATION (ServerProperties) | # EMBEDDED SERVER CONFIGURATION (ServerProperties) | ||||||
| server.address=127.0.0.1 | server.address=0.0.0.0 | ||||||
| server.port=8080 | server.port=8080 | ||||||
							
								
								
									
										1726
									
								
								src/main/resources/html/Timetable_normal-week.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1726
									
								
								src/main/resources/html/Timetable_normal-week.html
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @ -28,7 +28,7 @@ import org.junit.jupiter.api.Test | |||||||
| import java.io.File | import java.io.File | ||||||
|  |  | ||||||
| internal class MensaParserTest { | internal class MensaParserTest { | ||||||
|     private val mensaMenuURL = "https://www.swfr.de/de/essen-trinken/speiseplaene/mensa-offenburg/" |     private val mensaMenuURL = "https://www.swfr.de/essen-trinken/speiseplaene/mensa-offenburg/" | ||||||
|  |  | ||||||
|     @Test |     @Test | ||||||
|     fun parseMensaMenuNormalWeek() { |     fun parseMensaMenuNormalWeek() { | ||||||
|  | |||||||
| @ -32,8 +32,8 @@ class TimetableParserTest { | |||||||
|     @Test |     @Test | ||||||
|     fun parseTimetableNormalWeek() { |     fun parseTimetableNormalWeek() { | ||||||
|         val htmlFile = File(TimetableParserTest::class.java.getResource("/html/Timetable_normal-week.html").path) |         val htmlFile = File(TimetableParserTest::class.java.getResource("/html/Timetable_normal-week.html").path) | ||||||
|         val htmlDoc = Jsoup.parse(htmlFile,"UTF-8", "https://www.hs-offenburg.de/") |         val htmlDoc = Jsoup.parse(htmlFile, "UTF-8", "https://www.hs-offenburg.de/") | ||||||
|         val actualTimetable = TimetableParser().parseTimeTable(htmlDoc).toString().trim() |         val actualTimetable = TimetableParser(htmlDoc = htmlDoc).parseTimeTable().toString().trim() | ||||||
|         val expectedTimetable = TimetableParserTest::class.java.getResource("/expected/Timetable_normal-week.txt").readText().trim() |         val expectedTimetable = TimetableParserTest::class.java.getResource("/expected/Timetable_normal-week.txt").readText().trim() | ||||||
|  |  | ||||||
|         Assertions.assertEquals(expectedTimetable, actualTimetable) |         Assertions.assertEquals(expectedTimetable, actualTimetable) | ||||||
| @ -42,8 +42,8 @@ class TimetableParserTest { | |||||||
|     @Test |     @Test | ||||||
|     fun parseTimetableEmptyWeek() { |     fun parseTimetableEmptyWeek() { | ||||||
|         val htmlFile = File(TimetableParserTest::class.java.getResource("/html/Timetable_empty-week.html").path) |         val htmlFile = File(TimetableParserTest::class.java.getResource("/html/Timetable_empty-week.html").path) | ||||||
|         val htmlDoc = Jsoup.parse(htmlFile,"UTF-8", "https://www.hs-offenburg.de/") |         val htmlDoc = Jsoup.parse(htmlFile, "UTF-8", "https://www.hs-offenburg.de/") | ||||||
|         val actualTimetable = TimetableParser().parseTimeTable(htmlDoc).toString().trim() |         val actualTimetable = TimetableParser(htmlDoc = htmlDoc).parseTimeTable().toString().trim() | ||||||
|         val expectedTimetable = TimetableParserTest::class.java.getResource("/expected/Timetable_empty-week.txt").readText().trim() |         val expectedTimetable = TimetableParserTest::class.java.getResource("/expected/Timetable_empty-week.txt").readText().trim() | ||||||
|  |  | ||||||
|         Assertions.assertEquals(expectedTimetable, actualTimetable) |         Assertions.assertEquals(expectedTimetable, actualTimetable) | ||||||
| @ -52,8 +52,8 @@ class TimetableParserTest { | |||||||
|     @Test |     @Test | ||||||
|     fun parseWeekNumberYear() { |     fun parseWeekNumberYear() { | ||||||
|         val htmlFile = File(TimetableParserTest::class.java.getResource("/html/Timetable_normal-week.html").path) |         val htmlFile = File(TimetableParserTest::class.java.getResource("/html/Timetable_normal-week.html").path) | ||||||
|         val htmlDoc = Jsoup.parse(htmlFile,"UTF-8", "https://www.hs-offenburg.de/") |         val htmlDoc = Jsoup.parse(htmlFile, "UTF-8", "https://www.hs-offenburg.de/") | ||||||
|         val actualWeekNumberYear = TimetableParser().parseWeekNumberYear(htmlDoc) |         val actualWeekNumberYear = TimetableParser(htmlDoc = htmlDoc).parseWeekNumberYear() | ||||||
|  |  | ||||||
|         Assertions.assertEquals(42, actualWeekNumberYear) |         Assertions.assertEquals(42, actualWeekNumberYear) | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| <div id="speiseplan-tabs"> | <div id="speiseplan-tabs"> | ||||||
|     <div id="tab-menu-container" class="row"> |     <div id="tab-menu-container" class="row"> | ||||||
|         <div class="col-md-1 col-xs-6"> |         <div class="col-md-1 col-xs-6"> | ||||||
|             <a class="prev-week" title="eine Woche zurück" href="/de/essen-trinken/speiseplaene/mensa-offenburg/?tx_swfrspeiseplan_pi1%5BweekToShow%5D=0&tx_swfrspeiseplan_pi1%5Baction%5D=show&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=ad074b11cc01a680e4c0d0675dd1f91c"><img src="/fileadmin/templates/images/keil-grau-links.png" width="16" height="16" alt="" /><span class="d-inline d-sm-none">vorherige Woche</span></a> |             <a class="prev-week" title="eine Woche zurück" href="/essen-trinken/speiseplaene/mensa-offenburg/?tx_swfrspeiseplan_pi1%5BweekToShow%5D=0&tx_swfrspeiseplan_pi1%5Baction%5D=show&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=ad074b11cc01a680e4c0d0675dd1f91c"><img src="/fileadmin/templates/images/keil-grau-links.png" width="16" height="16" alt="" /><span class="d-inline d-sm-none">vorherige Woche</span></a> | ||||||
|         </div> |         </div> | ||||||
|         <div class="col-md-10"> |         <div class="col-md-10"> | ||||||
|             <ul class="nav classic-tabs tabs-primary" role="tablist"> |             <ul class="nav classic-tabs tabs-primary" role="tablist"> | ||||||
| @ -13,7 +13,7 @@ | |||||||
|                 <li class="col-md-2"><a href="#tab-sat" class="nav-link" data-toggle="tab" role="tab">Sa 14.03.</a></li> |                 <li class="col-md-2"><a href="#tab-sat" class="nav-link" data-toggle="tab" role="tab">Sa 14.03.</a></li> | ||||||
|             </ul> |             </ul> | ||||||
|         </div> |         </div> | ||||||
|         <div class="col-md-1 col-xs-6 pull-right"><a class="next-week text-right" title="eine Woche weiter" href="/de/essen-trinken/speiseplaene/mensa-offenburg/?tx_swfrspeiseplan_pi1%5BweekToShow%5D=2&tx_swfrspeiseplan_pi1%5Baction%5D=show&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=3c3048ebdd7f42a104bd780a0a6a0fd1"><span class="d-inline d-sm-none">nächste Woche</span><img src="/fileadmin/templates/images/keil-grau-rechts.png" width="16" height="16" alt="" /></a></div> |         <div class="col-md-1 col-xs-6 pull-right"><a class="next-week text-right" title="eine Woche weiter" href="/essen-trinken/speiseplaene/mensa-offenburg/?tx_swfrspeiseplan_pi1%5BweekToShow%5D=2&tx_swfrspeiseplan_pi1%5Baction%5D=show&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=3c3048ebdd7f42a104bd780a0a6a0fd1"><span class="d-inline d-sm-none">nächste Woche</span><img src="/fileadmin/templates/images/keil-grau-rechts.png" width="16" height="16" alt="" /></a></div> | ||||||
|     </div> |     </div> | ||||||
|     <div class="row"> |     <div class="row"> | ||||||
|         <div class="col-sm-4"> |         <div class="col-sm-4"> | ||||||
| @ -34,12 +34,12 @@ | |||||||
|         </div> |         </div> | ||||||
|         <div class="col-sm-4 pt-3"> |         <div class="col-sm-4 pt-3"> | ||||||
|             <div class="hide-with-allergenes"> |             <div class="hide-with-allergenes"> | ||||||
|                 <p><a download="Mensa_Offenburg_KW_11-2020.pdf" title="Wochenplan in Farbe herunterladen" target="_blank" href="/de/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=1&tx_swfrspeiseplan_pi1%5Bcolored%5D=1&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdf&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=4de091579c564e42f79473da2701f179"><i class="fas fa-file-download" aria-hidden="true"></i> Wochenplan farbig</a></p> |                 <p><a download="Mensa_Offenburg_KW_11-2020.pdf" title="Wochenplan in Farbe herunterladen" target="_blank" href="/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=1&tx_swfrspeiseplan_pi1%5Bcolored%5D=1&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdf&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=4de091579c564e42f79473da2701f179"><i class="fas fa-file-download" aria-hidden="true"></i> Wochenplan farbig</a></p> | ||||||
|                 <p><a download="Mensa_Offenburg_KW_11-2020.pdf" title="Wochenplan in schwarz-weiß herunterladen" target="_blank" href="/de/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=1&tx_swfrspeiseplan_pi1%5Bcolored%5D=0&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdf&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=84edf4562bdc95f103b70ef99e4d7af6"><i class="fas fa-file-download" aria-hidden="true"></i> Wochenplan s/w</a></p> |                 <p><a download="Mensa_Offenburg_KW_11-2020.pdf" title="Wochenplan in schwarz-weiß herunterladen" target="_blank" href="/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=1&tx_swfrspeiseplan_pi1%5Bcolored%5D=0&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdf&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=84edf4562bdc95f103b70ef99e4d7af6"><i class="fas fa-file-download" aria-hidden="true"></i> Wochenplan s/w</a></p> | ||||||
|             </div> |             </div> | ||||||
|             <div class="show-with-allergenes"> |             <div class="show-with-allergenes"> | ||||||
|                 <p><a download="Mensa_Offenburg_KW_11-2020.pdf" title="Wochenplan in Farbe herunterladen" target="_blank" href="/de/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=1&tx_swfrspeiseplan_pi1%5Bcolored%5D=1&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdfAllergenes&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=51f0f1e0083f9ef48038920dfb676eb6"><i class="fas fa-file-download" aria-hidden="true"></i> Wochenplan farbig</a></p> |                 <p><a download="Mensa_Offenburg_KW_11-2020.pdf" title="Wochenplan in Farbe herunterladen" target="_blank" href="/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=1&tx_swfrspeiseplan_pi1%5Bcolored%5D=1&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdfAllergenes&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=51f0f1e0083f9ef48038920dfb676eb6"><i class="fas fa-file-download" aria-hidden="true"></i> Wochenplan farbig</a></p> | ||||||
|                 <p><a download="Mensa_Offenburg_KW_11-2020.pdf" title="Wochenplan in schwarz-weiß herunterladen" target="_blank" href="/de/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=1&tx_swfrspeiseplan_pi1%5Bcolored%5D=0&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdfAllergenes&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=c3a9d1eafce78d8f8d281c6060be989c"><i class="fas fa-file-download" aria-hidden="true"></i> Wochenplan s/w</a></p> |                 <p><a download="Mensa_Offenburg_KW_11-2020.pdf" title="Wochenplan in schwarz-weiß herunterladen" target="_blank" href="/essen-trinken/speiseplaene/mensa-offenburg/?type=99&tx_swfrspeiseplan_pi1%5BweekToShow%5D=1&tx_swfrspeiseplan_pi1%5Bcolored%5D=0&tx_swfrspeiseplan_pi1%5Bort%5D=651&tx_swfrspeiseplan_pi1%5Baction%5D=buildPdfAllergenes&tx_swfrspeiseplan_pi1%5Bcontroller%5D=Speiseplan&cHash=c3a9d1eafce78d8f8d281c6060be989c"><i class="fas fa-file-download" aria-hidden="true"></i> Wochenplan s/w</a></p> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|         <div class="col-sm-4 pt-3"> |         <div class="col-sm-4 pt-3"> | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user