← Back to home

Purdue Housing 2023

This is a script to automatically search for open housing spots on the Purdue housing portal.

Notes:

Using this script is a bit more complicated than just pasting it into the console in the housing portal; see the Using this script section for more details on how it works.

Apartment
Double
Quad
Single
Suite
Triple
Cary Quadrangle
Earhart
First Street Towers
Frieda Parker
Harrison
Hawkins
Hillenbrand
Honors
McCutcheon
Meredith
Meredith South
Owen
Shreve
Tarkington
Wiley
Windsor
Winifred Parker

Code:

1const roomTypeIds = [165, 167, 166];
2const buildingIds = [1, 34, 16, 6, 4, 5];
3const lowerRoomRate = 1000;
4const upperRoomRate = 20000;
5const delay = 1000;
6
7let id;
8;(async () => {
9    // Parse URL token from search param after login
10    const params = new URLSearchParams(location.search);
11    const urlToken = params.get('UrlToken');
12
13    const dateStart = params.get('DateStart');
14    const dateEnd = params.get('DateEnd');
15    const termId = params.get('TermID');
16    const classificationId = params.get('ClassificationID');
17
18    const verificationToken = document.body.innerHTML.match(/<input name="__RequestVerificationToken" type="hidden" value="(.+)"\s*\/?>/)?.[1];
19    if (!verificationToken) return log('Could not parse request verification token!');
20
21    const data = document.body.innerHTML.match(/<div class="" data-actualaddingroomtocartoption=".*" data-canaddroomstocart=".*" data-classificationid=".+" data-currencysymbol=".*" data-currentpagenumber="(.+)" data-dateend="(.+)" data-datestart="(.+)" data-filterresultshash="(.+)" data-invalidfieldresponse=".*" data-isauthenticated=".*" data-lowerroomratevalue=".*" data-maximumroomratefilterarialabel=".*" data-maximumroomratefiltervalue=".*" data-minimumroomratefilterarialabel=".*" data-minimumroomratefiltervalue=".*" data-mustselectroommessage=".+" data-portalpageid=".+" data-portalrulestatus=".*" data-processid="(.+)" data-roombaseids=".*" data-roomrateid=".*" data-roomselectionpageid="(.*)" data-showfilterswithoutrooms=".*" data-termid=".+" data-unknowninvalidfieldresponse=".*" data-upperroomratevalue=".*" data-useroommateclassifications=".*" id="page-container">/)
22    if (!data) return log('Could not parse other page data!');
23    const [, currentPageNumber, dateEndISO, dateStartISO, filterHash, processId, roomSelectionPageId] = data;
24
25    // Construct filter request body
26    const filterBody = {
27        classificationID: Number(classificationId),
28        currentPageNumber: Number(currentPageNumber),
29        termID: Number(termId),
30        filters: {
31            DateStart: dateStartISO,
32            DateEnd: dateEndISO,
33            ProfileItemID: roomTypeIds, // Room type IDs
34            RoomLocationID: buildingIds, // Building IDs
35            RoomBaseIDs: [],
36            RoomLocationAreaID: [],
37            RoomLocationFloorSuiteID: [],
38            RoomTypeID: [],
39            UseRoommateClassifications: false,
40            LowerRoomRateValue: lowerRoomRate,
41            UpperRoomRateValue: upperRoomRate,
42        }
43    }
44
45    id = setInterval(queryRooms, delay);
46
47    async function queryRooms() {
48        const res = await fetch(`https://purdue.starrezhousing.com/StarRezPortalX/General/RoomSearch/roomsearch/GetFilterResultsAuthenticated?hash=${filterHash}&pageID=${roomSelectionPageId}&processID=${processId}`, {
49            method: 'POST',
50            body: JSON.stringify(filterBody),
51            headers: {
52                __RequestVerificationToken: verificationToken,
53                'Content-Type': 'application/json',
54                'X-Requested-With': 'XMLHttpRequest'
55            }
56        });
57        if (!res.ok) return log('Error while fetching filter endpoint');
58
59        const parsed = await res.json();
60        if (!('ResultsHtml' in parsed)) {
61            log('Error response when fetching filter endpoint');
62            return console.log(parsed);
63        }
64
65        console.log(parsed.ResultsHtml);
66
67        // Search HTML response for "add to cart" buttons
68        const matches = [...parsed.ResultsHtml.matchAll(/<button aria-label="Add (.+) To Cart" class="ui-add-room-to-cart sr_button_primary sr_button" data-hash="(.+)" data-url="(.+)" title="Add To Cart" type="button">.+<\/button>/g)];
69        if (!matches.length) return log('No rooms found');
70
71        for (const [, name, hash, url] of matches) {
72            log(`Found room ${name}`);
73
74            // Match room base and rate ID from broken url data param
75            const roomMatch = url.match(/roomBaseID=(.+?)&amp;roomRateID=(.+?)&amp;/);
76            if (!roomMatch) {
77                log(`Couldn't find base and rate ID for room ${name}`)
78                continue;
79            }
80
81            // Add room to cart
82            const [, roomBaseId, roomRateId] = roomMatch;
83            const res = await fetch(`https://purdue.starrezhousing.com/StarRezPortalX/General/RoomSearch/roomsearch/AddRoomToCartForTerm?hash=${hash}&checkInDate=${dateStartISO}&checkOutDate=${dateEndISO}&termID=${termId}&pageID=${roomSelectionPageId}&roomBaseID=${roomBaseId}&roomRateID=${roomRateId}`, {
84                method: 'POST',
85                body: JSON.stringify({}),
86                headers: {
87                    __RequestVerificationToken: verificationToken,
88                    'Content-Type': 'application/json',
89                    'X-Requested-With': 'XMLHttpRequest'
90                }
91            });
92            if (!res.ok) {
93                log(`Error adding room ${name} to cart`);
94                continue;
95            }
96
97            // Construct next page request body
98            const continueBody = {
99                RoomLocationID: buildingIds,
100                ProfileItemID: roomTypeIds,
101                PageWidgetData: []
102            }
103            for (const id of [1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 13, 14, 15, 16, 17, 33, 34])
104                continueBody[`${id}-checkbox`] = buildingIds.includes(id);
105            for (const id of [165, 166, 167, 168, 169, 198])
106                continueBody[`${id}-checkbox`] = roomTypeIds.includes(id);
107
108            // Fetch next page URL and open it in a new tab if everything is successful
109            const checkoutRawUrl = await (await fetch(window.location.href, {
110                method: 'POST',
111                body: JSON.stringify(continueBody),
112                headers: {
113                    __RequestVerificationToken: verificationToken,
114                    'Content-Type': 'application/json',
115                    'X-Requested-With': 'XMLHttpRequest'
116                }
117            })).text();
118
119            window.open(checkoutRawUrl.slice(1, -1).replaceAll('\\u0026', '&'));
120            clearInterval(id);
121        }
122    }
123})();
124
125function log(m) {
126    console.log(`[${new Date().toLocaleTimeString()}]: ${m}`);
127}

Using this script

Because of how the housing portal's request hashes and dynamic routes work, this script only works on the List Rooms page (ie. the page that looks like the image on the right):

List rooms pageList rooms page

Left: Initial Selection page with no rooms available; Right: List Rooms page

If you have navigated to the Initial Selection page (left) but cannot go further due to there being no available rooms, run the following code in the console to jump to the next page:

Code:

1const rawUrl = await (await fetch(window.location.href, {
2    method: 'POST',
3    body: JSON.stringify({PageWidgetData: []})
4})).text()
5
6window.location = rawUrl.slice(1, -1).replaceAll('\\u0026', '&');

Once you're at the list rooms page, copying the original script into your console should start the room search process.

The script sends a lot of logs in the console, mostly for debugging purposes. You'll know the script has found a room when it starts opening a bunch of tabs (an imperfect solution, but one that works given the time constraints and limited opportunities to test it). These tabs will be the bed assignment pages for the room the script has just added to your cart; wait for one to load, fill it out as normal, and pray that the request goes through. Remember to hit confirm as normal on the proceeding page as well.

Other things to note

During execution, the script makes a request to the filter rooms endpoint (the same one the list rooms webpage uses) every delay milliseconds. If, for whatever reason, you want to stop sending new requests, you can clear the interval with

Code:

1clearInterval(id)

Note that at peak times, response times from the API will greatly exceed the rate at which the script makes new fetches to it (the worst I've seen were response times of almost 3 minutes). Clearing the interval does not prevent fetches that are still pending from resolving, only the creation of new ones. The list of pending requests can easily range in the thousands during peak times; to clear all requests immediately, reload the page or close the tab.

If, during execution, you want to add or remove buildings or room types from your filter, simply mutate the roomTypeIds and buildingIds arrays directly:

Code:

1buildingIds.push(1) // Add Cary to the building id filter

Code:

1roomTypeIds.splice(i, 1) // Remove one id from the room type filter, where `i` is the index of the element to remove

The full list of IDs corresponding to buildings and rooms can be referenced here:

Building nameBuilding ID
Cary Quadrangle1
Earhart2
First Street Towers3
Frieda Parker34
Harrison4
Hawkins5
Hillenbrand6
Honors17
McCutcheon8
Meredith9
Meredith South33
Owen10
Shreve12
Tarkington13
Wiley14
Windsor15
Winifred Parker16
Room typeRoom type ID
Apartment166
Double165
Quad198
Single169
Suite167
Triple168