A lightweight Telegram bot designed for Saarland University students — offering fast, clear, and focused public transport information using SaarVV and HAFAS APIs. — Try Now
🔍 What it is
UdS Fahrplan is a minimal alternative to the full Saarfahrplan app. No clutter. No overwhelming options. Just a clean Telegram interface for:
🔄 Trip planning with /trip
🕒 Live departure boards with /depart
🏠 One-tap departures from your home station with /home
Built with students and locals in mind.
✨ Features
/trip – Search from A to B in Saarland & Luxembourg
/depart – View all upcoming buses from any station
/home – Configure your home station and check buses in both directions (e.g., City ↔ Dudweiler)
/sethome – Set or update your personal home station
Intelligent Filtering – Excludes school buses, allows regional/suburban focus
Persistent Storage – Remembers user settings across bot restarts
As an UdS Student, Are you tired of seeing french fries🍟 3 times a week, or wondering when I can have the best pizza 🍕 in the Mensacafe? MensaarLecker aims to collect all the data from Menu 1, 2, and Mensacafe to trace your favourite, or Mensa Ladies’, favourite menu!
After the website is published, we noticed that people now prefer to have lunch in HTW Campus Rotenbühl. Since their menu come from the same site, it is very easy to introduce new menu to our project.
New website layout
Before we used two pages to store today’s menu and the menu history. And we think in general, all menu are simple texts, so we can put all contents into the index page without any problem. We can switch the visibility using JavaScript:
In the previous notes, we’ve already implemented all the commands we need! But there’s a question, why we need to format out query in f”{session}:{step}:{loc}”? Also, we haven’t talked about the function that connects to the SaarVV api.
We also have another study notes for all of the telegram bot projects in here, it gives you the basics on how to create your own bot and further descriptions on different functions and attributes on the package python-telegram-bot.
HTTP request on SaarVV
In Application layer, if we send a GET request to the URL, it returns the HTML of the URL. For example:
request.py
1 2 3 4 5 6 7
import requests
try: res = requests.get("https://greenmeeple.github.io/") print(res.text) except Exception as e: print(f"❌ Error fetching location matches: {e}")
Then we are basically retriving the source code the website.
This is a simple web-based tool to help you decode and encode bitmasks used by the HAFAS (HaCon Fahrplan-Auskunfts-System) API. It visually maps a decimal bitmask to the transport modes it represents and vice versa.
🔍 What is HAFAS?
HAFAS is a timetable and journey planning system widely used across European transport networks, including:
In HAFAS APIs, transport modes (ICE trains, buses, trams, ferries, etc.) are often encoded in a bitmask — a single number where each bit (1, 2, 4, 8, …) represents a specific type of transport.
✨ Features
🔢 Enter a number to decode it into selected transport modes
✅ Click transport icons to encode a bitmask value
🔄 Switch between different mode sets (DB, SaarVV, Luxembourg, HAFAS client spec)
In the previous notes, we’ve already implemented /trip and /depart based on this. Now, let’s start explaining them one by one.
We also have another study notes for all of the telegram bot projects in here, it gives you the basics on how to create your own bot and further descriptions on different functions and attributes on the package python-telegram-bot.
/sethome – Storing users’ starting station
Similarly, our main function should have app.add_handler(CommandHandler("sethome", spawn)) and InlineKeyboardMarkup[] ready, these components are defined in previous notes and they are reusable.
Next, our function has to be able to distinguish different users. Users who interact with the bot using command or tagged message will have their id stored in message.from_user.id; and query.from_user.id if users are interacting using buttons.
Then, we also need to create a database or storage file to read and write users’ stations. We decide the tool to use based on our use case and needs. Here, the data we need to store are user_id, station_name, and station_id. Which the columns are stable and small size. Therefore, it can be handled by a simple JSON file. Before we store the station, remember to check if the user already stored any station before to avoid creating duplicates.
asyncdefhandle_spawn_start(query, context, data): station = locations[data] if data in locations else data name = context.user_data.get("spawn_session", {}).get("search_s", {}).get(data, data) user_id = query.from_user.id
if os.path.exists(SPAWN_DATA): withopen(SPAWN_DATA, "r") as file: try: data_list = json.load(file) except json.JSONDecodeError: data_list = [] else: data_list = []
# Check if user already exists user_found = False for entry in data_list: if entry["user_id"] == user_id: entry["home_id"] = station entry["home_name"] = name user_found = True break
ifnot user_found: # Add new user entry data_list.append({"user_id": user_id, "home_id": station, "home_name": name})
# Save updated data withopen(SPAWN_DATA, "w") as file: json.dump(data_list, file, indent=2)
await query.edit_message_text( f"✅ User {user_id}, your home is set to {name:<20}")
asyncdefspawn(update: Update, context: ContextTypes.DEFAULT_TYPE): context.user_data["spawn_session"] = {} await update.message.reply_text(f"Hello User {update.message.from_user.id}, Set your spawn point", reply_markup=build_location_keyboard("spawn","start") )
In the previous notes, we’ve talked about our motivation and planned functions we wanted to implement. Now, let’s start explaining them one by one.
We also have another study notes for all of the telegram bot projects in here, it gives you the basics on how to create your own bot and further descriptions on different functions and attributes on the package python-telegram-bot.
/trip – A basic Station A to Station B search
We start with the basic trip searching function just like the original app. We used CommandHandler to create the command, InlineKeyboardMarkup to create buttons for stations, CallbackQueryHandler to handle the button actions, and update.message.reply_text to send message back to the users.
await update.message.reply_text( "🚌 Choose Your Starting Station.", reply_markup=build_location_keyboard("trip","start") )
Buttons for stations and departure time
When the user called the command /trip, the bot will update the message into a list of buttons thanks to the InlineKeyboardMarkup, it passes a string of message with three components, seperated with colon. f”{session}:{step}:{loc}”, it will then pass into handle_callback() and used to indentify different command using session, then it identifies the current state using steps (since stations can be either start or destination), and eventually the station details in loc will be passed to next step.
SaarFahrplan is a public transport app to provide real-time information and services related to public transportation in Saarland. Its target users are the people who live and travel in Saarland for general purpose. When it comes to a smaller group of users, for example Uni students, some functionalities might be redundant and we may optimized some common use case for better user experience.
Common use cases for students
Instead of searching different connections, Uni students’ timetable are usually consistent and repetitive. There are a few places where they always go, for example:
Go to the Uni
Go to the Mensa
Go to the Dormintory
Go to the City
Go to the Supermarket
Another scenario will be when we are in somewhere new, we would like to know how to go back home (dormitory).
Key functions on our telegram bot
Based on the above use case, So we create our own app/bot that only accommodate them.
My personal study notes on HAFAS, a public transport management system used around europe
What is HAFAS?
The HaCon Timetable Information System (HAFAS) is a software for timetable information from the company HaCon (Hannover Consulting). – Wikipedia
Basically, the entire Germany, Luxembourg, and the surrounding countries/regions use HAFAS to obtain depatures and stations details. This centralized software system can be visited using APIs. Different service providers may create their own HAFAS backend endpoint that exposes a public transport API over HTTP for customized usage. For example, you can send your HTTP requests to Deutsche Bahn (DB) if you have the access of their API, which can be found in DB API Marketplace.
What can we do with HAFAS?
There are four basic functions that the system has provided:
TripSearch – return connections from location A to location B
StationBoard – return all arrivals and departures of a certain station
LocGeoPos – return list of stations in a give range of area
LocMatch – return list of stations based on the keyword given
These includes most of the functionalities for users. For example, when we tried to plan our journey on DB navigator, the app uses TripSearch to show you connections; when we type the stations in the search box, the app LocMatch to give you related results.
Continuing from last post, we have already implemented a script that collect the Mensa menu and stored it on Google Sheets. It is time to build our web interface to connect the database.
Fetch Data from Google Sheets using Publish
First, we need to publish our spreadsheet so that it is public to fetch the data.
In the Spreadsheet, click Share → Change access to Anyone with the link.
Click File → Share → Publish to the web.
Select Entire Document → Comma-separated values (.csv) and click Publish.
Copy the public CSV link.
menu.py
1 2 3 4 5 6 7 8 9 10 11
SCRIPT_URL = {PUBLISH_LINK}
# Fetch JSON data deffetch_menu(): try: response = requests.get(SCRIPT_URL) response.raise_for_status() # Raise error if bad response return response.json() except requests.exceptions.RequestException as e: print(f"❌ Error fetching menu: {e}") return []
However, the script return no data, why?
caret.ebnf
1 2 3
Access to fetch at 'https://docs.google.com/spreadsheets/...' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
My personal experience when developing a web crawler using Selenium
Explained with examples from my Repository: MensaarLecker
Motivation
Me and my friends hatelove the UdS Mensa so much! The infinite frozen food and french fries menus give us so much energy and motivation for the 5-hour afternoon coding marathon. However, no one actually knows how many potatoes they have exterminated throughout the week. We have a genius webpage created by some Schnitzel lover. Personally, I like its minimalistic layout and determination on Schnitzel searching.
However, we want more.
It’s not just Schnitzel; we want to know everything about their menu. We want to know what’s inside the mensa ladies’ brains when they design next week’s menu.
The desire never ends. We need more data, more details, more, More, MORE!
Developing Process
Our Goal here is simple:
Scrape the Mensa menu every weekday and store it to Google Sheets
Fetch the Data Collection from Google Sheets and update the website
Implement the HTML tag <ruby> for Hexo using Tag Plugin feature. Provide auto pronounciation indication for Jyutping (Cantonese), Zhuyin (Taiwanese Mandarin), and Pinyin (Chinese Mandarin), and the default setting for general usage. Support Traditonal and Simplified Chinese characters.
Ruby (ルビ) is also known as Furigana (振り仮名). It contains two basic use cases:
To clarify or indicate the pronunciation for readers
Gikun, in which the characters have different pronunciations than they seem due to convention or for a specific context. For example, the pronunciation of 煙草 in Japanese is tabako (tobacco).
Usage
TLDR: Usage: {% tag rb|rt %}; Tag options: ruby_def, ruby_jy, ruby_py, ruby_zy.