diff --git a/README.md b/README.md
index 5b3aab7851ca96648404101ef669a1dde4036791..2fbcc561ab946c0aaae7cafd3da0ae369baf020b 100644
--- a/README.md
+++ b/README.md
@@ -1,95 +1,37 @@
-# ⏰ reminder-agenda bot
-A [maubot](https://github.com/maubot/maubot) to remind you about things.
+# ETH Zurich Lunch Bot
 
-Basically [matrix-reminder-bot](https://github.com/anoadragon453/matrix-reminder-bot/tree/master) and [maubot/reminder](https://github.com/maubot/reminder) smushed together. This project includes code taken from both repositories, credit goes to them!
+A [maubot](https://github.com/maubot/maubot) plugin for the canteen lunch menus at ETH Zurich.
 
-![example of interacting with the bot](screenshot.png)
+Forked from [reminder-agenda bot](https://github.com/MxMarx/reminder), which is
+basically [matrix-reminder-bot](https://github.com/anoadragon453/matrix-reminder-bot/tree/master) and [maubot/reminder](https://github.com/maubot/reminder) smushed together. This project includes code taken from all repositories, credit goes to them!
 
 ## Features
 
-* Set once-off reminders and recurring reminders
-* To-do lists
-* Parse natural language (e.g. `every friday at 2pm`) and crontab syntax
-* Remind just yourself or the whole room
+* Show lunch menu (optional canteens filter)
+* Persistent user config: menu language, canteens filter, price category
+* Set up recurring reminders to post the lunch menu
 * Subscribe to other people's reminders
-* Lots of [locales](https://dateparser.readthedocs.io/en/latest/supported_locales.html)
-* Per-user rate limits
-* Maubot!
 
 ## Setup
-This bot requires python libraries that aren't included in the official maubot docker image.
 
+Dependencies:
+
+```bash
+pip install pytz
+pip install dateparser
+pip install apscheduler
+pip install cron_descriptor
+```
+
+* [pytz](https://pypi.org/project/pytz/)
 * [apscheduler](https://github.com/agronholm/apscheduler)
 * [dateparser](https://github.com/scrapinghub/dateparser)
 * [cron_descriptor](https://github.com/Salamek/cron-descriptor) (optional, shows cron reminders with natural language)
 
-Be sure to add them to the optional-requirements.txt file in [maubot](https://github.com/maubot/maubot) and build a new docker image with
-`docker build --tag maubot-for-reminders . -f Dockerfile`
-
-This is pretty easy to use with the [ansible deployment](https://github.com/spantaleev/matrix-docker-ansible-deploy), just add this line to your vars.yml: `matrix_bot_maubot_docker_image: maubot-for-reminders`
-
 ## Usage
-### Creating optionally recurring reminders:
-`!remind|remindme|r <date> <message>` Adds a reminder
-* `!remind 8 hours buy more pumpkins`
-* `!remind 2023-11-30 15:00 befriend rats`
-* `!remind abolish closed-access journals at 3pm tomorrow`
-* `July 2`, `tuesday at 2pm`, `8pm`, `20 days`, `4d`, `2wk`, ...
-* Dates doesn't need to be at the beginning of the string, but parsing works better if they are.
-
-`!remind [room] [every] ...`
-* `[room]` pings the whole room
-* `[every]` create recurring reminders `!remind every friday 3pm take out the trash`
-
-`!remind [room] <cron> <message>` Schedules a reminder using a crontab syntax
-* `!remind cron 30 9 * * mon-fri do something` sets reminders for 9:30am, Monday through Friday.
-* `!remind cron` lists more examples
-
-You can also reply to any message with `!remind ...` to get reminded about that message.\
-To get pinged by someone else's reminder, react to their message with 👍.
 
-### Creating agenda items
-`!agenda [room] <message>` creates an agenda item. Agenda items are like reminders but don't have a time, for things like to-do lists.
-
-### Listing active reminders
-`!remind list [all] [my] [subscribed]` lists all reminders in a room 
-* `all` lists all reminders from every room
-* `my` lists only reminders you created
-* `subscribed` lists only reminders you are subscribed to
-
-### Deleting reminders
-Cancel reminders by removing the message creating it, unsubscribe by removing your upvote.\
-Cancel recurring reminders by replying with `!remind cancel|delete` 
-* `!remind cancel|delete <ID>` deletes a reminder matching the 4 letter ID shown by `list`
-* `!remind cancel|delete <message>` deletes a reminder **beginning with** <message>
-    * e.g. `!remind delete buy more` would delete the reminder `buy more pumpkins`
-
-### Rescheduling
-Reminders can be rescheduled after they have fired by replying with `!remind <new date>`
-
-### Settings
-Dates are parsed using your [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zone) and [locale](https://dateparser.readthedocs.io/en/latest/supported_locales.html).
-* `!remind tz|timezone [new-timezone]` view or set your timezone
-* `!remind locale [new-locale]` view or set your locale
-
-## Cron Syntax
-```
-*	any value
-,	value list separator
--	range of values
-/	step 
-
-┌─────── minute (0 - 59)
-│ ┌─────── hour (0 - 23)
-│ │ ┌─────── day of the month (1 - 31)
-│ │ │ ┌─────── month (1 - 12)
-│ │ │ │ ┌─────── weekday (0 - 6) (Sunday to Saturday)                             
-│ │ │ │ │
-* * * * * <message>
 ```
-
+!lunch
+!lunch config
+!lunch help
 ```
-30 9 * * *              Every day at 9:30am
-0/30 9-17 * * mon-fri   Every 30 minutes from 9am to 5pm, Monday through Friday
-0 14 1,16 * *           2:00pm on the 1st and 16th day of the month
-0 0 1-7 * mon           First Monday of the month at midnight
\ No newline at end of file
diff --git a/base-config.yaml b/base-config.yaml
index 1e0fc93d830742f3291fee8ad0a4a80860ffa8b8..7e20ddc22ff7fc88d822690a10702d5505d2a14a 100644
--- a/base-config.yaml
+++ b/base-config.yaml
@@ -1,45 +1,40 @@
-# https://github.com/MxMarx/reminder
-# Feature requests welcome!
+# Default timezone
+default_timezone: Europe/Zurich
 
-# Default timezone for users who did not set one.
-# This is parsed with dateparser, so Continent/City, UTC offsets, and abbreviations should work.
-#  e.g. Europe/Helsinki, EET, +0300
-# See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
-default_timezone: America/Los_Angeles
-
-# Default locale/language compatible with dateparser
-# See https://dateparser.readthedocs.io/en/latest/supported_locales.html
+# Default language (locale) `en` or `de`
 default_locale: en
 
 # Base command without the prefix (!).
 # If a list is provided, the first is the main name and the rest are aliases.
 base_command:
-- remind
-- remindme
-
-# Agenda items are like reminders but don't have a time, for things like to-do lists.
-# Aliases used to create an agenda items by calling "!agenda <message>":
-agenda_command:
-- agenda
-- todo
-
-# subcommands used to cancel reminders
-cancel_command:
-- cancel
-- delete
+  - lunch
 
-# If verbose is true, display full confirmation messages. If false, confirm by reacting with :thumbs-up:
-verbose: true
+# Hunger command (same as the `!<base_command> menu` subcommand)
+hunger_command:
+  - hunger
+  - hungry
+  - food
+  - essen
+  - lunchmenu
 
 # Rate limit for individual users, as the number of reminders allowed to fire in an interval
 rate_limit: 10
 rate_limit_minutes: 60
 
-# Power level needed to delete someone else's reminder
+# Power level needed to create reminders and delete someone else's reminders
 admin_power_level: 50
 
-# time_format: strftime format when listing reminders.
-#   "%-I:%M%P %Z on %A, %B %-d %Y"  - 7:36pm PDT on Sunday, September 17 2023
-#   "%Y-%m-%d %H:%M %Z"             - 2023-09-17 19:36 PDT
-#   Currently, reminders within 7 days will be displayed as just the relative delta, e.g. "2 days and 1 hour"
-time_format: "%-I:%M%P %Z on %A, %B %-d %Y"
\ No newline at end of file
+# time_format: strftime format when listing reminders
+time_format: "%Y-%m-%d %H:%M %Z"
+
+# URLs of cookpit api https://idapps.ethz.ch/cookpit-pub-services/swagger-ui/
+url_facilities: https://idapps.ethz.ch/cookpit-pub-services/v1/facilities/?client-id=ethz-wcms&rs-first=0&rs-size=50
+url_menus: https://idapps.ethz.ch/cookpit-pub-services/v1/weeklyrotas/?client-id=ethz-wcms&rs-first=0&rs-size=50
+
+# Default facilities filter
+default_facilities:
+  - food market
+  - fusion meal
+
+# Default price to show (one of `int`, `ext`, `stud` or `off`)
+default_price: int
diff --git a/maubot.yaml b/maubot.yaml
index 11a7e7d74eccba2c5f6ca40b443540c9259db854..d80bb2edb449ab0dca887e01631a5d90fd31f1ec 100644
--- a/maubot.yaml
+++ b/maubot.yaml
@@ -1,6 +1,6 @@
 maubot: 0.4.1
-id: org.bytemarx.reminder
-version: 0.1.2
+id: ch.ethz.phys.lunch
+version: 0.0.2
 license: AGPL-3.0-or-later
 modules:
 - reminder
@@ -15,4 +15,3 @@ soft_dependencies:
 - cron_descriptor
 database: true
 database_type: asyncpg
-
diff --git a/reminder/bot.py b/reminder/bot.py
index 5b7a4e0f2d76353c6d2360ca95e6cd869e96bfd4..9652983c6f2b024504fb10c0e866804eac99b4a3 100644
--- a/reminder/bot.py
+++ b/reminder/bot.py
@@ -1,4 +1,5 @@
-# reminder - A maubot plugin to create_reminder you about things.
+# ethzlunch - A maubot plugin for the canteen lunch menus at ETH Zurich.
+# Copyright (C) 2024 Sven Mäder
 # Copyright (C) 2020 Tulir Asokan
 #
 # This program is free software: you can redistribute it and/or modify
@@ -13,13 +14,11 @@
 #
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
-import re
 from typing import Type, Tuple, List, Dict
-from datetime import datetime, timedelta
+from datetime import date, timedelta
 import pytz
 
-from mautrix.types import (EventType, RedactionEvent, StateEvent, Format, MessageType,
-                           TextMessageEventContent, ReactionEvent, UserID, EventID, RelationType)
+from mautrix.types import (EventType, RedactionEvent, StateEvent, ReactionEvent, EventID)
 from maubot import Plugin, MessageEvent
 from maubot.handlers import command, event
 from mautrix.util.async_db import UpgradeTable
@@ -27,34 +26,39 @@ from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
 
 from .migrations import upgrade_table
 from .db import ReminderDatabase
-from .util import validate_locale, validate_timezone, CommandSyntaxError, parse_date, CommandSyntax, make_pill
+from .util import validate_locale, validate_timezone, CommandSyntaxError
 from .reminder import Reminder
+from .ethz import (parse_facilities, parse_menus, filter_facilities, markdown_facilities,
+                   markdown_menus)
 from apscheduler.schedulers.asyncio import AsyncIOScheduler
 
-# TODO: merge licenses
 
 class Config(BaseProxyConfig):
     def do_update(self, helper: ConfigUpdateHelper) -> None:
         helper.copy("default_timezone")
         helper.copy("default_locale")
         helper.copy("base_command")
-        helper.copy("agenda_command")
-        helper.copy("cancel_command")
+        helper.copy("hunger_command")
         helper.copy("rate_limit_minutes")
         helper.copy("rate_limit")
-        helper.copy("verbose")
         helper.copy("admin_power_level")
         helper.copy("time_format")
+        helper.copy("url_facilities")
+        helper.copy("url_menus")
+        helper.copy("default_price")
+        helper.copy("default_facilities")
 
 
 class ReminderBot(Plugin):
     base_command: Tuple[str, ...]
-    agenda_command: Tuple[str, ...]
-    cancel_command: Tuple[str, ...]
+    hunger_command: Tuple[str, ...]
     default_timezone: pytz.timezone
+    admin_power_level: int
     scheduler: AsyncIOScheduler
     reminders: Dict[EventID, Reminder]
     db: ReminderDatabase
+    url_facilities: str
+    url_menus: str
 
     @classmethod
     def get_config_class(cls) -> Type[BaseProxyConfig]:
@@ -64,26 +68,21 @@ class ReminderBot(Plugin):
     def get_db_upgrade_table(cls) -> UpgradeTable:
         return upgrade_table
 
-
-
     async def start(self) -> None:
         self.scheduler = AsyncIOScheduler()
-        # self.scheduler.configure({"apscheduler.timezone": self.config["default_timezone"]})
         self.scheduler.start()
         self.db = ReminderDatabase(self.database)
         self.on_external_config_update()
-        # load all reminders
         self.reminders = await self.db.load_all(self)
 
     def on_external_config_update(self) -> None:
-
         self.config.load_and_update()
 
         def config_to_tuple(list_or_str: List | str):
             return tuple(list_or_str) if isinstance(list_or_str, list) else (list_or_str,)
         self.base_command = config_to_tuple(self.config["base_command"])
-        self.agenda_command = config_to_tuple(self.config["agenda_command"])
-        self.cancel_command = config_to_tuple(self.config["cancel_command"])
+        self.hunger_command = config_to_tuple(self.config["hunger_command"])
+        self.admin_power_level = self.config["admin_power_level"]
 
         # If the locale or timezone is invalid, use default one
         self.db.defaults.locale = self.config["default_locale"]
@@ -94,95 +93,189 @@ class ReminderBot(Plugin):
         if not validate_timezone(self.config["default_timezone"]):
             self.log.warning(f'unknown default timezone: {self.config["default_timezone"]}')
             self.db.defaults.timezone = "UTC"
-
+        self.url_facilities = self.config["url_facilities"]
+        self.url_menus = self.config["url_menus"]
+        self.db.defaults.price = self.config["default_price"]
+        self.db.defaults.facilities = ",".join(self.config["default_facilities"])
 
     async def stop(self) -> None:
         self.scheduler.shutdown(wait=False)
 
+    async def get_facilities_data(self, lang: str) -> Dict:
+        headers = {"Accept": "application/json"}
+        params = {"lang": lang}
+        resp = await self.http.get(self.url_facilities, headers=headers, params=params)
+        if resp.status == 200:
+            data = await resp.json()
+            return data
+        resp.raise_for_status()
+        return None
+
+    async def get_menus_data(self, lang: str) -> Dict:
+        today = date.today().strftime("%Y-%m-%d")
+        tomorrow = (date.today() + timedelta(days=1)).strftime("%Y-%m-%d")
+        headers = {"Accept": "application/json"}
+        params = {"lang": lang, "valid-after": today, "valid-before": tomorrow}
+        resp = await self.http.get(self.url_menus, headers=headers, params=params)
+        if resp.status == 200:
+            data = await resp.json()
+            return data
+        resp.raise_for_status()
+        return None
+
+    async def get_facilities(self, user: str = "") -> Dict:
+        lang = (await self.db.get_user_info(user)).locale
+        facilities_data = await self.get_facilities_data(lang)
+        return parse_facilities(facilities_data)
+
+    async def get_menus(self, user: str = "", facilities_filter: str = None) -> Dict:
+        user_info = await self.db.get_user_info(user)
+        lang = user_info.locale
+        price = user_info.price
+
+        facilities = await self.get_facilities(user)
+        menus_data = await self.get_menus_data(lang)
+
+        if not facilities_filter:
+            facilities_filter = user_info.facilities
+
+        if facilities_filter == "all":
+            facilities_filter = None
+
+        if facilities_filter:
+            facilities = filter_facilities(facilities, facilities_filter)
+
+        return parse_menus(menus_data, facilities, customer=price)
+
+    async def get_markdown_facilities(self, user: str = "") -> Dict | None:
+        facilities = await self.get_facilities(user=user)
+
+        if facilities:
+            return markdown_facilities(facilities)
+        else:
+            return None
+
+    async def get_markdown_menus(self, user: str = "",
+                                 facilities_filter: str = None) -> Dict | None:
+        menus = await self.get_menus(user=user, facilities_filter=facilities_filter)
+
+        if menus:
+            return markdown_menus(menus)
+        else:
+            return None
 
+    async def show_lunch_menu(self, evt: MessageEvent, canteens: str) -> None:
+        markdown_menus = await self.get_markdown_menus(user=evt.sender, facilities_filter=canteens)
+
+        if markdown_menus:
+            await evt.respond(markdown_menus)
+        else:
+            await evt.respond("No results")
+
+    @command.new(name=lambda self: self.hunger_command[0],
+                 aliases=lambda self, alias: alias in self.hunger_command)
+    @command.argument("canteens", pass_raw=True, required=False)
+    async def hunger(self, evt: MessageEvent, canteens: str) -> None:
+        await self.show_lunch_menu(evt, canteens)
 
     @command.new(name=lambda self: self.base_command[0],
-                 aliases=lambda self, alias: alias in self.base_command + self.agenda_command,
-                 help="Create a reminder", require_subcommand=False, arg_fallthrough=False)
-    @command.argument("room", matches="room", required=False)
-    @command.argument("every", matches="every", required=False)
-    @command.argument("start_time", matches="(.*?);", pass_raw=True, required=False)
-    @command.argument("cron", matches="cron ?(?:\s*\S*){0,5}", pass_raw=True, required=False)
-    @command.argument("message", pass_raw=True, required=False)
-    async def create_reminder(self, evt: MessageEvent,
-                              room: str = None,
-                              every: str = None,
-                              start_time: Tuple[str] = None,
-                              cron: Tuple[str] = None,
-                              message: str = None,
-                              again: bool = False) -> None:
-        """Create a reminder or an alarm with a given target
-        Args:
-            evt:
-            room: if true, ping the whole room
-            cron: crontab syntax
-            every: is the reminder recurring?
-            start_time: can be explicitly specified with a semicolon: !remind <start_time>; <message>
-            message: contains both the start_time and the message if not using a semicolon to separate them
-            again:
-        """
-        date_str = None
-        reply_to_id = evt.content.get_reply_to()
-        reply_to = None
-        user_info = await self.db.get_user_info(evt.sender)
+                 aliases=lambda self, alias: alias in self.base_command)
+    async def lunch(self, evt: MessageEvent) -> None:
+        pass
+
+    @lunch.subcommand("menu", aliases=["menus", "show"],
+                      help="Show lunch menu (canteens example: `all` or `poly,food market,fusion`)")
+    @command.argument("canteens", pass_raw=True, required=False)
+    async def show(self, evt: MessageEvent, canteens: str) -> None:
+        await self.show_lunch_menu(evt, canteens)
+
+    @lunch.subcommand("canteen", aliases=["canteens", "mensa"], help="List all canteen names")
+    async def facilities_list(self, evt: MessageEvent) -> None:
+        markdown_facilities = await self.get_markdown_facilities(user=evt.sender)
+
+        if markdown_facilities:
+            await evt.respond(markdown_facilities)
+        else:
+            await evt.respond("No results")
+
+    @lunch.subcommand("config", aliases=["conf"], help="Set or show config settings")
+    async def settings(self, evt: MessageEvent) -> None:
+        pass
+
+    @settings.subcommand("language", aliases=["lang"], help="Set menu language (`en` or `de`)")
+    @command.argument("lang")
+    async def config_lang(self, evt: MessageEvent, lang: str) -> None:
+        if not lang:
+            await evt.reply(f"Menu language is "
+                            f"{(await self.db.get_user_info(evt.sender)).locale}")
+            return
+        if lang in ["en", "de"]:
+            await self.db.set_user_info(evt.sender, key="locale", value=lang)
+            await evt.react("👍")
+        else:
+            await evt.reply(f"Unknown language: `{lang}`\n"
+                            f"Available languages: `en`, `de`")
+
+    @settings.subcommand("canteen", aliases=["canteens", "mensa"],
+                         help="Set canteens (example: `all` or `poly,food market,fusion`)")
+    @command.argument("canteens", pass_raw=True)
+    async def config_canteen(self, evt: MessageEvent, canteens: str) -> None:
+        if not canteens:
+            canteens = (await self.db.get_user_info(evt.sender)).facilities
+            await evt.reply(f"Canteen filter is: `{canteens}`")
+            return
 
-        # Determine is the agenda command was used instead of creating a subcommand so [room] can still be used
-        agenda = evt.content.body[1:].startswith(self.agenda_command)
-        if agenda:
-            # Use the date the message was created as the date for agenda items
-            start_time = datetime.now(tz=pytz.UTC)
-
-        # If we are replying to a previous reminder, recreate the original reminder with a new time
-        if reply_to_id:
-            reply_to = await self.client.get_event(room_id=evt.room_id, event_id=reply_to_id)
-            if "org.bytemarx.reminder" in reply_to.content:
-                again = True
-                start_time = (message,)
-                message = reply_to.content["org.bytemarx.reminder"]["message"]
-                reply_to_id = reply_to.content["org.bytemarx.reminder"]["reply_to"]
-                event_id = reply_to.content["org.bytemarx.reminder"]["id"]
-                if event_id in self.reminders:
-                    await self.reminders[event_id].cancel()
+        await self.db.set_user_info(evt.sender, key="facilities", value=canteens)
+        await evt.react("👍")
 
-        try:
-            if not cron and not agenda:
-                if start_time:
-                    start_time, date_str = parse_date(start_time[0], user_info)
-                elif message.strip(): # extract the date from the message if not explicitly given
-                    start_time, date_str = parse_date(message, user_info, search_text=True)
+    @settings.subcommand("price", help="Set price category (`int`, `ext`, `stud` or `off`)")
+    @command.argument("category")
+    async def config_price(self, evt: MessageEvent, category: str) -> None:
+        if not category:
+            category = (await self.db.get_user_info(evt.sender)).price
+            off = " (prices not shown)" if category == "off" else ""
+            await evt.reply(f"Price category is: `{category}`{off}")
+            return
 
-                    # Check if "every" appears immediately before the date, if so, the reminder should be recurring.
-                    # This makes "every" possible to use in a sentence instead of just with @command.argument("every")
-                    if not every:
-                        every = message.lower().find('every ' + date_str.lower()) >= 0
+        if category in ["int", "ext", "stud", "off"]:
+            await self.db.set_user_info(evt.sender, key="price", value=category)
+            await evt.react("👍")
+        else:
+            await evt.reply(f"Unknown price category: `{category}`\n"
+                            f"Available price categories: `int`, `ext`, `stud`\n"
+                            f"Disable prices in menus: `off`")
+
+    @lunch.subcommand("remind", help="Create reminder (time: `hh:mm`, days default: `mon-fri`)")
+    @command.argument("time", matches="[0-9]{1,2}:[0-9]{2}")
+    @command.argument("days", required=False)
+    @command.argument("canteens", required=False, pass_raw=True)
+    async def remind(self, evt: MessageEvent,
+                     time: str = None,
+                     days: str = None,
+                     canteens: str = None) -> None:
+        power_levels = await self.client.get_state_event(room_id=evt.room_id,
+                                                         event_type=EventType.ROOM_POWER_LEVELS)
+        user_power = power_levels.users.get(evt.sender, power_levels.users_default)
 
-                    # Remove the date from the messages, converting "buy ice cream on monday" to "buy ice cream"
-                    compiled = re.compile("(every )?" + re.escape(date_str), re.IGNORECASE)
-                    message = compiled.sub("", message,  1).strip()
+        if user_power < self.admin_power_level:
+            await evt.reply(f"Power level of {self.admin_power_level} is required")
+            return
+
+        user_info = await self.db.get_user_info(evt.sender)
+        hour, minute = tuple(time.split(':'))
 
-                else: # If no arguments are supplied, return the help message
-                    await evt.reply(self._help_message())
-                    return
+        if not days:
+            days = "mon-fri"
 
-            # If the reminder was created by replying to a message, use that message's text
-            if reply_to_id and not message:
-                message = reply_to.content["body"]
+        cron = f"{minute} {hour} * * {days}"
 
+        try:
             reminder = Reminder(
                 bot=self,
                 room_id=evt.room_id,
-                message=message,
+                message=canteens,
                 event_id=evt.event_id,
-                reply_to=reply_to_id,
-                start_time=start_time,
                 cron_tab=cron,
-                recur_every=date_str if every else None,
-                is_agenda=agenda,
                 creator=evt.sender,
                 user_info=user_info,
             )
@@ -191,198 +284,49 @@ class ReminderBot(Plugin):
             await evt.reply(e.message)
             return
 
-        # Record the reminder and subscribe to it
         await self.db.store_reminder(reminder)
-
-        # If the command was called with a "room_command", make the reminder ping the room
-        user_id = UserID("@room") if room else evt.sender
-        await reminder.add_subscriber(subscribing_event=evt.event_id, user_id=user_id)
-
-        # Send a message to the room confirming the creation of the reminder
-        await self.confirm_reminder(evt, reminder, again=again, agenda=agenda)
-
+        await self.confirm_reminder(evt, reminder)
         self.reminders[reminder.event_id] = reminder
 
-
-    async def confirm_reminder(self, evt: MessageEvent, reminder: Reminder, again: bool = False, agenda: bool = False):
-        """Sends a message to the room confirming the reminder is set
-        If verbose is set in the config, print out the full message. If false, just react with 👍
-
-        Args:
-            evt:
-            reminder: The Reminder to confirm
-            again: Is this a reminder that was rescheduled?
-            agenda: Is this an agenda instead of a reminder?
-        """
+    async def confirm_reminder(self, evt: MessageEvent, reminder: Reminder):
         confirmation_event = await evt.react("\U0001F44D")
-
-        if self.config["verbose"]:
-
-            action = "add this to the agenda for" if agenda else "remind" if reminder.message else "ping"
-            target = "the room" if "@room" in reminder.subscribed_users.values() else "you"
-            message = f"to {reminder.message}" if reminder.message else ""
-
-            if reminder.reply_to:
-                evt_link = f"[message](https://matrix.to/#/{reminder.room_id}/{reminder.reply_to})"
-                message += f" (replying to that {evt_link})" if reminder.message else f" about that {evt_link}"
-
-            msg = f"I'll {action} {target} {message}"
-            msg += " again" if again else ""
-
-            if again:
-                msg += " again"
-            if not agenda:
-                formatted_time = reminder.formatted_time(await self.db.get_user_info(evt.sender))
-                msg += " " + formatted_time
-
-
-            confirmation_event = await evt.reply(f"{msg}\n\n"
-                            f"(others can \U0001F44D the message above to get pinged too)")
-
         await reminder.set_confirmation(confirmation_event)
 
-
-    # @command.new("cancel", help="Cancel a recurring reminder", aliases=("delete",))
-    @create_reminder.subcommand(name=lambda self: self.cancel_command[0],
-                                help="Cancel a recurring reminder",
-                                aliases=lambda self, alias: alias in self.cancel_command)
-    @command.argument("search_text", pass_raw=True, required=False)
-    async def cancel_reminder(self, evt: MessageEvent, search_text: str) -> None:
-        """Cancel a reminder by replying to a reminder, or searching by either message or event ID"""
-
-        reminder = []
+        body = "Reminder"
+        if reminder.message:
+            body += f" for `{reminder.message}`"
+        body += " scheduled"
+        if reminder.recur_every or reminder.cron_tab:
+            user_info = await self.db.get_user_info(evt.sender)
+            formatted_time = reminder.formatted_time(user_info)
+            body += f" {formatted_time}"
+        body += ".\n\nAnyone can \U0001F44D the command message above to get pinged."
+
+        await evt.reply(body)
+
+    @lunch.subcommand("cancel", help="Cancel reminder")
+    async def cancel_reminder(self, evt: MessageEvent) -> None:
+        reminders = []
         if evt.content.get_reply_to():
             reminder_message = await self.client.get_event(evt.room_id, evt.content.get_reply_to())
-            if "org.bytemarx.reminder" not in reminder_message.content:
+            if "ch.ethz.phys.lunch" not in reminder_message.content:
                 await evt.reply("That doesn't look like a valid reminder event.")
                 return
-            reminder = self.reminders[reminder_message.content["org.bytemarx.reminder"]["id"]]
-        elif search_text:
-            # First, check the reminders created by the user, then everything else
-            for rem in sorted(self.reminders.values(), key=lambda x: x.creator == evt.sender, reverse=True):
-                # Using the first four base64 digits of the hash, p(collision) > 0.01 at ~10000 reminders
-                if rem.event_id[1:5] == search_text or re.match(re.escape(search_text.strip()), rem.message.strip(), re.IGNORECASE):
-                # if rem.event_id[1:5] == search_text or rem.message.upper().strip() == search_text.upper().strip():
-                    reminder = rem
-                    break
-        else: # Display the help message
-            await evt.reply(CommandSyntax.REMINDER_CANCEL.value.format(base_command=self.base_command[0],
-                                            cancel_command=self.cancel_command[0],
-                                            cancel_aliases="|".join(self.cancel_command)))
-            return
-
-        if not reminder:
-            await evt.reply(f"It doesn't look like you have any reminders matching the text `{search_text}`")
-            return
-
-        power_levels = await self.client.get_state_event(room_id=reminder.room_id,event_type=EventType.ROOM_POWER_LEVELS)
-        user_power = power_levels.users.get(evt.sender, power_levels.users_default)
-
-        if reminder.creator == evt.sender or user_power >= self.config["admin_power_level"]:
-            await reminder.cancel()
-            await evt.reply("Reminder cancelled!") if self.config["verbose"] else await evt.react("👍")
-        else:
-            await evt.reply(f"Power levels of {self.config['admin_power_level']} are required to cancel other people's reminders")
-
-
-    @create_reminder.subcommand("help", help="Usage instructions")
-    async def help(self, evt: MessageEvent) -> None:
-        await evt.reply(self._help_message(), allow_html=True)
-
-
-    @create_reminder.subcommand("list", help="List your reminders")
-    @command.argument("my", parser=lambda x: (re.sub(r"\bmy\b", "", x), re.search(r"\bmy\b", x)), required=False, pass_raw=True) # I hate it but it makes arguments not positional
-    @command.argument("subscribed", parser=lambda x: (re.sub(r"\bsubscribed\b", "", x), re.search(r"\bsubscribed\b", x)), required=False, pass_raw=True)
-    @command.argument("all", parser=lambda x: (re.sub(r"\ball\b", "", x), re.search(r"\ball\b", x)), required=False, pass_raw=True)
-    async def list(self, evt: MessageEvent, all: str, subscribed: str, my: str) -> None:
-        """Print out a formatted list of all the reminders for a user
-
-        Args:
-            evt: message event
-            my:  only list reminders the user created
-            all: list all reminders in every room
-            subscribed: only list reminders the user is subscribed to
-        """
-        room_id = None if all else evt.room_id
-        user_info = await self.db.get_user_info(evt.sender)
-        categories = {"**📜 Agenda items**": [], '**📅 Cron reminders**': [], '**🔁 Repeating reminders**': [], '**1️⃣ One-time reminders**': []}
-
-        # Sort the reminders by their next run date and format as bullet points
-        for reminder in sorted(self.reminders.values(), key=lambda x: x.job.next_run_time if x.job else datetime(2000,1,1,tzinfo=pytz.UTC)):
-            if (
-                    (not subscribed or any(x in reminder.subscribed_users.values() for x in [evt.sender, "@room"])) and
-                    (not my or evt.sender == reminder.creator) and
-                    (all or reminder.room_id == room_id)):
-
-                message = reminder.message
-                next_run = reminder.formatted_time(user_info)
-                short_event_id = f"[`{reminder.event_id[1:5]}`](https://matrix.to/#/{reminder.room_id}/{reminder.event_id})"
-
-                if reminder.reply_to:
-                    evt_link = f"[event](https://matrix.to/#/{reminder.room_id}/{reminder.reply_to})"
-                    message = f'{message} (replying to {evt_link})' if message else evt_link
-
-                if reminder.cron_tab:
-                    category = "**📅 Cron reminders**"
-                elif reminder.recur_every:
-                    category = "**🔁 Repeating reminders**"
-                elif not reminder.is_agenda:
-                    category = "**1️⃣ One-time reminders**"
-                else:
-                    category = "**📜 Agenda items**"
-
-                room_link = f"https://matrix.to/#/{reminder.room_id}" if all else ""
-                # creator_link = await make_pill(reminder.creator) if not my else ""
-
-                categories[category].append(f"* {short_event_id + room_link} {next_run}  **{message}**")
-
-        # Upack the nested dict into a flat list of reminders seperated by category
-        in_room_msg = " in this room" if room_id else ""
-        output = []
-        for category, reminders in categories.items():
-            if reminders:
-                output.append("\n" + category + in_room_msg)
-                for reminder in reminders:
-                    output.append(reminder)
-        output = "\n".join(output)
-
-        if not output:
-            await evt.reply(f"You have no upcoming reminders{in_room_msg} :(")
-
-        await evt.reply(output + f"\n\n`!{self.base_command[0]} list [all] [my] [subscribed]`\\"
-                                 f"\n`!{self.base_command[0]} {self.cancel_command[0]} [4-letter ID or start of message]`")
-
-
-
-    @create_reminder.subcommand("locale", help="Set your locale")
-    @command.argument("locale", required=False, pass_raw=True)
-    async def locale(self, evt: MessageEvent, locale: str) -> None:
-        if not locale:
-            await evt.reply(f"Your locale is `{(await self.db.get_user_info(evt.sender)).locale}`")
-            return
-        if validate_locale(locale):
-            await self.db.set_user_info(evt.sender, key="locale", value=locale)
-            await evt.reply(f"Setting your locale to {locale}")
+            reminders = [self.reminders[reminder_message.content["ch.ethz.phys.lunch"]["id"]]]
         else:
-            await evt.reply(f"Unknown locale: `{locale}`\n\n"
-                            f"[Available locales](https://dateparser.readthedocs.io/en/latest/supported_locales.html)"
-                            f" (case sensitive)")
-
+            reminders = [v for k, v in self.reminders.items() if v.room_id == evt.room_id]
 
+        for reminder in reminders:
+            power_levels = await self.client.get_state_event(room_id=reminder.room_id,
+                                                             event_type=EventType.ROOM_POWER_LEVELS)
+            user_power = power_levels.users.get(evt.sender, power_levels.users_default)
 
-    @create_reminder.subcommand("timezone", help="Set your timezone", aliases=("tz",))
-    @command.argument("timezone", required=False, pass_raw=True)
-    async def timezone(self, evt: MessageEvent, timezone: pytz.timezone) -> None:
-        if not timezone:
-            await evt.reply(f"Your timezone is `{(await self.db.get_user_info(evt.sender)).timezone}`")
-            return
-        if validate_timezone(timezone):
-            await self.db.set_user_info(evt.sender, key="timezone", value=timezone)
-            await evt.reply(f"Setting your timezone to {timezone}")
-        else:
-            await evt.reply(f"Unknown timezone: `{timezone}`\n\n"
-                            f"[Available timezones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)")
+            if reminder.creator == evt.sender or user_power >= self.admin_power_level:
+                await reminder.cancel()
+            else:
+                await evt.reply(f"Power level of {self.admin_power_level} is required")
 
+        await evt.react("👍")
 
     @command.passive(regex=r"(?:\U0001F44D[\U0001F3FB-\U0001F3FF]?)",
                      field=lambda evt: evt.content.relates_to.key,
@@ -396,7 +340,6 @@ class ReminderBot(Plugin):
         if reminder:
             await reminder.add_subscriber(user_id=evt.sender, subscribing_event=evt.event_id)
 
-
     @event.on(EventType.ROOM_REDACTION)
     async def redact(self, evt: RedactionEvent) -> None:
         """Unsubscribe from a reminder by redacting the message"""
@@ -409,41 +352,39 @@ class ReminderBot(Plugin):
                     await reminder.cancel(redact_confirmation=True)
                 break
 
-
     @event.on(EventType.ROOM_TOMBSTONE)
     async def tombstone(self, evt: StateEvent) -> None:
         """If a room gets upgraded or replaced, move any reminders to the new room"""
         if evt.content.replacement_room:
             await self.db.update_room_id(old_id=evt.room_id, new_id=evt.content.replacement_room)
 
-    def _help_message(self) -> str:
-        return f"""
-**⏰ Maubot [Reminder](https://github.com/MxMarx/reminder) plugin**\\
-TLDR: `!{self.base_command[0]} every friday 3pm take out the trash` `!{self.base_command[0]} {self.cancel_command[0]} take out the trash`
-
-**Creating optionally recurring reminders:**
-{CommandSyntax.REMINDER_CREATE.value.format(base_command=self.base_command[0],
-                                            base_aliases="|".join(self.base_command))}
-
-**Creating agenda items**
-{CommandSyntax.AGENDA_CREATE.value.format(agenda_command="|".join(self.agenda_command))}
-
-**Listing active reminders**
-{CommandSyntax.REMINDER_LIST.value.format(base_command=self.base_command[0])}
-
-**Deleting reminders**
-
-{CommandSyntax.REMINDER_CANCEL.value.format(base_command=self.base_command[0],
-                                            cancel_command=self.cancel_command[0],
-                                            cancel_aliases="|".join(self.cancel_command))}
-
-**Rescheduling**
-
-{CommandSyntax.REMINDER_RESCHEDULE.value.format(base_command=self.base_command[0])}
-
-**Settings**
+    @lunch.subcommand("help", help="Show the help")
+    async def help(self, evt: MessageEvent) -> None:
+        await evt.reply(self._help_message())
 
-{CommandSyntax.REMINDER_SETTINGS.value.format(base_command=self.base_command[0],
-                                              default_tz=self.db.defaults.timezone,
-                                              default_locale=self.db.defaults.locale)}
-"""
\ No newline at end of file
+    def _help_message(self) -> str:
+        bc = f"!{self.base_command[0]}"
+        hc = f"`!{'`, `!'.join(self.hunger_command)}`"
+        default_facilities_markdown = '\n- '.join(self.config["default_facilities"])
+        return (f"Type `{bc}` for available subcommands and syntax\n\n"
+                f"Type `{bc} menu` to show the lunch menus of the day\n\n"
+                f"By default the menus for then following canteens are shown:\n"
+                f"- {default_facilities_markdown}\n\n"
+                f"Type `{bc} canteens` to show all available canteens\n\n"
+                f"Type `{bc} config` for configuration settings and syntax\n\n"
+                f"Type `{bc} config canteen <canteens>` to configure other canteens.\n\n"
+                f"Replace `<canteens>` with a comma-separated list of canteen names,\n"
+                f"a comma-separated list of sequences of characters matching parts of\n"
+                f"canteen names (example: `poly,food market,fusion`) or `all`.\n"
+                f"This will store your canteen selection and remember it for any commands\n"
+                f"or reminders without explicit `[canteens]` selection.\n\n"
+                f"Type `{bc} config language de` to show menus in German\n\n"
+                f"Type `{bc} config price off` to hide menu prices\n\n"
+                f"Type `{bc} remind 11:00` to schedule a reminder in the room.\n"
+                f"The bot will then send the lunch menu every weekday at the specified time.\n"
+                f"A power level of {self.admin_power_level} is required for reminders.\n\n"
+                f"React with \U0001F44D to any `{bc} remind` command message\n"
+                f"to get pinged (mentioned) in the reminder.\n\n"
+                f"Type `{bc} cancel` in a new message to cancel all reminders in the room\n"
+                f"or reply to a reminder to cancel a specific reminder\n\n"
+                f"The following commands are aliases for the `{bc} menu` subcommand: {hc}")
diff --git a/reminder/db.py b/reminder/db.py
index be60f75127d8b704eabbe5ba355016979cbd68ff..fdf58ec5a205e7b7401031ef45cdd3876d0050bb 100644
--- a/reminder/db.py
+++ b/reminder/db.py
@@ -16,17 +16,16 @@
 from __future__ import annotations
 import logging
 
-from typing import Optional, Iterator, Dict, List, DefaultDict
+from typing import Dict, DefaultDict, Literal, TYPE_CHECKING
 from datetime import datetime
 from collections import defaultdict
-from .util import validate_timezone, validate_locale, UserInfo
+from .util import validate_timezone, validate_locale, validate_price, validate_facilities, UserInfo
 
 import pytz
 
 from mautrix.util.async_db import Database
 
 from mautrix.types import UserID, EventID, RoomID
-from typing import Dict, Literal, TYPE_CHECKING
 
 
 if TYPE_CHECKING:
@@ -48,20 +47,22 @@ class ReminderDatabase:
         self.cache = defaultdict()
         self.defaults = defaults
 
-
     async def get_user_info(self, user_id: UserID) -> UserInfo:
         """ Get the timezone and locale for a user. Data is cached in memory.
         Args:
             user_id: ID for the user to query
         Returns:
-            UserInfo: a dataclass with keys: 'locale' and 'timezone'
+            UserInfo: a dataclass with keys: 'locale', 'timezone', 'price' and 'facilities'
         """
         if user_id not in self.cache:
-            query = "SELECT timezone, locale FROM user_settings WHERE user_id = $1"
+            query = ("SELECT timezone, locale, price, facilities"
+                     " FROM user_settings WHERE user_id = $1")
             row = dict(await self.db.fetchrow(query, user_id) or {})
 
             locale = row.get("locale", self.defaults.locale)
             timezone = row.get("timezone", self.defaults.timezone)
+            price = row.get("price", self.defaults.price)
+            facilities = row.get("facilities", self.defaults.facilities)
 
             # If fetched locale is invalid, use default one
             if not locale or not validate_locale(locale):
@@ -71,19 +72,29 @@ class ReminderDatabase:
             if not timezone or not validate_timezone(timezone):
                 timezone = self.defaults.timezone
 
-            self.cache[user_id] = UserInfo(locale=locale, timezone=timezone)
+            if not price or not validate_price(price):
+                price = self.defaults.price
 
-        return self.cache[user_id]
+            if not facilities or not validate_facilities(facilities):
+                facilities = self.defaults.facilities
+
+            self.cache[user_id] = UserInfo(locale=locale,
+                                           timezone=timezone,
+                                           price=price,
+                                           facilities=facilities)
 
+        return self.cache[user_id]
 
-    async def set_user_info(self, user_id: UserID, key: Literal["timezone", "locale"], value: str) -> None:
+    async def set_user_info(self, user_id: UserID,
+                            key: Literal["timezone", "locale", "price", "facilities"],
+                            value: str) -> None:
         # Make sure user_info is populated first
         await self.get_user_info(user_id)
         # Update cache
         setattr(self.cache[user_id], key, value)
         # Update the db
         q = """
-        INSERT INTO user_settings (user_id, {0}) 
+        INSERT INTO user_settings (user_id, {0})
         VALUES ($1, $2) ON CONFLICT (user_id) DO UPDATE SET {0} = EXCLUDED.{0}
         """.format(key)
         await self.db.execute(q, user_id, value)
@@ -114,15 +125,30 @@ class ReminderDatabase:
             reminder.is_agenda,
             reminder.creator)
 
-
     async def load_all(self, bot: ReminderBot) -> Dict[EventID, Reminder]:
         """ Load all reminders in the database and return them as a dict for the main bot
         Args:
-            bot: it feels dirty to do it this way, but it seems to work and make code cleaner but feel free to fix this
+            bot: it feels dirty to do it this way, but it seems to work and make code cleaner
+                 but feel free to fix this
         Returns:
             a dict of Reminders, with the event id as the key.
         """
-        rows = await self.db.fetch("""
+        rows_reminders = await self.db.fetch("""
+                SELECT
+                    event_id,
+                    room_id,
+                    message,
+                    reply_to,
+                    start_time,
+                    recur_every,
+                    cron_tab,
+                    is_agenda,
+                    confirmation_event,
+                    creator
+                FROM reminder
+            """)
+
+        rows_subscribers = await self.db.fetch("""
                 SELECT
                     event_id,
                     room_id,
@@ -138,14 +164,33 @@ class ReminderDatabase:
                     creator
                 FROM reminder NATURAL JOIN reminder_target
             """)
-        logger.debug(f"Loaded {len(rows)} reminders")
+
+        logger.info(f"Loaded {len(rows_reminders)} reminders"
+                    f" with {len(rows_subscribers)} subscribers")
+
+        rows = rows_reminders + rows_subscribers
         reminders = {}
+
         for row in rows:
-            # Reminder subscribers are stored in a separate table instead of in an array type for sqlite support
-            if row["event_id"] in reminders:
-                # reminders[row["event_id"]].subscribed_users.append(row["user_id"])
-                reminders[row["event_id"]].subscribed_users[row["user_id"]] = row["subscribing_event"]
-                continue
+            if "user_id" in row.keys():
+                # If a row has the key `"user_id:` it is a subscriber.
+                # Reminder subscribers are stored in a separate table
+                # instead of in an array type for sqlite support.
+                # So we need to handle them here and add subscribers to reminders
+                if row["event_id"] in reminders:
+                    rid = row["event_id"]
+                    sid = row["subscribing_event"]
+                    uid = row["user_id"]
+                    # Reminders with already existing event_id in reminders are just subscribers
+                    reminders[rid].subscribed_users[sid] = uid
+                    # Reminder already exists
+                    continue
+
+                # New reminder with subscriber
+                subscribed_users = {row["subscribing_event"]: row["user_id"]}
+            else:
+                # New reminder without subscriber
+                subscribed_users = {}
 
             start_time = datetime.fromisoformat(row["start_time"]) if row["start_time"] else None
 
@@ -165,6 +210,8 @@ class ReminderDatabase:
                         await self.delete_reminder(row["event_id"])
                         continue
 
+            logger.info(f"load reminder: {row['event_id']}")
+
             reminders[row["event_id"]] = Reminder(
                 bot=bot,
                 event_id=row["event_id"],
@@ -175,9 +222,9 @@ class ReminderDatabase:
                 recur_every=row["recur_every"],
                 cron_tab=row["cron_tab"],
                 is_agenda=row["is_agenda"],
-                subscribed_users={row["subscribing_event"]: row["user_id"]},
+                subscribed_users=subscribed_users,
                 creator=row["creator"],
-                user_info= await self.get_user_info(row["creator"]),
+                user_info=await self.get_user_info(row["creator"]),
                 confirmation_event=row["confirmation_event"],
             )
 
@@ -199,6 +246,7 @@ class ReminderDatabase:
             start_time.replace(microsecond=0).isoformat(),
             event_id
         )
+
     async def update_room_id(self, old_id: RoomID, new_id: RoomID) -> None:
         await self.db.execute("""
             UPDATE reminder
@@ -231,4 +279,4 @@ class ReminderDatabase:
             WHERE event_id = $2
         """,
             confirmation_event,
-            event_id)
\ No newline at end of file
+            event_id)
diff --git a/reminder/ethz.py b/reminder/ethz.py
new file mode 100644
index 0000000000000000000000000000000000000000..b0ce478cccd9e96cdbfcccce402a925ba45ec874
--- /dev/null
+++ b/reminder/ethz.py
@@ -0,0 +1,126 @@
+# ethzlunch - A maubot plugin for the canteen lunch menus at ETH Zurich.
+# Copyright (C) 2024 Sven Mäder
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+import re
+from typing import List, Dict
+from datetime import date
+
+client_id = "?client-id=ethz-wcms"
+default_meal_time_names = ["lunch", "mittag"]
+
+
+def parse_facilities(data: Dict) -> Dict:
+    data = data["facility-array"]
+    return {d["facility-name"]: d["facility-id"] for d in data}
+
+
+def filter_facilities(facilities: Dict, facilities_filter: str) -> Dict:
+    filter_list = list(filter(len, re.split(r"\s?,\s?|\s?\n+\s?", facilities_filter)))
+    return {k: v for k, v in facilities.items() if any(f.lower() in k.lower() for f in filter_list)}
+
+
+def markdown_facilities(facilities: Dict) -> str:
+    return "\n".join(['- ' + m for m in sorted(facilities)])
+
+
+def parse_menus(data: Dict, facilities: Dict, customer: str = "int",  # noqa: C901
+                meal_time_names: List = default_meal_time_names) -> Dict:
+    menus = {}
+    data = data["weekly-rota-array"]
+
+    for facility_name, facility_id in facilities.items():
+        weekday = date.today().weekday()
+        menu = next(filter(lambda m: m["facility-id"] == facility_id, data), None)
+
+        if not menu:
+            menus[facility_name] = None
+            continue
+
+        day = menu["day-of-week-array"][weekday]
+
+        if "opening-hour-array" in day and day["opening-hour-array"]:
+            oha = day["opening-hour-array"][0]
+            open_hours_from = oha["time-from"]
+            open_hours_to = oha["time-to"]
+            open_hours = f"{open_hours_from} - {open_hours_to}"
+        else:
+            menus[facility_name] = None
+            continue
+
+        if "meal-time-array" in oha and oha["meal-time-array"]:
+            mta = oha["meal-time-array"]
+        else:
+            menus[facility_name] = None
+            continue
+
+        for mt in mta:
+            mt_name = mt["name"].lower()
+
+            if not any(mtn in mt_name for mtn in meal_time_names):
+                continue
+
+            mt_from = mt["time-from"]
+            mt_to = mt["time-to"]
+            time = f"{mt_from} - {mt_to}"
+            meals = {}
+
+            if "line-array" not in mt:
+                menus[facility_name] = {"open": open_hours, "time": time, "meals": None}
+                continue
+
+            for meal in mt["line-array"]:
+                try:
+                    station = meal["name"].strip()
+                    name = meal["meal"]["name"].strip()
+                    description = meal["meal"]["description"].strip()
+                    image_url = ""
+                    if "image-url" in meal["meal"]:
+                        image_url = meal["meal"]["image-url"].strip()
+                    image = image_url + client_id if image_url else ""
+                    price = ""
+
+                    if "meal-price-array" in meal["meal"]:
+                        for mp in meal["meal"]["meal-price-array"]:
+                            if customer in mp["customer-group-desc-short"].lower():
+                                price = mp["price"]
+
+                    meals[station] = {"name": name, "description": description,
+                                      "price": price, "image": image}
+                except KeyError:
+                    continue
+
+            menus[facility_name] = {"open": open_hours, "time": time, "meals": meals}
+
+    return menus
+
+
+def markdown_menus(menus: Dict) -> str:
+    md = ""
+
+    for facility, value in dict(sorted(menus.items())).items():
+        time = value["time"] if value and value['meals'] else "no menu"
+        md += f"#### {facility.lower()} ({time})\n"
+
+        if value and value['meals']:
+            for meal, value in dict(sorted(value['meals'].items())).items():
+                md += "- "
+                md += f"**{meal.lower()}** "
+                md += f"[{value['name'].lower()}]({value['image']})"
+                if value['price']:
+                    md += f" [{float(value['price']):.2f}]"
+                md += f": {value['description'].lower()}"
+                md += "\n"
+
+    return md
diff --git a/reminder/migrations.py b/reminder/migrations.py
index 276e7fc6026e186d5e085eccecaef9b830413287..6bfb192ed34c5bb991eef9da2f7e548d3a81a078 100644
--- a/reminder/migrations.py
+++ b/reminder/migrations.py
@@ -33,7 +33,6 @@ async def upgrade_v1(conn: Connection, scheme: Scheme) -> None:
             is_agenda   BOOL,                   /* agendas are alarms that don't trigger */
             confirmation_event TEXT,            /* event_id of the confirmation message, so that we can delete the confirmation if the reminder is deleted */
             PRIMARY KEY (event_id)
-            
         )"""
     )
 
@@ -52,6 +51,8 @@ async def upgrade_v1(conn: Connection, scheme: Scheme) -> None:
             user_id     VARCHAR(255) NOT NULL,  /* user_id */
             timezone    TEXT,                   /* user's timezone, e.g. America/Los_Angeles, PST. see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones */
             locale      TEXT,                   /* user's locale or langauge, e.g. en, en-AU, fr, fr-CA. See https://dateparser.readthedocs.io/en/latest/supported_locales.html */
+            price       TEXT,                   /* user's price category */
+            facilities  TEXT,                   /* user's facilities filter list */
             PRIMARY KEY (user_id)
         )"""
     )
diff --git a/reminder/reminder.py b/reminder/reminder.py
index 291a178e47f368c02e1296f22f6d433b998a779a..1b1cb9784a5eebae8b4e1efc3eeae37a97d70364 100644
--- a/reminder/reminder.py
+++ b/reminder/reminder.py
@@ -8,13 +8,11 @@ from apscheduler.triggers.cron import CronTrigger
 from apscheduler.triggers.date import DateTrigger
 
 from mautrix.util import markdown
-
 from mautrix.types import (Format, MessageType, TextMessageEventContent, UserID, EventID, RoomID)
-
 from .util import CommandSyntaxError, CommandSyntax, make_pill, parse_date, UserInfo, format_time
 
 try:
-    from cron_descriptor import Options, CasingTypeEnum, DescriptionTypeEnum, ExpressionDescriptor
+    from cron_descriptor import CasingTypeEnum, ExpressionDescriptor
     USE_CRON_DESCRIPTOR = True
 except ImportError:
     USE_CRON_DESCRIPTOR = False
@@ -24,6 +22,7 @@ if TYPE_CHECKING:
 
 logger = logging.getLogger(__name__)
 
+
 class Reminder(object):
     def __init__(
         self,
@@ -49,14 +48,16 @@ class Reminder(object):
             room_id: The ID of the room the reminder should appear in
             message: The text to include in the reminder message
             event_id: The event ID of the message creating the reminder
-            reply_to: The event ID of the message the reminder is replying to if applicable, so we can link it in the reply
+            reply_to: The event ID of the message the reminder is replying to if applicable,
+                      so we can link it in the reply
             start_time: When the reminder should first go off
             recur_every: date string to parse to schedule the next recurring reminder
             cron_tab: cron text
             subscribed_users: dict of subscribed users with corresponding subscription events
             creator: ID of the creator
             user_info: contains timezone and locale for the person who created the reminder
-            confirmation_event: EventID of the confirmation message. Store this so it can be redacted when the reminder is removed.
+            confirmation_event: EventID of the confirmation message.
+                                Store this so it can be redacted when the reminder is removed.
             is_agenda: Agenda items are reminders that never fire.
         """
         self.bot = bot
@@ -72,22 +73,20 @@ class Reminder(object):
         self.confirmation_event = confirmation_event
         self.is_agenda = is_agenda
 
-        # Schedule the reminder
-
-        # TODO add agenda
         trigger = None
         self.job = None
 
         if not is_agenda:
             # Determine how the reminder is triggered.
             # For both once-off and recurring reminders, user date trigger (runs once).
-            # Recurring reminders are rescheduled when the job runs, so we don't need to worry about them here.
+            # Recurring reminders are rescheduled when the job runs,
+            # so we don't need to worry about them here.
             if cron_tab:
                 try:
                     self.cron_tab = cron_tab.removeprefix("cron")
                     trigger = CronTrigger.from_crontab(self.cron_tab, timezone=user_info.timezone)
                 except ValueError as e:
-                    raise CommandSyntaxError(f"The crontab `{self.cron_tab}` is invalid. \n\n\t{str(e)}",
+                    raise CommandSyntaxError(f"The crontab is invalid. \n\n\t{str(e)}",
                                              CommandSyntax.CRON_EXAMPLE)
 
             elif recur_every and start_time < datetime.now(tz=pytz.UTC):
@@ -114,41 +113,64 @@ class Reminder(object):
             self.job = self.bot.scheduler.add_job(self._fire, trigger=trigger, id=self.event_id)
             await self.bot.db.reschedule_reminder(start_time=start_time, event_id=self.event_id)
 
-
         # Check if the user is rate limited
-        reminder_count = user_info.check_rate_limit(max_calls=self.bot.config["rate_limit"],
-                                                    time_window=self.bot.config["rate_limit_minutes"])
-
+        rate_limit = self.bot.config["rate_limit"]
+        rate_limit_minutes = self.bot.config["rate_limit_minutes"]
+        reminder_count = user_info.check_rate_limit(max_calls=rate_limit,
+                                                    time_window=rate_limit_minutes)
 
         # Send the message to the room if we aren't rate limited
         if reminder_count > self.bot.config["rate_limit"]:
             logger.debug(f"User {self.creator} is rate limited skipping reminder: {self.message}")
         else:
-            # Build the message with the format "(users to ping) ⬆️(link to the reminder): message text [next run]
-            # Note: ️using "⬆️" as a link seems to have trouble rendering in element for android, but "⬆" works and element on my PC still renders it as the emoji
+            # Build the message with the format:
+            # (users to ping) ⬆️(link to the reminder): message text [next run]
+            # Note: ️using "⬆️" as a link seems to have trouble rendering in element for android,
+            # but "⬆" works and element on my PC still renders it as the emoji
             targets = list(self.subscribed_users.values())
             link = f"https://matrix.to/#/{self.room_id}/{self.event_id}"
             users = " ".join([(await make_pill(user_id=user_id, client=self.bot.client))
-                                  for user_id in targets])
+                              for user_id in targets])
+
+            body = ""
+
+            if users:
+                body += f"{users}: "
 
-            body = f"{users}: [⬆]({link}) {self.message}"
+            body += f"[🍔]({link}) Reminder"
+
+            if self.message:
+                body += f" for `{self.message}`"
 
             if self.recur_every or self.cron_tab:
-                body += f"\n\nReminding again {self.formatted_time(user_info)}." \
-                        f" Reply `!{self.bot.base_command[0]} {self.bot.cancel_command[0]}` to stop."
+                body += f" {self.formatted_time(user_info)}."
+                # body += f" Reply `!{self.bot.base_command[0]} cancel` to stop."
 
             # Warn the user before rate limiting happens
             if reminder_count == self.bot.config["rate_limit"]:
                 body += f"\n\n*You've reached the rate limit " \
-                        f"({self.bot.config['rate_limit']} per {self.bot.config['rate_limit_minutes']} minutes). " \
+                        f"({self.bot.config['rate_limit']} per " \
+                        f"{self.bot.config['rate_limit_minutes']} minutes). " \
                         f"Any upcoming reminders might be ignored!*"
 
-            # Create the message, and include all the data necessary to reschedule the reminder in org.bytemarx.reminder
+            menus = await self.bot.get_markdown_menus(user=self.creator,
+                                                      facilities_filter=self.message)
+
+            if not menus:
+                menus = "No results"
+
+            body += f"\n\n{menus}"
+
+            # Create the message, and include all the data necessary to reschedule the reminder
             content = TextMessageEventContent(
-                msgtype=MessageType.TEXT, body=body, format=Format.HTML, formatted_body=markdown.render(body))
-            content["org.bytemarx.reminder"] = {"id": self.event_id,
-                                              "message": self.message,
-                                              "reply_to": self.reply_to}
+                msgtype=MessageType.TEXT,
+                body=body,
+                format=Format.HTML,
+                formatted_body=markdown.render(body))
+
+            content["ch.ethz.phys.lunch"] = {"id": self.event_id,
+                                             "message": self.message,
+                                             "reply_to": self.reply_to}
 
             # Add subscribed users to MSC3952 mentions
             content["m.mentions"] = {"room": True} if "@room" in targets else {"user_ids": targets}
@@ -163,7 +185,6 @@ class Reminder(object):
             # We set cancel_alarm to False here else the associated alarms wouldn't even fire
             await self.cancel(redact_confirmation=False)
 
-
     async def cancel(self, redact_confirmation: bool = False):
         """Cancels a reminder and all recurring instances
 
@@ -192,7 +213,6 @@ class Reminder(object):
             await self.bot.db.remove_subscriber(subscribing_event=subscribing_event)
             del self.subscribed_users[subscribing_event]
 
-
     def formatted_time(self, user_info: UserInfo):
         """
         Format the next run time. as
@@ -203,21 +223,31 @@ class Reminder(object):
 
         """
         if self.is_agenda:
-            return format_time(self.start_time, user_info=user_info, time_format=self.bot.config['time_format'])
+            return format_time(self.start_time,
+                               user_info=user_info,
+                               time_format=self.bot.config['time_format'])
         else:
-            next_run = format_time(self.job.next_run_time, user_info=user_info, time_format=self.bot.config['time_format'])
+            next_run = format_time(self.job.next_run_time,
+                                   user_info=user_info,
+                                   time_format=self.bot.config['time_format'])
             if self.cron_tab:
                 # TODO add languages
                 if USE_CRON_DESCRIPTOR:
-                    return f"{ExpressionDescriptor(self.cron_tab, casing_type=CasingTypeEnum.LowerCase)} (`{self.cron_tab}`), next run {next_run}"
+                    ed = ExpressionDescriptor(self.cron_tab,
+                                              casing_type=CasingTypeEnum.LowerCase,
+                                              use_24hour_time_format=True)
+                    return f"{ed}"
+                    # return f"{ed} (`{self.cron_tab}`), next run {next_run}"
                 else:
-                    return f"`{self.cron_tab}`, next run at {next_run}"
+                    return f"`{self.cron_tab}`"
+                    # return f"`{self.cron_tab}`, next run at {next_run}"
             elif self.recur_every:
                 return f"every {self.recur_every}, next run at {next_run}"
-            else: # once-off reminders
+            else:  # once-off reminders
                 return next_run
 
     async def set_confirmation(self, confirmation_event: EventID):
         """ Set the confirmation message so that it can be redacted if the message is deleted"""
         self.confirmation_event = confirmation_event
-        await self.bot.db.set_confirmation_event(event_id=self.event_id, confirmation_event=confirmation_event)
+        await self.bot.db.set_confirmation_event(event_id=self.event_id,
+                                                 confirmation_event=confirmation_event)
diff --git a/reminder/util.py b/reminder/util.py
index 50a81016ab74699bdb2395c2ff9f57712e05197b..ac410ebab740a2dca6f752702a04679db25c287a 100644
--- a/reminder/util.py
+++ b/reminder/util.py
@@ -19,7 +19,7 @@ import re
 from itertools import islice
 from collections import deque
 
-from typing import Optional, Dict, List, Tuple, TYPE_CHECKING
+from typing import Tuple
 from datetime import datetime, timedelta
 from attr import dataclass
 from dateparser.search import search_dates
@@ -29,97 +29,22 @@ import pytz
 from enum import Enum
 
 from maubot.client import MaubotMatrixClient
-from mautrix.types import UserID, RoomID, EventID
-from maubot.handlers.command import  ArgumentSyntaxError
-
-if TYPE_CHECKING:
-    from .reminder import Reminder
+from mautrix.types import UserID
 
 logger = logging.getLogger(__name__)
 
 
 class CommandSyntax(Enum):
-    REMINDER_CREATE = """
-`!{base_aliases} <date> <message>` Adds a reminder
-* `!{base_command} 8 hours buy more pumpkins`
-* `!{base_command} 2023-11-30 15:00 befriend rats`
-* `!{base_command} abolish closed-access journals at 3pm tomorrow`
-* `July 2`, `tuesday at 2pm`, `8pm`, `20 days`, `4d`, `2wk`, ...
-* Dates doesn't need to be at the beginning of the string, but parsing works better if they are.
-
-`!{base_command} [room] [every] ...`
-* `[room]` pings the whole room
-* `[every]` create recurring reminders `!{base_command} every friday 3pm take out the trash`
-
-`!{base_command} [room] <cron> <message>` Schedules a reminder using a crontab syntax
-* `!{base_command} cron 30 9 * * mon-fri do something` sets reminders for 9:30am, Monday through Friday.
-* `!{base_command} cron` lists more examples
-
-You can also reply to any message with `!{base_command} ...` to get reminded about that message.\\
-To get pinged by someone else's reminder, react to their message with 👍.
-"""
-
-    AGENDA_CREATE = """
-`!{agenda_command} [room] <message>` creates an agenda item. Agenda items are like reminders but don't have a time, for things like to-do lists.
-    """
-
-    REMINDER_LIST = """
-`!{base_command} list [all] [my] [subscribed]` lists all reminders in a room 
-* `all` lists all reminders from every room
-* `my` lists only reminders you created
-* `subscribed` lists only reminders you are subscribed to
-    """
-
-    REMINDER_CANCEL = """
-Cancel reminders by removing the message creating it, unsubscribe by removing your upvote.\\
-Cancel recurring reminders by replying with `!{base_command} {cancel_aliases}` 
-* `!{base_command} {cancel_aliases} <ID>` deletes a reminder matching the 4 letter ID shown by `list`
-* `!{base_command} {cancel_aliases} <message>` deletes a reminder **beginning with** <message>
-    * e.g. `!remind {cancel_command} buy more` would delete the reminder `buy more pumpkins`
-"""
-
-    REMINDER_RESCHEDULE = """
-Reminders can be rescheduled by replying to the ping with `!{base_command} <new_date>`
-"""
-
-    REMINDER_SETTINGS = """
-Dates are parsed using your [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zone) and [locale](https://dateparser.readthedocs.io/en/latest/supported_locales.html).
-Defaults are `{default_tz}` and `{default_locale}`
-* `!{base_command} tz|timezone [new-timezone]` view or set your timezone
-* `!{base_command} locale [new-locale]` view or set your locale
-"""
-
     PARSE_DATE_EXAMPLES = "Examples: `Tuesday at noon`, `2023-11-30 10:15 pm`, `July 2`, `6 hours`, `8pm`, `4d`, `2wk`"
-
-    CRON_EXAMPLE = """
-```
-*	any value
-,	value list separator
--	range of values
-/	step 
-
-┌─────── minute (0 - 59)
-│ ┌─────── hour (0 - 23)
-│ │ ┌─────── day of the month (1 - 31)
-│ │ │ ┌─────── month (1 - 12)
-│ │ │ │ ┌─────── weekday (0 - 6) (Sunday to Saturday)                             
-│ │ │ │ │
-* * * * * <message>
-```
-
-```
-30 9 * * *              Every day at 9:30am
-0/30 9-17 * * mon-fri   Every 30 minutes from 9am to 5pm, Monday through Friday
-0 14 1,16 * *           2:00pm on the 1st and 16th day of the month
-0 0 1-7 * mon           First Monday of the month at midnight
-```
- """
+    CRON_EXAMPLE = "Valid weekday examples: `mon-fri`, `mon,tue,thu`, `mon-wed,fri`"
 
 
 @dataclass
 class UserInfo:
     locale: str = None
     timezone: str = None
+    price: str = None
+    facilities: str = None
     last_reminders: deque = deque()
 
     def check_rate_limit(self, max_calls=5, time_window=60) -> int:
@@ -138,6 +63,7 @@ class UserInfo:
             self.last_reminders.append(now)
         return len(self.last_reminders)
 
+
 class CommandSyntaxError(ValueError):
     def __init__(self, message: str, command: CommandSyntax | None = None):
         """ Format error messages with examples """
@@ -147,19 +73,30 @@ class CommandSyntaxError(ValueError):
             message += "\n\n" + command.value
         self.message = message
 
+
 def validate_timezone(tz: str) -> bool | str:
     try:
         return dateparser.utils.get_timezone_from_tz_string(tz).tzname(None)
     except pytz.UnknownTimeZoneError:
         return False
 
+
 def validate_locale(locale: str):
     try:
         return dateparser.languages.loader.LocaleDataLoader().get_locale(locale)
     except ValueError:
         return False
 
-def parse_date(str_with_time: str, user_info: UserInfo, search_text: bool=False) -> Tuple[datetime, str]:
+
+def validate_price(price: str) -> bool:
+    return price.lower() in ["int", "ext", "stud", "off"]
+
+
+def validate_facilities(facilities: str) -> bool:
+    return isinstance(facilities, str)
+
+
+def parse_date(str_with_time: str, user_info: UserInfo, search_text: bool = False) -> Tuple[datetime, str]:
     """
     Extract the date from a string.
 
@@ -217,11 +154,13 @@ def parse_date(str_with_time: str, user_info: UserInfo, search_text: bool=False)
 
     return date, date_str
 
+
 def pluralize(val: int, unit: str) -> str:
     if val == 1:
         return f"{val} {unit}"
     return f"{val} {unit}s"
 
+
 def format_time(time: datetime, user_info: UserInfo, time_format: str = "%-I:%M%P %Z on %A, %B %-d %Y") -> str:
     """
     Format time as something readable by humans.
@@ -237,23 +176,23 @@ def format_time(time: datetime, user_info: UserInfo, time_format: str = "%-I:%M%
 
     # If the date is coming up in less than a week, print the two most significant figures of the duration
     if abs(delta) <= timedelta(days=7):
-            parts = []
-            if delta.days > 0:
-                parts.append(pluralize(delta.days, "day"))
-            hours, seconds = divmod(delta.seconds, 60)
-            hours, minutes = divmod(hours, 60)
-            if hours > 0:
-                parts.append(pluralize(hours, "hour"))
-            if minutes > 0:
-                parts.append(pluralize(minutes, "minute"))
-            if seconds > 0:
-                parts.append(pluralize(seconds, "second"))
-
-            formatted_time = " and ".join(parts[0:2])
-            if time > now:
-                formatted_time = "in " + formatted_time
-            else:
-                formatted_time = formatted_time + " ago"
+        parts = []
+        if delta.days > 0:
+            parts.append(pluralize(delta.days, "day"))
+        hours, seconds = divmod(delta.seconds, 60)
+        hours, minutes = divmod(hours, 60)
+        if hours > 0:
+            parts.append(pluralize(hours, "hour"))
+        if minutes > 0:
+            parts.append(pluralize(minutes, "minute"))
+        if seconds > 0:
+            parts.append(pluralize(seconds, "second"))
+
+        formatted_time = " and ".join(parts[0:2])
+        if time > now:
+            formatted_time = "in " + formatted_time
+        else:
+            formatted_time = formatted_time + " ago"
     else:
         formatted_time = time.astimezone(
             dateparser.utils.get_timezone_from_tz_string(user_info.timezone)).strftime(time_format)
@@ -282,4 +221,3 @@ async def make_pill(user_id: UserID, display_name: str = None, client: MaubotMat
 
     # return f'<a href="https://matrix.to/#/{user_id}">{display_name}</a>'
     return f'[{display_name}](https://matrix.to/#/{user_id})'
-
diff --git a/screenshot.png b/screenshot.png
deleted file mode 100644
index 844341b41c443dcc6b2cf441e20c5fb18c73836e..0000000000000000000000000000000000000000
Binary files a/screenshot.png and /dev/null differ