1 /* This file is part of Beifahrer.
3 * Copyright (C) 2010 Philipp Zabel
5 * Beifahrer is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * Beifahrer is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with Beifahrer. If not, see <http://www.gnu.org/licenses/>.
23 public double latitude;
24 public double longitude;
30 public City (int _number, string _name) {
37 internal double bb_area () {
38 return (north - south) * (east - west);
42 public enum LiftFlags {
49 public class Lift : Object {
50 public string city_from;
51 public string city_to;
55 public LiftFlags flags;
59 public List<string> city_via;
65 public string email_image_uri;
66 public string description;
67 public string modified;
74 public class MyInformation {
82 public string first_name;
83 public string last_name;
85 public Date registered_since;
90 public string zip_code;
92 public string country;
100 public string email2;
103 public bool adac_member;
106 public class AdacMitfahrclub {
107 const string HTTP_BASE_URI = "http://mitfahrclub.adac.de";
108 const string HTTPS_BASE_URI = "https://mitfahrclub.adac.de";
110 Curl.EasyHandle curl;
111 List<City> city_list = null;
113 public AdacMitfahrclub () {
114 curl = new Curl.EasyHandle ();
115 // FIXME: Fremantle SDK doesn't come with certs
116 curl.setopt (Curl.Option.SSL_VERIFYPEER, 0);
117 // curl.setopt (Curl.Option.VERBOSE, 1);
120 private string _url = null;
121 private SourceFunc callback = null;
122 void* download_thread () {
123 curl.setopt (Curl.Option.WRITEFUNCTION, write_callback);
124 curl.setopt (Curl.Option.WRITEDATA, this);
125 curl.setopt (Curl.Option.URL, _url);
126 if (aeolus_cookie != null)
127 curl.setopt (Curl.Option.COOKIE, "MIKINIMEDIA=%s; Quirinus[adacAeolus]=%s;".printf (mikini_cookie, aeolus_cookie));
128 else if (mikini_cookie != null)
129 curl.setopt (Curl.Option.COOKIE, "MIKINIMEDIA=%s".printf (mikini_cookie));
130 var res = curl.perform ();
132 if (callback != null)
139 StringBuilder result;
141 [CCode (instance_pos = -1)]
142 size_t write_callback (void *buffer, size_t size, size_t nmemb) {
143 // if (cancellable != null && cancellable.is_cancelled ())
146 result.append_len ((string) buffer, (ssize_t) (size * nmemb));
151 private async Html.Doc* get_html_document (string url) {
153 callback = get_html_document.callback;
154 result = new StringBuilder ();
156 Thread.create(download_thread, false);
157 } catch (ThreadError e) {
158 critical ("Failed to create download thread\n");
164 return Html.Doc.read_memory ((char[]) result.str, (int) result.len,
165 url, null, Html.ParserOption.NOERROR | Html.ParserOption.NOWARNING);
168 private string username;
169 private string password;
170 private string mikini_cookie;
171 private string aeolus_cookie;
173 public void set_cookie (string value) {
174 aeolus_cookie = value;
177 public void set_credentials (string _username, string _password) {
178 username = _username;
179 password = _password;
182 public void login (string? _username, string? _password) {
183 set_credentials (_username, _password);
186 if (login_callback != null)
191 public bool logged_in = false;
192 private SourceFunc login_callback = null;
193 public async bool login_async () {
196 if (login_callback != null || username == null || password == null)
198 login_callback = login_async.callback;
199 result = new StringBuilder ();
201 Thread.create(login_thread, false);
202 } catch (ThreadError e) {
203 critical ("Failed to create login thread\n");
208 login_callback = null;
213 void *login_thread () {
214 result = new StringBuilder ();
215 curl.setopt (Curl.Option.URL, HTTP_BASE_URI);
216 curl.setopt (Curl.Option.WRITEFUNCTION, write_callback);
217 curl.setopt (Curl.Option.WRITEDATA, this);
218 curl.setopt (Curl.Option.COOKIEFILE, "");
219 var res = curl.perform ();
222 curl.getinfo (Curl.Info.COOKIELIST, out cookies);
223 unowned Curl.SList cookie = cookies;
224 while (cookie != null) {
225 if (cookie.data != null) {
226 var c = cookie.data.split ("\t");
228 print ("%s=%s\n", c[5], c[6]);
229 if (c[5] == "MIKINIMEDIA")
230 mikini_cookie = c[6];
232 cookie = cookie.next;
235 result = new StringBuilder ();
236 string postdata = "data[User][continue]=/&data[User][js_allowed]=0&data[User][cookie_allowed]=1&data[User][username]=%s&data[User][password]=%s&data[User][remember_me]=1".printf (username, password);
237 curl.setopt (Curl.Option.POSTFIELDS, postdata);
238 curl.setopt (Curl.Option.URL, HTTPS_BASE_URI + "/users/login/");
239 curl.setopt (Curl.Option.SSL_VERIFYPEER, 0);
240 res = curl.perform ();
241 // print ("%s\n", result.str);
244 curl.getinfo (Curl.Info.COOKIELIST, out cookies);
246 while (cookie != null) {
247 if (cookie.data != null) {
248 var c = cookie.data.split ("\t");
250 print ("%s=%s\n", c[5], c[6]);
251 // "Quirinus[adacAeolus]"
252 if (c[5] == "Quirinus[adacAeolus]") {
253 aeolus_cookie = c[6];
257 cookie = cookie.next;
260 if (result.str.contains ("<div id=\"flashMessage\" class=\"message\">Die eingegebenen Zugangsdaten konnten nicht gefunden werden. Bitte versuchen Sie es erneut.</div>")) {
261 print ("LOGIN FAILED\n");
262 aeolus_cookie = null;
266 if (login_callback != null)
267 Idle.add (login_callback);
271 private void save_city_list () {
272 FileStream list_file = FileStream.open ("/home/user/.cache/beifahrer/city_list", "w");
273 if (list_file == null)
276 foreach (unowned City city in city_list) {
277 if (city.north != 0.0 || city.south != 0.0 || city.east != 0.0 || city.west != 0.0)
278 list_file.printf ("%d\t%s\t%f\t%f\t%f\t%f\t%f\t%f\n", city.number, city.name, city.latitude, city.longitude, city.north, city.south, city.east, city.west);
279 else if (city.latitude != 0.0 || city.longitude != 0.0)
280 list_file.printf ("%d\t%s\t%f\t%f\n", city.number, city.name, city.latitude, city.longitude);
282 list_file.printf ("%d\t%s\n", city.number, city.name);
286 private bool load_city_list () {
287 FileStream list_file = FileStream.open ("/home/user/.cache/beifahrer/city_list", "r");
288 if (list_file == null)
289 list_file = FileStream.open ("/usr/share/beifahrer/city_list", "r");
290 if (list_file == null)
293 city_list = new List<City> ();
294 string line = list_file.read_line ();
295 while (line != null) {
296 var split_line = line.split ("\t");
297 if (split_line.length < 2)
299 int number = split_line[0].to_int ();
300 weak string name = split_line[1];
302 var city = new City (number, name);
303 if (split_line.length >= 4) {
304 city.latitude = split_line[2].to_double ();
305 city.longitude = split_line[3].to_double ();
307 if (split_line.length >= 8) {
308 city.north = split_line[4].to_double ();
309 city.south = split_line[5].to_double ();
310 city.east = split_line[6].to_double ();
311 city.west = split_line[7].to_double ();
313 city_list.append ((owned) city);
315 line = list_file.read_line ();
321 public unowned List<City>? get_city_list () {
322 if (city_list != null)
325 if (load_city_list ())
331 public async unowned List<City>? download_city_list () {
332 var doc = yield get_html_document (HTTP_BASE_URI);
334 stderr.printf ("Error: parsing failed\n");
338 var form = search_tag_by_id (doc->children, "form", "search_national_form");
340 stderr.printf ("Error: does not contain search_national_form\n");
344 var select = search_tag_by_name (form->children, "select", "city_from");
345 if (select == null) {
346 stderr.printf ("Error: does not contain city_from\n");
350 city_list = new List<City> ();
351 for (var n = select->children; n != null; n = n->next) {
352 if (n->name == "option" && n->children != null && n->children->name == "text") {
353 int number = n->get_prop ("value").to_int ();
354 // Skip 0 "Alle St.dte"
357 var city = new City(number,
358 n->children->content);
359 city_list.append ((owned) city);
363 // TODO: get coordinates
370 private int get_city_number (string name) {
371 foreach (unowned City city in city_list) {
372 if (city.name == name)
378 public unowned City find_nearest_city (double latitude, double longitude) {
379 unowned City result = null;
380 double min_distance = 0.0;
381 bool in_result = false;
383 foreach (unowned City city in city_list) {
384 double lat = latitude - city.latitude;
385 double lng = longitude - city.longitude;
386 double distance = lat * lat + lng * lng;
387 bool in_city = ((city.south <= latitude <= city.north) &&
388 (city.west <= longitude <= city.east));
390 if ((result == null) ||
391 (in_city && !in_result) ||
392 (in_city && in_result && distance / city.bb_area () < min_distance / result.bb_area ()) ||
393 (!in_city && !in_result && distance < min_distance)) {
395 min_distance = distance;
403 public string? get_lift_list_url (string city_from, int radius_from, string city_to, int radius_to, Date date, int tolerance = 0) {
404 if (city_list == null)
407 int num_from = get_city_number (city_from);
411 int num_to = get_city_number (city_to);
415 string url = HTTP_BASE_URI + "/mitfahrclub/%s/%s/b.html".printf (
420 url += "?type=b&city_from=%d&radius_from=%d&city_to=%d&radius_to=%d".printf (
427 url += "&date=date&day=%d&month=%d&year=%d&tolerance=%d&smoking=&avg_speed=&".printf (
437 public async List<Lift>? get_lift_list (string city_from, int radius_from, string city_to, int radius_to, Date date, int tolerance = 0) {
438 var doc = yield get_html_document (get_lift_list_url (city_from, radius_from, city_to, radius_to, date, tolerance));
440 stderr.printf ("Error: parsing failed\n");
444 var table = search_tag_by_class (doc->children, "table", "list p_15");
446 stderr.printf ("Error: does not contain list p_15 table\n");
450 var list = new List<Lift> ();
451 for (var n = table->children; n != null; n = n->next) {
452 if (n->name == "tr") {
453 var lift = parse_lift_row (n->children);
454 if (lift.city_from != null) // Skip the title row
455 list.append ((owned) lift);
460 var div = table->next;
461 if (div != null && div->get_prop ("class") == "error-message") {
462 if (div->children == null || div->children->content == null ||
463 !div->children->content.has_prefix ("Es sind leider noch keine Einträge vorhanden.")) {
464 stderr.printf ("Got an unknown error message!\n");
465 if (div->children != null && div->children->content != null)
466 stderr.printf ("\"%s\"\n", div->children->content);
473 Lift parse_lift_row (Xml.Node* node) {
474 var lift = new Lift ();
476 for (var n = node; n != null; n = n->next) {
477 if (n->name == "td") {
478 var n2 = n->children;
480 if (n2->name == "a") {
481 var href = n2->get_prop ("href");
482 if (href != null && lift.href == null)
484 var n3 = n2->children;
486 if (n3->name == "text")
489 lift.city_from = n3->content;
492 lift.city_to = n3->content;
495 parse_date (n3->content, out lift.time);
498 parse_time (n3->content.strip (), out lift.time);
501 lift.places = n3->content.to_int ();
504 lift.price = n3->content;
507 print ("TEXT:%s\n", n3->content);
510 if (n3->name == "span") {
511 string class = n3->get_prop ("class");
512 if (class == "icon_smoker")
513 lift.flags |= LiftFlags.SMOKER;
514 else if (class == "icon_non_smoker")
515 lift.flags |= LiftFlags.NON_SMOKER;
516 else if (class == "icon_adac")
517 lift.flags |= LiftFlags.ADAC_MEMBER;
518 else if (class == "icon_women")
519 lift.flags |= LiftFlags.WOMEN_ONLY;
520 else if (class != null)
521 print ("SPAN %s\n", class);
534 public string get_lift_details_url (Lift lift) {
535 return HTTP_BASE_URI + lift.href;
538 public async bool update_lift_details (Lift lift) {
539 var doc = yield get_html_document (get_lift_details_url (lift));
541 stderr.printf ("Error: parsing failed\n");
545 var table = search_tag_by_class (doc->children, "table", "lift");
547 stderr.printf ("Error: does not contain lift table\n");
552 for (n = table->children; n != null; n = n->next) {
553 if (n->name == "tr") {
554 var n2 = n->children;
555 if (n2 == null || n2->name != "td" ||
556 n2->children == null || n2->children->name != "text")
559 string text = n2->children->content;
561 if (text != "Strecke & Infos" && text != " " && !text.has_prefix ("\xc2\xa0") &&
563 text != "Freie Pl\xc3\xa4tze" &&
567 text != "Telefon 2" &&
568 text != "E-Mail 1" &&
570 text != "Beschreibung")
577 // Skip text between td nodes
578 if (n2->name == "text")
581 if (n2 == null || n2->name != "td" || n2->children == null)
584 if (n2->children->name == "img") {
585 // FIXME: email image
586 lift.email_image_uri = n2->children->get_prop ("src");
590 if (n2->children->name == "div" && text == "Beschreibung") {
591 var n3 = n2->children->children;
592 lift.description = "";
594 if (n3->name == "text")
595 lift.description += n3->content.strip () + "\n";
599 } else if (n2->children->name != "text") {
603 var text1 = n2->children->content.strip ();
605 if (text == "Freie Pl\xc3\xa4tze")
606 lift.places = text1.to_int ();
607 else if (text == "Name")
609 else if (text == "Handy")
611 else if (text == "Telefon")
613 else if (text == "Telefon 2")
615 else if (text == "E-Mail 1")
617 else if (text != "Strecke & Infos" && text != " " &&
618 !text.has_prefix ("\xc2\xa0") && text != "Datum" &&
626 // Skip text between td nodes
627 if (n2->name == "text")
630 if (n2 == null || n2->name != "td" ||
631 n2->children == null)
634 if (n2->children->name == "span" &&
635 n2->children->get_prop ("class") == "icon_non_smoker") {
636 lift.flags |= LiftFlags.NON_SMOKER;
638 } else if (n2->children->name == "span" &&
639 n2->children->get_prop ("class") == "icon_smoker") {
640 lift.flags |= LiftFlags.SMOKER;
642 } else if (n2->children->name != "text")
645 var text2 = n2->children->content.strip ();
648 lift.city_from = text2;
649 else if (text1.has_prefix ("\xc3\xbc"))
650 lift.city_via.append (text2);
651 else if (text1 == "nach")
652 lift.city_to = text2;
653 else if (text1 == "Datum")
654 parse_date (text2, out lift.time);
655 else if (text1 == "Uhrzeit")
656 parse_time (text2, out lift.time);
657 else if (text1 == "Raucher")
658 print ("Raucher: %s\n", text2);
659 else if (text1 == "Fahrpreis")
661 else if (text1 == "ADAC-Mitglied" && text2 != "nein")
662 lift.flags |= LiftFlags.ADAC_MEMBER;
666 // The paragraph after the table contains the date of last modification
668 for (n = p->children; n != null; n = n->next) {
669 if (n->name != "text")
672 var s = n->content.strip ();
673 if (s.has_prefix ("Letztmalig aktualisiert am "))
674 lift.modified = s.offset (27).dup (); // "Do 15.04.2010 20:32"
680 public string get_my_information_url () {
681 return HTTPS_BASE_URI + "/users/view";
684 public async MyInformation get_my_information () {
685 var doc = yield get_html_document (get_my_information_url ());
687 stderr.printf ("Error: parsing failed\n");
691 var table = search_tag_by_class (doc->children, "table", "user");
693 stderr.printf ("Error: does not contain user table\n");
697 var my_info = new MyInformation ();
700 for (n = table->children; n != null; n = n->next) {
701 if (n->name == "tr") {
702 var n2 = n->children;
703 if (n2 == null || n2->name != "td" ||
704 n2->children == null || n2->children->name != "text")
707 string text = n2->children->content;
710 if (n2 != null && n2->name == "text")
712 if (n2 == null || n2->name != "td" ||
713 n2->children == null || n2->children->name != "text")
716 string content = n2->children->content;
720 my_info.gender = (content == "Herr") ? MyInformation.Gender.MALE : MyInformation.Gender.FEMALE;
723 my_info.title = content;
726 my_info.first_name = content;
729 my_info.last_name = content;
732 // my_info.birthday = ...
734 case "registriert seit":
735 // my_info.registered_since = content;
738 // print ("\t%s=%s\n", text, content);
745 if (n2 != null && n2->name == "text")
747 if (n2 == null || n2->name != "td")
750 if (n2->children != null && n2->children->name != "text")
751 content = n2->children->content;
757 my_info.street = content;
761 my_info.zip_code = "";
762 my_info.city = content;
765 my_info.country = content;
768 my_info.phone1 = content;
771 my_info.phone2 = content;
774 my_info.phone3 = content;
777 my_info.cell = content;
780 my_info.email1 = content;
783 my_info.email2 = content;
787 my_info.smoker = false;
789 case "ADAC-Mitglied":
791 my_info.adac_member = false;
794 // print ("\"%s\"=\"%s\"\n", text, content);
801 <tr class="head top">
802 <td width="150" class="label">Anrede</td>
803 <td width="400">Herr</td>
806 <td class="label">Titel</td>
815 Xml.Node* search_tag_by_property (Xml.Node* node, string tag, string prop, string val) requires (node != null) {
816 for (var n = node; n != null; n = n->next) {
817 if (n->name == tag && n->get_prop (prop) == val)
819 if (n->children != null) {
820 var found = search_tag_by_property (n->children, tag, prop, val);
828 Xml.Node* search_tag_by_id (Xml.Node* node, string tag, string id) requires (node != null) {
829 return search_tag_by_property (node, tag, "id", id);
832 Xml.Node* search_tag_by_name (Xml.Node* node, string tag, string name) requires (node != null) {
833 return search_tag_by_property (node, tag, "name", name);
836 Xml.Node* search_tag_by_class (Xml.Node* node, string tag, string @class) requires (node != null) {
837 return search_tag_by_property (node, tag, "class", @class);
840 void parse_date (string date, out Time time) {
842 if (date.length == 11)
843 date = date.offset (3);
844 if (date.length != 8)
846 var res = date.scanf ("%02d.%02d.%02d", out time.day, out time.month, out year);
847 time.year = year + 2000;
850 void parse_time (string time, out Time result) {
851 var res = time.scanf ("%d.%02d Uhr", out result.hour, out result.minute);