diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65c34dc --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Files and directories created by pub. +.dart_tool/ +.packages + +# Conventional directory for build outputs. +build/ + +# Omit committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9a99faa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +## 0.1.0-alpha + +- První verze, funkčnost omezená +- Metody pro zobrazení jídelníčku diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md new file mode 100644 index 0000000..880142b --- /dev/null +++ b/COMPATIBILITY.md @@ -0,0 +1,12 @@ +# Kompatibilita knihovny s instancemi služby iCanteen +V následující tabulce naleznete instance iCanteen, které byly testovány pro jejich funkčnost s touto knihovnou. + +- ❌ - nefunkční nebo netestováno +- ✅ - plně funkční nebo pouze s malými chybami +- ❓ - částečně funkční + +| Provozovatel | Verze iCanteen | Funkční | Verze knihovny | +|:--------------:|------------------|---------|----------------| +| SŠTE Brno | iCanteen 2.18.19 | ❓ | 0.1.0-alpha | +| SPŠ Třebíč | iCanteen 2.10.25 | ❓ | 0.1.0-alpha | +| Výsledky testů | nahlašujte | [zde](https://github.com/hernikplays/canteenlib/issues/new) | | \ No newline at end of file diff --git a/README.md b/README.md index 12714de..b727abf 100644 --- a/README.md +++ b/README.md @@ -1 +1,10 @@ -canteenlib +## O knihovně +Experimentální **neoficiální** webscrape knihovna pro komunikaci se systémem [iCanteen](https://www.z-ware.cz/internetove-objednavky) + +## Funkční funkce(*) +- získání jídelníčku na aktuální den (s cenami) + +*\* Knihovna nemusí fungovat na všech instancích systému iCanteen, proto žádám každého, kdo může a je uživatelem iCanteen, aby otestoval funkčnost této knihovny a případné problémy [nahlásil](https://github.com/hernikplays/canteenlib/issues)* + +### Otestované instance iCanteen +[zde]() \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..dee8927 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/example/canteenlib_example.dart b/example/canteenlib_example.dart new file mode 100644 index 0000000..516694e --- /dev/null +++ b/example/canteenlib_example.dart @@ -0,0 +1,10 @@ +import 'package:canteenlib/canteenlib.dart'; + +void main() { + var canteen = Canteen("http://icanteen.vasedomena.neco"); + canteen.login("user", "password").then((_) { + canteen.jidelnicekDen().then((jidelnicek) { + print(jidelnicek.jidla[0].hlavni); + }); + }); +} diff --git a/lib/canteenlib.dart b/lib/canteenlib.dart new file mode 100644 index 0000000..3e5f56b --- /dev/null +++ b/lib/canteenlib.dart @@ -0,0 +1,5 @@ +/// Hlavní knihovna +library canteenlib; + +export 'src/canteen.dart'; +export 'src/jidlo.dart'; diff --git a/lib/src/canteen.dart b/lib/src/canteen.dart new file mode 100644 index 0000000..df67bbd --- /dev/null +++ b/lib/src/canteen.dart @@ -0,0 +1,220 @@ +import 'dart:io'; + +import 'package:http/http.dart' as http; + +import 'jidlo.dart'; + +/* + (C) 2022 Matyáš Caras and contributors + This library is released under the GNU LGPLv3 license. + If you have not received the license with this library, you can view it here http://www.gnu.org/licenses/lgpl-3.0.txt +*/ +class Canteen { + final String url; + Map cookies = {"JSESSIONID": "", "XSRF-TOKEN": ""}; + Canteen(this.url); + + Future getFirstSession() async { + var res = await http.get(Uri.parse(url)); + _parseCookies(res.headers['set-cookie']!); + } + + /// Převede cookie řetězec z požadavku do mapy + void _parseCookies(String cookieString) { + Map cookies = this.cookies; + var regCookie = RegExp(r'([A-Z\-]+=.+?(?=;))|(remember-me=.+?)(?=;)') + .allMatches(cookieString) + .toList(); + for (var cook in regCookie) { + var c = cook.group(0).toString().split("="); + cookies[c[0]] = c[1]; + } + } + + /// Přihlášení do iCanteen + /// + /// `user` - uživatelské jméno + /// `password` - heslo + /// + /// TODO: Házet chyby + Future login(String user, String password) async { + if (cookies["JSESSIONID"] == "" || cookies["XSRF-TOKEN"] == "") { + await getFirstSession(); + } + var res = + await http.post(Uri.parse(url + "/j_spring_security_check"), headers: { + "Cookie": "JSESSIONID=" + + cookies["JSESSIONID"]! + + "; " + + "XSRF-TOKEN=" + + cookies["XSRF-TOKEN"]! + + ";", + "Content-Type": "application/x-www-form-urlencoded", + }, body: { + "j_username": user, + "j_password": password, + "terminal": "false", + "_csrf": cookies["XSRF-TOKEN"], + "_spring_security_remember_me": "on", + "targetUrl": + "/faces/secured/main.jsp?terminal=false&status=true&printer=&keyboard=" + }); + _parseCookies(res.headers['set-cookie']!); + if (res.statusCode != 302) { + print(res.body); + print("ERROR"); + } + } + + /// Builder pro GET request + Future _getRequest(String path) async { + var r = await http.get(Uri.parse(url + path), headers: { + "Cookie": "JSESSIONID=" + + cookies["JSESSIONID"]! + + "; " + + "XSRF-TOKEN=" + + cookies["XSRF-TOKEN"]! + + (cookies.containsKey("remember-me") + ? "; " + cookies["remember-me"]! + ";" + : ";"), + }); + if (r.headers.containsKey("set-cookie")) { + _parseCookies(r.headers["set-cookie"]!); + } + return r.body; + } + + /// Získá jídelníček bez cen + /// **nevrací** ceny, ale umožňuje získat jídelníček bez přihlášení + Future> ziskejJidelnicek() async { + var res = await _getRequest("/"); + var reg = RegExp( + r'((?=
).+?(?=
))|((?=
).*<\/span>)', + dotAll: true) + .allMatches(res!) + .toList(); + List jidelnicek = []; + for (var t in reg) { + // projedeme každý den individuálně + var j = t + .group(0) + .toString() /*.replaceAll(RegExp(r'( )+|([^>a-z]\n)'), + '')*/ + ; // převedeme text na něco přehlednějšího + var den = DateTime.parse(RegExp(r'(?<=day-).+?(?=")', dotAll: true) + .firstMatch(j)! + .group(0) + .toString()); + var jidlaDenne = RegExp( + r'(?=
).+?<\/div>.+?(?=<\/div>)', + dotAll: true) + .allMatches(j) + .toList(); // získáme jednotlivá jídla pro den / VERZE 2.18 + if (jidlaDenne.isEmpty) { + jidlaDenne = RegExp( + r'(?=
).+?(?=<\/div>)', + dotAll: true) + .allMatches(j) + .toList(); // získáme jednotlivá jídla pro den / VERZE 2.10 + } + + List jidla = []; + for (var jidloNaDen in jidlaDenne) { + // projedeme vsechna jidla + var s = jidloNaDen.group(0)!.replaceAll( + RegExp( + r'[a-zA-ZěščřžýáíéÉÍÁÝŽŘČŠĚŤŇťň.,:] [a-zA-ZěščřžýáíéÉÍÁÝŽŘČŠĚŤŇťň.,:]'), + ''); // odstraní dvojté mezery mezi písmeny + var cislo = RegExp(r'(?<=).+?(?=<)') + .firstMatch(s); // název výdejny / verze 2.18 + cislo ??= RegExp( + r'(?<=).+?(?=<)') + .firstMatch(s); // název výdejny / verze 2.10 + File("dva.txt").writeAsStringSync(s); + var hlavni = RegExp( + r' {20}(([a-zA-ZěščřžýáíéÉÍÁÝŽŘČŠĚŤŇťň.,:\/]+ )+[a-zA-ZěščřžýáíéÉÍÁÝŽŘČŠĚŤŇťň.,:\/]+)', + dotAll: true) + .firstMatch(s)! + .group(1) + .toString(); // Jídlo + jidla.add(Jidlo( + nazev: hlavni, + objednano: false, + cislo: cislo!.group(0).toString(), + lzeObjednat: false)); + } + jidelnicek.add(Jidelnicek(den, jidla)); + } + return jidelnicek; + } + + /// Získá jídlo pro daný den + /// Vyžaduje přihlášení pomocí [login] + Future jidelnicekDen() async { + var res = await _getRequest( + "/faces/secured/main.jsp?terminal=false&android=false&keyboard=false&printer=false"); + var den = DateTime.parse(RegExp(r'(?<=day-).+?(?=")', dotAll: true) + .firstMatch(res!)! + .group(0) + .toString()); + var jidla = []; + var jidelnicek = + RegExp(r'(?<=
).+?(?=
)', dotAll: true) + .allMatches(res) + .toList(); + for (var obed in jidelnicek) { + // formátování do třídy + var o = obed + .group(0) + .toString() + .replaceAll(RegExp(r'( )+|([^>a-z]\n)'), ''); + var objednano = o.contains("Máte objednáno"); + var lzeObjednat = + !(o.contains("nelze zrušit") || o.contains("nelze objednat")); + var cenaMatch = + RegExp(r'(?<=Cena objednaného jídla">).+?(?=&)').firstMatch(o); + cenaMatch ??= + RegExp(r'(?<=Cena při objednání jídla">).+?(?=&)').firstMatch(o); + var cena = + double.parse(cenaMatch!.group(0).toString().replaceAll(",", ".")); + var jidlaProDen = RegExp(r'(?<=Polévka: ).+') + .firstMatch(o)! + .group(0) + .toString() + .split(" / "); + var vydejna = RegExp( + r'(?<=).+?(?=<)') + .firstMatch(o)! + .group(0) + .toString(); + String? orderUrl; + if (lzeObjednat) { + // pokud lze objednat, nastavíme adresu pro objednání + orderUrl = RegExp(r"(?<=ajaxOrder\(this, ').+?(?=')") + .firstMatch(o)! + .group(0)! + .replaceAll("amp;", ""); + } + jidla.add(Jidlo( + nazev: jidlaProDen[1] + .replaceAll(r' (?=[^a-zA-ZěščřžýáíéĚŠČŘŽÝÁÍÉŤŇťň])', ''), + objednano: objednano, + cislo: vydejna, + lzeObjednat: lzeObjednat, + cena: cena, + orderUrl: orderUrl)); + // KONEC formátování do třídy + } + + return Jidelnicek(den, jidla); + } + + Future objednat(Jidlo j) async { + if (!j.lzeObjednat || j.orderUrl == null || j.orderUrl!.isEmpty) { + return false; + } + var res = await _getRequest(j.orderUrl!); + print(res); + return true; + } +} diff --git a/lib/src/jidlo.dart b/lib/src/jidlo.dart new file mode 100644 index 0000000..48db3c9 --- /dev/null +++ b/lib/src/jidlo.dart @@ -0,0 +1,35 @@ +/// Reprezentuje jedno jídlo z jídelníčku +class Jidlo { + /// Název jídla + String nazev; + + /// Objednal si uživatel toto jídlo? + bool objednano; + + /// Název výdejny + String cislo; + + /// Cena + double? cena; + + ///Lze objednat? + bool lzeObjednat; + final String? orderUrl; + Jidlo( + {required this.nazev, + required this.objednano, + required this.cislo, + this.cena, + required this.lzeObjednat, + this.orderUrl}); +} + +/// Reprezentuje jídelníček pro jeden dan +class Jidelnicek { + /// Den, pro který je jídelníček zveřejněn + DateTime den; + + /// Seznam jídel + List jidla; + Jidelnicek(this.den, this.jidla); +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..f48ddf2 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,17 @@ +name: canteenlib +description: Knihovna pro komunikaci se stravovacím systémem iCanteen +version: 0.1.0-alpha +# homepage: https://www.example.com + +environment: + sdk: '>=2.16.1 <3.0.0' + + +# dependencies: +# path: ^1.8.0 + +dev_dependencies: + lints: ^1.0.0 + test: ^1.16.0 +dependencies: + http: ^0.13.4 diff --git a/test/canteenlib_test.dart b/test/canteenlib_test.dart new file mode 100644 index 0000000..7d719ac --- /dev/null +++ b/test/canteenlib_test.dart @@ -0,0 +1,16 @@ +import 'package:canteenlib/canteenlib.dart'; +import 'package:test/test.dart'; + +void main() { + group('A group of tests', () { + final awesome = Awesome(); + + setUp(() { + // Additional setup goes here. + }); + + test('First Test', () { + expect(awesome.isAwesome, isTrue); + }); + }); +}