console.log("[+] Wow, JavaScript!"); import * as wasm from "rsp6-decoder"; import { BrowserAztecCodeReader } from '@zxing/library'; let stations = require("./stations.json"); let fares = require("./fares.json"); let discounts = require("./discounts.json"); window.stations = stations; window.fares = fares; window.discounts = discounts; let loaded = false; function onload() { if (loaded) { return; } loaded = true; console.log("[+] initialising wasm"); window.wasm = wasm; window.wasm.init(); let video_div = document.getElementById("video-container"); let scan_button = document.getElementById("scan-button"); let error_banner = document.getElementById("error-banner"); let error_header = document.getElementById("error-header"); let error_text = document.getElementById("error-text"); let decode_banner = document.getElementById("decode-banner"); let decode_reason = document.getElementById("decode-reason"); let raw_barcode = document.getElementById("raw-barcode"); let raw_barcode_wrapper = document.getElementById("raw-barcode-wrapper"); let raw_json = document.getElementById("raw-json"); let ticket_data = document.getElementById("ticket-data"); let ticket_content = document.getElementById("ticket-content"); let file_upload = document.getElementById("file-upload-1"); const codeReader = new BrowserAztecCodeReader(); console.log("[+] Looks like everything initialised fine!"); document.getElementById('code-version').innerHTML = __COMMIT_HASH__; let selectedDeviceId; function error(header, text) { video_div.style.display = "none"; error_header.innerHTML = header; error_text.innerHTML = text; error_banner.style.display = "block"; error_banner.focus(); error_banner.scrollIntoView(); } function decodeError(text) { decode_banner.style.display = "block"; decode_reason.innerHTML = text; decode_banner.scrollIntoView(); } function row(k, v) { let tr = document.createElement("tr"); tr.classList.add("govuk-table__row"); let th = document.createElement("th"); th.classList.add("govuk-table__header"); th.scope = "row"; let td = document.createElement("td"); td.classList.add("govuk-table__cell"); th.innerHTML = k; td.innerHTML = v; tr.appendChild(th); tr.appendChild(td); ticket_data.appendChild(tr); } function nlcify(nlc) { let data = stations[nlc]; if (!data || !data.desc) { return nlc; } else { if (data.crs) { return data.crs; } else { return nlc; } } } function handleTicket(ticket) { window.ticket = ticket; video_div.style.display = "none"; codeReader.reset(); console.log("Got ticket: " + ticket); raw_barcode.innerHTML = ticket; raw_barcode_wrapper.style.display = "block"; try { let result = window.wasm.decode_ticket(window.ticket); if (result["Err"]) { decodeError(result["Err"]); } else { let data = result["Ok"]; raw_json.innerHTML = JSON.stringify(data, null, 2); ticket_data.innerHTML = ""; document.getElementById("origin").innerHTML = nlcify(data.origin_nlc); document.getElementById("dest").innerHTML = nlcify(data.destination_nlc); let fare = fares[data.fare] || ("Unknown fare (" + data.fare + ")"); let origin = stations[data.origin_nlc] ? stations[data.origin_nlc].desc : "NLC " + data.origin_nlc; let destination = stations[data.destination_nlc] ? stations[data.destination_nlc].desc : "NLC " + data.destination_nlc; let type = ""; if (data.coupon_type == "Single") { type = "Single"; } else if (data.coupon_type == "ReturnOutbound") { type = "Return (outbound)"; } else if (data.coupon_type == "ReturnInbound") { type = "Return (inbound)"; } else { type = "Season"; } if (data.bidirectional) { type += " (bidirectional)"; } let discount = null; let discount_name = null; if (data.discount_code) { if (discounts[data.discount_code]) { discount = discounts[data.discount_code].desc; if (discounts[data.discount_code].code) { discount_name = discounts[data.discount_code].code; } } else { discount = "Unknown discount (" + data.discount_code + ")"; } } let adult = data.child_ticket ? "Child" : "Adult"; let first = data.standard_class ? "Standard Class" : "First Class"; document.getElementById("ticket-fromto").innerHTML = origin + " to " + destination; if (data.manually_inspect) { row("", "Manual inspection required"); } let brfares_link = "https://www.brfares.com/!faredetail?orig=" + data.origin_nlc + "&dest=" + data.destination_nlc + "&tkt=" + data.fare; if (data.route_code && data.route_code != 0) { brfares_link += "&rte=" + data.route_code; } if (discount_name) { brfares_link += "&rlc=" + discount_name; } row("Fare", fare + "
View on brfares.com"); row("Class", adult + " " + first); if (discount) { row("Discount", discount); } row("Ticket type", type); row("Start date", data.start_date); if (data.depart_time_flag || data.depart_time != "00:00:00.0") { let extra = ""; if (data.depart_time_flag == "Suggested") { extra = " (suggested)"; } else if (data.depart_time_flag == "Specific") { extra = " (mandatory)"; } row("Depart at", data.depart_time.substring(0, 5) + extra); } row("Ticket reference", data.issuer_id + data.ticket_reference); if (data.passenger_name) { row("Passenger name", data.passenger_name); } if (data.purchase_details) { let dov = data.purchase_details.days_of_validity; let maybe_s = ""; if (dov > 1) { maybe_s = "s"; } row("Valid for", dov + " day" + maybe_s); } if (data.limited_duration) { row("Limited duration", data.limited_duration); } row("Fare code", data.fare); if (data.purchase_details) { let price = data.purchase_details.price_pence; row("Price", "£" + Math.floor(price / 100) + "." + (price % 100).toString().padStart(2, "0")); row("Purchased", data.purchase_details.purchase_time.substring(0, 16)); if (data.purchase_details.purchase_reference) { row("Purchase reference", data.purchase_details.purchase_reference); } } if (data.reservations.length > 0) { let out = ""; data.reservations.forEach((resv) => { if (out != "") { out += "
"; } out += "Train " + resv.retail_service_id + "
"; if (resv.coach == "*" || (resv.seat_number == 0 && resv.coach == " ")) { out += "No specific seat"; } else { if (!resv.coach || resv.coach == " ") { resv.coach = "?"; } out += "Coach " + resv.coach + " Seat " + resv.seat_number + (resv.seat_letter ? resv.seat_letter : ""); } }); row("Reservations", out); } let retailer = stations[data.retailer_id] ? stations[data.retailer_id].desc : "???"; row("Sold by", retailer + " (" + data.retailer_id + ")"); if (data.route_code != 0) { row("Route code", data.route_code); } if (data.restriction_code) { row("Restriction", data.restriction_code); } if (data.discount_code != 0) { row("Discount code", (discount_name ? discount_name + " / " : "") + data.discount_code); } if (data.passenger_id) { row("Passenger ID", data.passenger_id); } row("Lennon code", data.lennon_ticket_type); row("Sequence number", data.sub_utn); ticket_content.style.display = "block"; ticket_content.scrollIntoView(); } } catch (e) { decodeError("exception: " + e); } } window.handleTicket = handleTicket; codeReader.getVideoInputDevices() .then((videoInputDevices) => { console.log("[+] Detected " + videoInputDevices.length + " video sources"); if (videoInputDevices.length >= 1) { selectedDeviceId = videoInputDevices[0].deviceId; scan_button.removeAttribute("disabled"); } }) .catch((err) => { error("Scanner failed", "Couldn't figure out what cameras you have on your device.
" + err + "
"); }); let epoch = 0; document.getElementById('scan-button').addEventListener('click', () => { let current_epoch = epoch + 1; epoch = current_epoch; decode_banner.style.display = "none"; ticket_content.style.display = "none"; codeReader.decodeFromInputVideoDevice(undefined, 'video').then((result) => { error_banner.style.display = "none"; handleTicket(result); }).catch((err) => { if (epoch == current_epoch) { error("Scanner failed", "Looks like the barcode scanner failed in some way. If this keeps happening, try reloading the page or using one of the alternative options.
" + err + "
"); console.error(err); } }); video_div.style.display = "block"; console.log(`[+] Barcode scanner started on device ${selectedDeviceId}`); }); document.getElementById('video').addEventListener('play', () => { console.log('[+] Video started playing'); video_div.scrollIntoView(); }); document.getElementById('raw-text-form').addEventListener('submit', (e) => { e.preventDefault(); console.log("[+] Raw text input pressed"); decode_banner.style.display = "none"; ticket_content.style.display = "none"; let value = document.getElementById('raw-text-in').value; document.getElementById('text-details').removeAttribute('open'); handleTicket(value); }); file_upload.addEventListener('change', () => { if (file_upload.files.length > 0) { console.log('[+] File selected'); const file = file_upload.files[0]; const uri = window.URL.createObjectURL(file); console.log('[+] Scanning URL ' + uri); const img = document.createElement("img"); img.src = uri; img.videoWidth = 0; let done = false; document.getElementById('image-sanctuary').innerHTML = ''; document.getElementById('image-sanctuary').appendChild(img); decode_banner.style.display = "none"; ticket_content.style.display = "none"; codeReader.reset(); setTimeout(() => { if (!done) { console.log("[+] Timed out!"); codeReader.reset(); } }, 750); codeReader.decodeFromImage(img).then((result) => { console.log("[+] Screenshot decode done!"); done = true; document.getElementById('screenshot-details').removeAttribute('open'); error_banner.style.display = "none"; window.URL.revokeObjectURL(uri); handleTicket(result); }).catch((err) => { done = true; console.error(err); error("Barcode reader failed", "Couldn't detect a barcode in that screenshot. Try another image.
" + err + "
"); window.URL.revokeObjectURL(uri); }); } }); } if (document.readyState == 'complete') { console.log("[+] Loaded already!"); onload(); } else { window.addEventListener('load', onload); }