Skip to content

Commit

Permalink
Refactor CanteenApi & add MRI
Browse files Browse the repository at this point in the history
  • Loading branch information
dfuchss committed Aug 10, 2024
1 parent e04d3fa commit 398d062
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 61 deletions.
2 changes: 2 additions & 0 deletions src/main/kotlin/org/fuchss/matrix/mensa/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import java.util.Date
* @param[timeToSendUpdates] the time the bot shall send updates about the meals every day (to subscribed rooms)
* @param[admins] the matrix ids of the admins. E.g. "@user:invalid.domain"
* @param[subscribers] the room ids of rooms that subscribed updates
* @param[canteensForSubscribers] the canteens the bot shall provide information about (only affects subscribers)
* @param[translation] the configuration for translations (optional, alpha)
*/
data class Config(
Expand All @@ -39,6 +40,7 @@ data class Config(
@JsonProperty override val users: List<String> = listOf(),
@JsonProperty val timeToSendUpdates: LocalTime,
@JsonProperty val subscribers: List<String>,
@JsonProperty val canteensForSubscribers: List<String> = listOf("adenauerring"),
@JsonProperty val translation: TranslationConfig? = null
) : IConfig {
companion object {
Expand Down
26 changes: 19 additions & 7 deletions src/main/kotlin/org/fuchss/matrix/mensa/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ import org.fuchss.matrix.bots.helper.createMediaStore
import org.fuchss.matrix.bots.helper.createRepositoriesModule
import org.fuchss.matrix.bots.helper.handleCommand
import org.fuchss.matrix.bots.helper.handleEncryptedCommand
import org.fuchss.matrix.mensa.api.CanteensApi
import org.fuchss.matrix.mensa.api.CanteenApi
import org.fuchss.matrix.mensa.handler.command.ShowCommand
import org.fuchss.matrix.mensa.handler.command.SubscribeCommand
import org.fuchss.matrix.mensa.handler.sendCanteenEventToRoom
import org.fuchss.matrix.mensa.swka.SWKAMensa
import org.fuchss.matrix.mensa.kit.MriMensa
import org.fuchss.matrix.mensa.kit.SwkaMensa
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.File
Expand All @@ -36,7 +37,18 @@ private lateinit var commands: List<Command>
fun main() {
runBlocking {
val config = Config.load()
val canteenApi: CanteensApi = SWKAMensa()
val canteenApis: List<CanteenApi> = listOf(SwkaMensa(), MriMensa())
val enabledApisForSubscribers =
canteenApis.filter {
config.canteensForSubscribers.isEmpty() ||
config.canteensForSubscribers.contains(
it.canteen().id
)
}
if (enabledApisForSubscribers.isEmpty()) {
error("No canteens enabled. Invalid configuration.")
}

val translationService = TranslationService(config.translation)

commands =
Expand All @@ -47,7 +59,7 @@ fun main() {
QuitCommand(config),
LogoutCommand(config),
ChangeUsernameCommand(),
ShowCommand(canteenApi, translationService),
ShowCommand(canteenApis, translationService),
SubscribeCommand(config)
)

Expand All @@ -58,7 +70,7 @@ fun main() {
matrixBot.subscribeContent { event -> handleCommand(commands, event, matrixBot, config) }
matrixBot.subscribeContent { event -> handleEncryptedCommand(commands, event, matrixBot, config) }

val timer = scheduleMensaMessages(matrixBot, config, canteenApi, translationService)
val timer = scheduleMensaMessages(matrixBot, config, enabledApisForSubscribers, translationService)

val loggedOut = matrixBot.startBlocking()

Expand Down Expand Up @@ -95,7 +107,7 @@ private suspend fun getMatrixClient(config: Config): MatrixClient {
private fun scheduleMensaMessages(
matrixBot: MatrixBot,
config: Config,
canteenApi: CanteensApi,
canteenApis: List<CanteenApi>,
translationService: TranslationService
): Timer {
val timer = Timer(true)
Expand All @@ -107,7 +119,7 @@ private fun scheduleMensaMessages(

for (roomId in config.subscriptions()) {
try {
sendCanteenEventToRoom(roomId, matrixBot, true, canteenApi, translationService)
sendCanteenEventToRoom(roomId, matrixBot, true, canteenApis, translationService)
} catch (e: Exception) {
logger.error(e.message, e)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import kotlinx.datetime.todayIn
/**
* This interface defines the minimum functionality that a canteen provides.
*/
interface CanteensApi {
interface CanteenApi {
fun canteen(): Canteen

suspend fun foodToday() = foodAtDate(Clock.System.todayIn(TimeZone.currentSystemDefault()))

/**
* Retrieve foods of different canteens at a certain date.
* @param[date] the date to consider
* @return a map that contains canteens with food mapped to their lines at a certain day
* (remember: the key (canteen) may contain more information than the value (list of canteen lines))
* @return the lines of the canteen at a certain day
*/
suspend fun foodAtDate(date: LocalDate): Map<Canteen, List<CanteenLine>>
suspend fun foodAtDate(date: LocalDate): List<CanteenLine>
}
2 changes: 0 additions & 2 deletions src/main/kotlin/org/fuchss/matrix/mensa/api/Meal.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import com.vdurmont.emoji.EmojiManager
/**
* This data class defines all information that should be provided by a meal.
* @param[name] the name of the meal
* @param[foodAdditiveNumbers] some specific additive numbers of the meal (e.g., "Nuts (PA)")
* @param[fish] indicator for fish
* @param[pork] indicator for pork
* @param[cow] indicator for cow
Expand All @@ -15,7 +14,6 @@ import com.vdurmont.emoji.EmojiManager
*/
data class Meal(
val name: String,
val foodAdditiveNumbers: List<String>,
val fish: Boolean,
val pork: Boolean,
val cow: Boolean,
Expand Down
60 changes: 30 additions & 30 deletions src/main/kotlin/org/fuchss/matrix/mensa/handler/CanteenFormatter.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
package org.fuchss.matrix.mensa.handler

import kotlinx.datetime.Clock
import kotlinx.datetime.DatePeriod
import kotlinx.datetime.TimeZone
import kotlinx.datetime.plus
import kotlinx.datetime.todayIn
import net.folivo.trixnity.core.model.RoomId
import org.fuchss.matrix.bots.MatrixBot
import org.fuchss.matrix.bots.markdown
import org.fuchss.matrix.mensa.TranslationService
import org.fuchss.matrix.mensa.api.CanteensApi
import org.fuchss.matrix.mensa.api.CanteenApi
import org.slf4j.Logger
import org.slf4j.LoggerFactory

Expand All @@ -19,37 +14,42 @@ suspend fun sendCanteenEventToRoom(
roomId: RoomId,
matrixBot: MatrixBot,
scheduled: Boolean,
canteen: CanteensApi,
canteens: List<CanteenApi>,
translationService: TranslationService
) {
logger.info("Sending Mensa to Room ${roomId.full}")

val mensaToday = canteen.foodToday()
val noFood = mensaToday.isEmpty() || mensaToday.all { (_, lines) -> lines.isEmpty() }
for (canteen in canteens) {
val mensaToday = canteen.foodToday()
val noFood = mensaToday.isEmpty() || mensaToday.all { (_, meals) -> meals.isEmpty() }

if (noFood && scheduled) {
logger.debug("Skipping sending of mensa plan to {} as there will be no food today.", roomId)
return
}

var response = ""
for ((mensa, lines) in mensaToday) {
response += if (mensa.link == null) "## ${mensa.name}\n" else "## [${mensa.name}](<${mensa.link}>)\n"
for (l in lines) {
response += "### ${l.name}\n"
for (meal in l.meals) response += "* ${meal.entry()}\n"
if (noFood && scheduled) {
logger.debug("Skipping sending of mensa plan to {} as there will be no food today.", roomId)
return
}
}

if (response.isBlank()) {
response = "Kein Essen heute :("
}
val mensa = canteen.canteen()
var title = if (mensa.link == null) "## ${mensa.name}\n" else "## [${mensa.name}](<${mensa.link}>)\n"

response = translationService.translate(response).trim()
// Crop translation indications ..
if (response.contains("#") && !response.startsWith("#")) {
response = response.substring(response.indexOf("#"))
}
var meals = ""
for (l in mensaToday) {
if (l.meals.isEmpty()) {
continue
}
meals += if (l.name.isNotBlank()) "### ${l.name}\n" else ""
for (meal in l.meals) meals += "* ${meal.entry()}\n"
}

matrixBot.room().sendMessage(roomId) { markdown(response) }
if (meals.isEmpty()) {
meals = "### Heute hier kein Essen.\n"
} else {
meals = translationService.translate(meals).trim()
// Crop translation indications ..
if (meals.contains("#") && !meals.startsWith("#")) {
meals = meals.substring(meals.indexOf("#"))
}
}
val response = title + meals
matrixBot.room().sendMessage(roomId) { markdown(response) }
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package org.fuchss.matrix.mensa.handler.command

import net.folivo.trixnity.client.room.message.text
import net.folivo.trixnity.core.model.EventId
import net.folivo.trixnity.core.model.RoomId
import net.folivo.trixnity.core.model.UserId
import net.folivo.trixnity.core.model.events.m.room.RoomMessageEventContent
import org.fuchss.matrix.bots.MatrixBot
import org.fuchss.matrix.bots.command.Command
import org.fuchss.matrix.mensa.TranslationService
import org.fuchss.matrix.mensa.api.CanteensApi
import org.fuchss.matrix.mensa.api.CanteenApi
import org.fuchss.matrix.mensa.handler.sendCanteenEventToRoom

class ShowCommand(private val canteen: CanteensApi, private val translationService: TranslationService) : Command() {
class ShowCommand(private val canteens: List<CanteenApi>, private val translationService: TranslationService) : Command() {
override val name: String = "show"
override val help: String = "show the mensa plan for today"
override val help: String = "show the mensa plan for today, if id provided, show the mensa plan for the canteen with the id"
override val params: String = "[canteen_id]"

override suspend fun execute(
matrixBot: MatrixBot,
Expand All @@ -22,6 +24,11 @@ class ShowCommand(private val canteen: CanteensApi, private val translationServi
textEventId: EventId,
textEvent: RoomMessageEventContent.TextBased.Text
) {
sendCanteenEventToRoom(roomId, matrixBot, false, canteen, translationService)
val consideredCanteens = if (parameters.isEmpty()) canteens else canteens.filter { it.canteen().id == parameters }
if (consideredCanteens.isEmpty()) {
matrixBot.room().sendMessage(roomId) { text("No canteen with id $parameters found.") }
return
}
sendCanteenEventToRoom(roomId, matrixBot, false, consideredCanteens, translationService)
}
}
70 changes: 70 additions & 0 deletions src/main/kotlin/org/fuchss/matrix/mensa/kit/MriMensa.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.fuchss.matrix.mensa.kit

import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.request
import io.ktor.http.HttpMethod
import kotlinx.datetime.DatePeriod
import kotlinx.datetime.LocalDate
import kotlinx.datetime.minus
import kotlinx.datetime.plus
import org.fuchss.matrix.mensa.api.Canteen
import org.fuchss.matrix.mensa.api.CanteenApi
import org.fuchss.matrix.mensa.api.CanteenLine
import org.fuchss.matrix.mensa.api.Meal
import org.jsoup.Jsoup

class MriMensa : CanteenApi {
companion object {
private const val MRI_WEBSITE = "https://casinocatering.de/speiseplan/"
}

override fun canteen() = Canteen("mri", "Max Rubner-Institut", link = MRI_WEBSITE)

override suspend fun foodAtDate(date: LocalDate): List<CanteenLine> {
val mealsThisWeek = parseCanteen(date)
val mealsToday = mealsThisWeek[date]?.let { listOf(it) } ?: emptyList()
return mealsToday
}

private suspend fun parseCanteen(date: LocalDate): Map<LocalDate, CanteenLine> {
val client = HttpClient()
val response = client.request(MRI_WEBSITE) { method = HttpMethod.Get }
val body: String = response.body()
val document = Jsoup.parse(body)

val mainContent = document.getElementById("content") ?: return emptyMap()

val dateToFood = mutableMapOf<LocalDate, CanteenLine>()
var dateForEntry = date.minus(DatePeriod(days = date.dayOfWeek.value - 1))

val entries = mainContent.getElementsByClass("elementor-column")
var startFound = false
for (entry in entries) {
val title = entry.getElementsByClass("elementor-heading-title").first()?.text() ?: continue
if (title.contains("Montag")) {
// First day of week
startFound = true
}
if (!startFound) {
continue
}

if (dateToFood.size == 5) {
// We have all days of the week
break
}

val foods = entry.getElementsByClass("elementor-icon-list-item").toList().map { it.text().replace("", "").trim() }
dateToFood[dateForEntry] = CanteenLine("", foods.map { toMeal(it) })
dateForEntry = dateForEntry.plus(DatePeriod(days = 1))
}
return dateToFood
}

private fun toMeal(name: String): Meal {
// e.g., "(a,b,c)"
val additionals = Regex("\\(([^)]+)\\)").findAll(name).toList().flatMap { it.groupValues[1].split(",") }
return Meal(name, additionals.contains("d") || additionals.contains("b"), false, false, false, false, false)
}
}
Original file line number Diff line number Diff line change
@@ -1,40 +1,42 @@
package org.fuchss.matrix.mensa.swka
package org.fuchss.matrix.mensa.kit

import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.request
import io.ktor.http.HttpMethod
import kotlinx.datetime.LocalDate
import org.fuchss.matrix.mensa.api.Canteen
import org.fuchss.matrix.mensa.api.CanteenApi
import org.fuchss.matrix.mensa.api.CanteenLine
import org.fuchss.matrix.mensa.api.CanteensApi
import org.fuchss.matrix.mensa.api.Meal
import org.fuchss.matrix.mensa.numberOfWeek
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import org.slf4j.LoggerFactory

class SWKAMensa : CanteensApi {
class SwkaMensa : CanteenApi {
companion object {
private val logger = LoggerFactory.getLogger(SWKAMensa::class.java)
private val logger = LoggerFactory.getLogger(SwkaMensa::class.java)
private const val SWKA_WEBSITE = "https://www.sw-ka.de/en/hochschulgastronomie/speiseplan/mensa_adenauerring/"
private const val SWKA_WEBSITE_API = //
"https://www.sw-ka.de/de/hochschulgastronomie/speiseplan/mensa_adenauerring/?view=ok&c=adenauerring&STYLE=popup_plain&kw=%%%WoY%%%"
private val LINES_TO_CONSIDER = listOf("Linie ", "Schnitzel", "[pizza]werk Pizza", "[pizza]werk Pasta", "[kœri]werk")
}

override suspend fun foodAtDate(date: LocalDate): Map<Canteen, List<CanteenLine>> {
override fun canteen() = Canteen("adenauerring", "Mensa am Adenauerring", link = SWKA_WEBSITE)

override suspend fun foodAtDate(date: LocalDate): List<CanteenLine> {
val week = numberOfWeek(date)
val html = request(week)

val document = Jsoup.parse(html)
val tableOfDay = document.select("h1:contains(${date.dayOfMonth.pad()}.${date.monthNumber.pad()}) + table")
if (tableOfDay.isEmpty()) {
return emptyMap()
return emptyList()
}
if (tableOfDay.size != 1) {
logger.error("Found more than one table for ${date.dayOfMonth.pad()}.${date.monthNumber.pad()}")
return emptyMap()
return emptyList()
}

val mensaLinesRaw = tableOfDay[0].select("td[width=20%] + td")
Expand All @@ -56,9 +58,7 @@ class SWKAMensa : CanteensApi {
}

mensaLines.sortBy { it.name }

val mensa = Canteen("adenauerring", "Mensa am Adenauerring", link = SWKA_WEBSITE)
return mapOf(mensa to mensaLines)
return mensaLines
}

private fun closed(meals: List<Meal>): Boolean {
Expand All @@ -81,7 +81,6 @@ class SWKAMensa : CanteensApi {

return Meal(
name = mealName,
foodAdditiveNumbers = emptyList(),
fish = additionalInformation.contains("MSC"),
pork = additionalInformation.contains("S") || additionalInformation.contains("SAT"),
cow = additionalInformation.contains("R") || additionalInformation.contains("RAT"),
Expand Down
Loading

0 comments on commit 398d062

Please sign in to comment.