Lazily finding appointments
I recently realised I was overdue some new glasses. My old ones were OK, but fairly scratched and I also never really liked them when I first bought them. It has also been some time since I wore contact lenses, and recently I have been making an effort to do "SPORT" (you know, that thing people do when they aren't doing computer stuff?).
Getting my glasses was easy enough. Appointment in Fielmann, pick some glasses (I had help choosing this time, and I am really happy with what we chose), picked them up, went back a few days later to get them adjusted, no problems. But when I asked them about making an appointment about contact lenses, they stopped being very helpful and asked me to try make an appointment online. The website either showed no appointments in my city, or appointments multiple weeks / months from now. Supposedly cancellations show up and you can get one quicker, but what am I going to do, check that multiple times a day? I needed to get an appointment quickly though, the sports were waiting.
Looking at the network calls when checking for available contact lens appointments, the page makes the following API calls:
GET https://termine.fielmann.de/api/v3/branches/closest/CL_CF?skip=0&locale=de-DE&address=53111
GET https://termine.fielmann.de/api/v3/times/001-0885/free/CL_CF?from=2024-10-03&days=4
GET https://termine.fielmann.de/api/v3/times/001-0836/free/CL_CF?from=2024-08-06&days=4
GET https://termine.fielmann.de/api/v3/times/001-0136/free/CL_CF?from=2024-10-03&days=4
Straightforward enough. The first request gets the branches near the postcode, the subsequent ones check for appointments within a 4 day window at each of the returned branches.
Rather than remember to check this over and over, I want to automatically get notified of the next available dates. The easiest way for me to receive these notifications is (unfortunately) discord. I already (ab)use discord for various automations, so why not another!
This year I worked on a discord bot for a game developer, just a simple currency system for his discord community to amuse themselves with. I was between projects at the time and looking for a new challenge so built the bot in rust. This is not the best application of Rust, but I had time and motivation, and fired ahead. I feel like people look down on using Rust for non-systems programming stuff but the type system is such a joy to build with, I have no regrests. So, armed with basic knowledge of discord's API, I set out to build a quick and dirty bot in rust to help me find appointments. The full code for this bot is available here
The API request we're concerned with is the /api/v3/times
call. We can write make this call in rust with the following function:
#[derive(Deserialize, Debug)]
struct FielmannTimeslot {
date: String,
timeslots: Timeslot,
}
#[derive(Deserialize, Debug)]
struct Timeslot {
from: String,
to: String,
}
async fn do_request(&self, store_code: &str) -> Result<Vec<FielmannTimeslot>, reqwest::Error> {
let url = format!(
"https://termine.fielmann.de/api/v3/times/001-{}/free/CL_CF/next",
store_code
);
let dates = self
.client // self.client is reqwest::Client
.get(url)
.send()
.await
.unwrap()
.json::<Vec<FielmannTimeslot>>()
.await
.unwrap();
Ok(dates)
}
Calling the function for our desired set of store codes, we get the dates, we can build a hashmap of stores and the next dates available for contact lens tests:
async fn get_dates(&self) -> HashMap<&String, Vec<FielmannTimeslot>> {
let mut dates = std::collections::HashMap::new();
for store in &self.stores { // self.stores is a vector of store codes
match self.do_request(store).await {
Ok(times) => dates.insert(store, times),
Err(e) => {
dbg!(e);
continue;
}
};
}
dates
}
Finally, all that's left is to send the result to Discord. I am not an expert on discord development by any means, and chose to run this as a full-fledged discord bot. I have since learned about the webhook functionality offered by discord, and that is far more appropriate for an entirely passive non-interactable bot. If I had done it that way, I would be able to POST the desired message to a webhook URL and have the message show up in a configured channel. Instead, I have used serenity/poise to build a basic bot and linked it to the server. Perhaps I will refactor this in future, because the webhook approach reduces so much complexity.
The function to send the response in our poise bot looks like this:
async fn send_result_message(&self, dates: HashMap<&String, Vec<FielmannTimeslot>>) {
let mut message = String::new();
for store in &self.stores {
let times = dates.get(store).unwrap();
let store_name = store_name_to_friendly_name(store.as_str());
message.push_str(&format!("**{}:** \n", store_name));
if times.is_empty() {
message.push_str("No dates available\n\n");
continue;
}
for time in times {
message.push_str(&format!(
"- **{}:** {} -> {}\n",
time.date, time.timeslots.from, time.timeslots.to
));
}
message.push('\n');
}
self.channel
.send_message(
&self.discord_client,
poise::serenity_prelude::CreateMessage::new().content(message),
)
.await
.unwrap();
}
Not super pretty, but low-effort was the theme I had going this week.
Some thoughts
I doubt Fielmann would be thrilled about me automating this against their API. As a low effort "please don't pay attention to these requests" I added all the same headers Firefox was sending, to lazily try and look like it was a Firefox session making the requests. Probably achieved nothing with that but I didn't get my requests blocked at all so it didn't hurt. I would much rather use the webhook approach than a full discord bot. I also did not make it configurable at all. If I ever get around to tidying up this (or if I ever need it again!) I will add configuration options and either remove the bot aspect and use webhooks or abandon discord altogether. And if I really want it to be good I will make the delivery mechanism generic, but don't hold me to that. Lastly, I gave up on Fielmann after a week and got contact lenses in another optician within 2 days.