-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
29d1ac9
commit c31cec3
Showing
7 changed files
with
395 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,251 @@ | ||
/* | ||
* Copyright (c) 2021 wilmaplus-foodmenu, developed by @developerfromjokela, for Wilma Plus mobile app | ||
*/ | ||
|
||
import {Request, Response} from "express"; | ||
import {errorResponse, responseStatus} from "../utils/response_utilities"; | ||
import {Builder, By, ThenableWebDriver} from "selenium-webdriver"; | ||
import {Restaurant} from "../models/Restaurant"; | ||
import {AsyncIterator} from "../utils/iterator"; | ||
import {elementLocated} from "selenium-webdriver/lib/until"; | ||
import {Http} from "../net/http"; | ||
import {parse} from "../parsers/aromiv2"; | ||
import {CacheContainer} from "node-ts-cache"; | ||
import {MemoryStorage} from "node-ts-cache-storage-memory"; | ||
import {HashUtils} from "../crypto/hash"; | ||
import {Day} from "../models/Day"; | ||
import {Diet} from "../models/Diet"; | ||
|
||
const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)/; | ||
let httpClient = new Http(); | ||
let userCache = new CacheContainer(new MemoryStorage()); | ||
|
||
|
||
function getRestaurantList(driver: ThenableWebDriver) { | ||
return new Promise<Restaurant[]>((resolve, reject) => { | ||
driver.findElement(By.id("MainContent_RestaurantDropDownList")).then(element => { | ||
let restaurants:Restaurant[] = []; | ||
element.findElements(By.css("option")).then(options => { | ||
new AsyncIterator((item, iterator) => { | ||
item.getAttribute("textContent").then(name => { | ||
if (name != null) { | ||
item.getAttribute("value").then(id => { | ||
if (id != null) { | ||
restaurants.push(new Restaurant(id, name)); | ||
iterator.nextItem(); | ||
} else | ||
iterator.nextItem(); | ||
}).catch(() => { | ||
iterator.nextItem(); | ||
}); | ||
} else | ||
iterator.nextItem(); | ||
}).catch(() => { | ||
iterator.nextItem(); | ||
}); | ||
}, options.splice(1, options.length-1), () => { | ||
resolve(restaurants); | ||
}).start(); | ||
}).catch(error => { | ||
reject(error); | ||
}); | ||
}).catch(error => { | ||
reject(error); | ||
}); | ||
}); | ||
|
||
} | ||
|
||
function selectRestaurant(driver: ThenableWebDriver, id: string) { | ||
return new Promise<void>((resolve, reject) => { | ||
getRestaurantList(driver).then(restaurants => { | ||
let position = -1; | ||
restaurants.forEach((restaurant, index) => { | ||
if (restaurant.id == id) { | ||
position = index | ||
} | ||
}); | ||
if (position === -1) { | ||
reject(new Error("Restaurant not found, check the ID")); | ||
return; | ||
} | ||
driver.findElement(By.id("MainContent_RestaurantDropDownList-button")).then(button => { | ||
button.click().then(() => { | ||
driver.findElement(By.id("MainContent_RestaurantDropDownList-menu")).then(dropDown => { | ||
dropDown.findElements(By.css("li")).then(listItems => { | ||
if (position > listItems.length-1) { | ||
reject(new Error("Restaurant not found, check the ID")); | ||
return; | ||
} | ||
let listItem = listItems[position]; | ||
listItem.click().then(() => { | ||
resolve(); | ||
}).catch(error => { | ||
reject(error); | ||
}); | ||
}).catch(error => { | ||
reject(error); | ||
}); | ||
}).catch(error => { | ||
reject(error); | ||
}); | ||
}).catch(error => { | ||
reject(error); | ||
}); | ||
}).catch(error => { | ||
reject(error); | ||
}); | ||
}).catch(error => { | ||
reject(error); | ||
}); | ||
}); | ||
|
||
} | ||
|
||
function getRestaurantPDFLink(driver: ThenableWebDriver) { | ||
return new Promise<string>((resolve, reject) => { | ||
driver.wait(elementLocated(By.id("MainContent_PdfUrl"))).then(() => { | ||
driver.findElement(By.id("MainContent_PdfUrl")).then(pdfUrl => { | ||
pdfUrl.getAttribute("href").then(url => { | ||
resolve(url); | ||
}).catch(error => { | ||
reject(error); | ||
}); | ||
}).catch(error => { | ||
reject(error); | ||
}); | ||
}).catch(error => { | ||
reject(error); | ||
}); | ||
}); | ||
} | ||
|
||
export function getMenuOptions(req: Request, res: Response) { | ||
if (!req.params.url) { | ||
responseStatus(res, 400, false, {cause: 'URL not specified!'}); | ||
return; | ||
} | ||
let url = req.params.url; | ||
if (!url.match(urlRegex)) { | ||
responseStatus(res, 400, false, {cause: 'Invalid of malformed URL!'}); | ||
return; | ||
} | ||
let hashKey = HashUtils.sha1Digest(url+"_aroma"); | ||
userCache.getItem(hashKey).then(cachedValue => { | ||
if (cachedValue) { | ||
responseStatus(res, 200, true, {restaurants: cachedValue}); | ||
} else { | ||
const driver = new Builder().forBrowser("chrome").build(); | ||
driver.get(url+"/Default.aspx").then(() => { | ||
getRestaurantList(driver).then(restaurants => { | ||
userCache.setItem(hashKey, restaurants, {ttl: 3600}).then(() => { | ||
responseStatus(res, 200, true, {restaurants}); | ||
driver.close(); | ||
}).catch(error => { | ||
responseStatus(res, 500, false, {cause: error.toString()}); | ||
driver.close(); | ||
}); | ||
}).catch(error => { | ||
responseStatus(res, 500, false, {cause: error.toString()}); | ||
driver.close(); | ||
}); | ||
}).catch(error => { | ||
responseStatus(res, 500, false, {cause: error.toString()}); | ||
driver.close(); | ||
}); | ||
} | ||
}).catch(error => { | ||
responseStatus(res, 500, false, {cause: error.toString()}); | ||
}); | ||
|
||
} | ||
|
||
export function getRestaurantPage(req: Request, res: Response) { | ||
if (!req.params.url || !req.params.id) { | ||
responseStatus(res, 400, false, {cause: 'Required parameters not specified!'}); | ||
return; | ||
} | ||
let url = req.params.url; | ||
let id = req.params.id; | ||
if (!url.match(urlRegex)) { | ||
responseStatus(res, 400, false, {cause: 'Invalid of malformed URL!'}); | ||
return; | ||
} | ||
const fetchDocument = (pdfUrl: string) => { | ||
const fetchDate = (date: string, callback: (restaurants: Day[], diets: Diet[]) => void, errorCallback: (error: Error | null) => void) => { | ||
httpClient.get(pdfUrl.replace("%dmd%", date), (error, response) => { | ||
if (error || response == undefined) { | ||
errorCallback(error); | ||
return; | ||
} | ||
parse(response.body, (restaurants, diets) => { | ||
if (restaurants == undefined || diets == undefined) { | ||
errorCallback(new Error("Unable to parse menu!")); | ||
return; | ||
} | ||
callback(restaurants, diets); | ||
}); | ||
}); | ||
}; | ||
const contains = (item: string, items: Diet[]) => { | ||
let found = false; | ||
items.forEach(item2 => { | ||
if (item.toLowerCase() == item2.name.toLowerCase()) | ||
found = true; | ||
}); | ||
return found; | ||
} | ||
fetchDate("1", (restaurants, diets) => { | ||
fetchDate("2", ((restaurants1, diets1) => { | ||
fetchDate("3", ((restaurants2, diets2) => { | ||
restaurants1.forEach(item => restaurants.push(item)); | ||
restaurants2.forEach(item => restaurants.push(item)); | ||
diets1.forEach(dItem => {if (!contains(dItem.name, diets)) {diets.push(dItem)}}); | ||
diets2.forEach(dItem => {if (!contains(dItem.name, diets)) {diets.push(dItem)}}); | ||
responseStatus(res, 200, true, {menu: restaurants, diets: diets}); | ||
}), error => { | ||
errorResponse(res, 500, error); | ||
return; | ||
}); | ||
}), error => { | ||
errorResponse(res, 500, error); | ||
return; | ||
}); | ||
}, error => { | ||
errorResponse(res, 500, error); | ||
return; | ||
}) | ||
} | ||
let hashKey = HashUtils.sha1Digest(url+"_"+id); | ||
userCache.getItem(hashKey).then(value => { | ||
// Check if cached value exists | ||
if (value) | ||
fetchDocument(value as string); | ||
else { | ||
const driver = new Builder().forBrowser("chrome").build(); | ||
driver.get(url+"/Default.aspx").then(() => { | ||
selectRestaurant(driver, id).then(() => { | ||
getRestaurantPDFLink(driver).then(pdfUrl => { | ||
driver.close(); | ||
pdfUrl = pdfUrl.replace("DateMode=0", "DateMode=%dmd%"); | ||
userCache.setItem(hashKey, pdfUrl, {ttl: 3600}).then(() => { | ||
fetchDocument(pdfUrl); | ||
}).catch(error => { | ||
responseStatus(res, 500, false, {cause: error.toString()}); | ||
}); | ||
}).catch(error => { | ||
responseStatus(res, 500, false, {cause: error.toString()}); | ||
driver.close(); | ||
}); | ||
}).catch(error => { | ||
responseStatus(res, 500, false, {cause: error.toString()}); | ||
driver.close(); | ||
}); | ||
}).catch(error => { | ||
responseStatus(res, 500, false, {cause: error.toString()}); | ||
driver.close(); | ||
}); | ||
} | ||
}) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
/* | ||
* Copyright (c) 2021 wilmaplus-foodmenu, developed by @developerfromjokela, for Wilma Plus mobile app | ||
*/ | ||
|
||
export class Restaurant { | ||
id: string | ||
name: string | ||
|
||
constructor(id: string, name: string) { | ||
this.id = id; | ||
this.name = name; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
/* | ||
* Copyright (c) 2021 wilmaplus-foodmenu, developed by @developerfromjokela, for Wilma Plus mobile app | ||
*/ | ||
|
||
import moment from 'moment'; | ||
import {Day} from "../models/Day"; | ||
import {Menu} from "../models/Menu"; | ||
import {Moment} from "moment/moment"; | ||
import {Meal} from "../models/Meal"; | ||
import {HashUtils} from "../crypto/hash"; | ||
import {Diet} from "../models/Diet"; | ||
|
||
const pdfParser = require("pdfreader"); | ||
|
||
const dateRegex = /[0-9]+\.[0-9]+\.[0-9]{4}/; | ||
|
||
const type = "aromiv2"; | ||
|
||
|
||
export function parse(content: any, callback: (content: Day[]|undefined, diets: Diet[]|undefined) => void) { | ||
let rows: any = {}; // indexed by y-position | ||
let days: Day[] = []; | ||
let diets: Diet[] = []; | ||
new pdfParser.PdfReader().parseBuffer(content, (pdfError: Error, pdf: any) => { | ||
if (pdfError) { | ||
console.error(pdfError); | ||
callback(undefined, undefined); | ||
return; | ||
} | ||
if (!pdf || pdf.page) { | ||
let items = Object.keys(rows).sort((y1, y2) => parseFloat(y1) - parseFloat(y2)); | ||
let lastDate: Moment|null = null; | ||
let tempMenuList:Menu[] = []; | ||
items.forEach(key => { | ||
let item = rows[key]; | ||
if (item.length > 0) { | ||
let firstEntry = item[0]; | ||
if (firstEntry.text.match(dateRegex)) { | ||
// Date found | ||
let regexResult = dateRegex.exec(firstEntry.text); | ||
if (regexResult != null) { | ||
if (tempMenuList.length > 0 && lastDate != null) { | ||
days.push(new Day(lastDate, tempMenuList)); | ||
tempMenuList = []; | ||
} | ||
lastDate = moment(regexResult[0], "DD.MM.YYYY").startOf('day'); | ||
} | ||
} else if (lastDate != null) { | ||
let mealType = "Lounas"; | ||
let items: Meal[] = []; | ||
for (let meal of item) { | ||
if (meal.x < 3 && meal.x > 1) { | ||
mealType = meal.text; | ||
} else if (meal.x > 4) { | ||
items.push(new Meal(HashUtils.sha1Digest(type+mealType+"_"+meal.text), meal.text)); | ||
} | ||
} | ||
tempMenuList.push(new Menu(mealType, items)); | ||
} | ||
} | ||
}); | ||
if (tempMenuList.length > 0 && lastDate != null) { | ||
days.push(new Day(lastDate, tempMenuList)); | ||
tempMenuList = []; | ||
} | ||
try { | ||
if (items.length > 0) { | ||
let dietsText = rows[items[items.length-1]][0].text; | ||
let dietSplit = dietsText.split(", ") | ||
for (let dietItem of dietSplit) { | ||
let parts = dietItem.split(" - "); | ||
if (parts.length > 1) { | ||
diets.push(new Diet(parts[0], parts[1])); | ||
} | ||
} | ||
} | ||
} catch (e) { | ||
console.error(e); | ||
} | ||
if (!pdf) { | ||
days.sort((i1: Day, i2:Day) => { | ||
return i1.date.unix()-i2.date.unix(); | ||
}); | ||
// Formatting date after sorting | ||
let correctedDateDays = days; | ||
correctedDateDays.forEach((item, index) => { | ||
item.date = (item.date.format() as any); | ||
correctedDateDays[index] = item; | ||
}); | ||
callback(correctedDateDays, diets); | ||
} | ||
} else if (pdf.text) { | ||
// accumulate text items into rows object, per line | ||
(rows[pdf.y] = rows[pdf.y] || []).push({text: pdf.text, x: pdf.x}); | ||
} | ||
}); | ||
} |
Oops, something went wrong.