NinjaCheetah c7639de06b
Added dining location menus from FD MealPlanner API
Dining locations that also exist on FD MealPlanner now have a "View Menu" button under the favorite/OnDemand/map buttons that take you to a new view that pulls the location's menu from FD MealPlanner. You can view all of the menu items that have actually been added to that site, and tap them for more details (which will be expanded on later). Searching the item list is supported, with more filtering options coming in the next update. Meal periods can be browsed using the clock button in the top right for locations that are open more than once per day.
Other changes:
- App renamed from "RIT Dining" to "TigerDine" to not get me in trouble for an App Store release
- Slightly changed the way that dining locations' short descriptions and current open times are displayed in the detail view
- Fixed the box truck icon used in the food truck view being squished
2025-11-08 22:33:20 -05:00

188 lines
7.2 KiB
Swift

//
// Requests.swift
// RIT Dining
//
// Created by Campbell on 8/31/25.
//
import Foundation
enum InvalidHTTPError: Error {
case invalid
}
// This code has now been mostly rewritten to be pretty and async instead of being horrifying callback based code in a context where
// callback based code made no sense. I love async!
// Get information for all dining locations.
func getAllDiningInfo(date: String?) async -> Result<DiningLocationsParser, Error> {
// The endpoint requires that you specify a date, so get today's.
let dateString: String = date ?? getTCAPIFriendlyDateString(date: Date())
let urlString = "https://tigercenter.rit.edu/tigerCenterApi/tc/dining-all?date=\(dateString)"
guard let url = URL(string: urlString) else {
return .failure(URLError(.badURL))
}
do {
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
return .failure(InvalidHTTPError.invalid)
}
let decoded = try JSONDecoder().decode(DiningLocationsParser.self, from: data)
return .success(decoded)
} catch {
return .failure(error)
}
}
// Get information for just one dining location based on its location ID.
func getSingleDiningInfo(date: String?, locId: Int) async -> Result<DiningLocationParser, Error> {
// The current date and the location ID are required to get information for just one location.
let dateString: String = date ?? getTCAPIFriendlyDateString(date: Date())
let urlString = "https://tigercenter.rit.edu/tigerCenterApi/tc/dining-single?date=\(dateString)&locId=\(locId)"
print("making request to \(urlString)")
guard let url = URL(string: urlString) else {
return .failure(URLError(.badURL))
}
do {
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
return .failure(InvalidHTTPError.invalid)
}
let decoded = try JSONDecoder().decode(DiningLocationParser.self, from: data)
return .success(decoded)
} catch {
return .failure(error)
}
}
// Get the occupancy information for a location using its MDO ID, whatever that stands for. This ID is provided alongside the other
// main ID in the data returned by the TigerCenter API.
func getOccupancyPercentage(mdoId: Int) async -> Result<Double, Error> {
let urlString = "https://maps.rit.edu/proxySearch/densityMapDetail.php?mdo=\(mdoId)"
print("making request to \(urlString)")
guard let url = URL(string: urlString) else {
return .failure(URLError(.badURL))
}
do {
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
return .failure(InvalidHTTPError.invalid)
}
let occupancy = try JSONDecoder().decode([DiningOccupancyParser].self, from: data)
if !occupancy.isEmpty {
print("current occupancy: \(occupancy[0].count)")
print("maximum occupancy: \(occupancy[0].max_occ)")
let occupancyPercentage = Double(occupancy[0].count) / Double(occupancy[0].max_occ) * 100
print("occupancy percentage: \(occupancyPercentage)%")
return .success(occupancyPercentage)
} else {
return .failure(DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Failed to decode JSON")))
}
} catch {
return .failure(error)
}
}
// Get the RIT Events food truck page and strip it down to just the part of the HTML that we'll need to parse with SwiftSoup.
func getFoodTruckPage() async -> Result<String, Error> {
let urlString = "https://www.rit.edu/events/weekend-food-trucks"
guard let url = URL(string: urlString) else {
return .failure(URLError(.badURL))
}
do {
let contents = try String(contentsOf: url)
let scheduleRegex = /<div class=\".*?field--name-field-event-description.*?\">([\s\S]*?)<\/div>/
if let match = contents.firstMatch(of: scheduleRegex) {
return .success(String(match.0))
}
return .success(contents)
} catch {
return .failure(error)
}
}
func searchFDMealPlannerLocations() async -> Result<FDSearchResponseParser, Error> {
let urlString = "https://locations.fdmealplanner.com/api/v1/location-data-webapi/search-locationByAccount?AccountShortName=RIT&pageIndex=1&pageSize=0"
guard let url = URL(string: urlString) else {
return .failure(URLError(.badURL))
}
do {
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
return .failure(InvalidHTTPError.invalid)
}
let decoded = try JSONDecoder().decode(FDSearchResponseParser.self, from: data)
return .success(decoded)
} catch {
return .failure(error)
}
}
func getFDMealPlannerOpenings(locationId: Int) async -> Result<FDMealPeriodsParser, Error> {
let urlString = "https://apiservicelocatorstenantrit.fdmealplanner.com/api/v1/data-locator-webapi/20/mealPeriods?LocationId=\(locationId)"
print("making request to \(urlString)")
guard let url = URL(string: urlString) else {
return .failure(URLError(.badURL))
}
do {
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
return .failure(InvalidHTTPError.invalid)
}
let decoded = try JSONDecoder().decode(FDMealPeriodsParser.self, from: data)
return .success(decoded)
} catch {
return .failure(error)
}
}
func getFDMealPlannerMenu(locationId: Int, accountId: Int, mealPeriodId: Int) async -> Result<FDMealsParser, Error> {
let dateString = getFDMPAPIFriendlyDateString(date: Date())
let urlString = "https://apiservicelocatorstenantrit.fdmealplanner.com/api/v1/data-locator-webapi/20/meals?menuId=0&accountId=\(accountId)&locationId=\(locationId)&mealPeriodId=\(mealPeriodId)&tenantId=20&monthId=\(Calendar.current.component(.month, from: Date()))&startDate=\(dateString)&endDate=\(dateString)"
print("making request to \(urlString)")
guard let url = URL(string: urlString) else {
return .failure(URLError(.badURL))
}
do {
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
return .failure(InvalidHTTPError.invalid)
}
let decoded = try JSONDecoder().decode(FDMealsParser.self, from: data)
return .success(decoded)
} catch {
return .failure(error)
}
}