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 {
50 public class Lift : Object {
51 public string city_from;
52 public string city_to;
56 public LiftFlags flags;
60 public List<string> city_via;
66 public string email_image_uri;
67 public string description;
68 public string modified;
75 public class MyInformation {
83 public string first_name;
84 public string last_name;
86 public Date registered_since;
91 public string zip_code;
93 public string country;
100 public string email1;
101 public string email2;
104 public bool adac_member;
107 public class AdacMitfahrclub {
108 const string HTTP_BASE_URI = "http://mitfahrclub.adac.de";
109 const string HTTPS_BASE_URI = "https://mitfahrclub.adac.de";
111 Curl.EasyHandle curl;
112 List<City> city_list = null;
114 public AdacMitfahrclub () {
115 curl = new Curl.EasyHandle ();
116 // FIXME: Fremantle SDK doesn't come with certs
117 curl.setopt (Curl.Option.SSL_VERIFYPEER, 0);
118 // curl.setopt (Curl.Option.VERBOSE, 1);
121 private string _url = null;
122 private SourceFunc callback = null;
123 void* download_thread () {
124 curl.setopt (Curl.Option.WRITEFUNCTION, write_callback);
125 curl.setopt (Curl.Option.WRITEDATA, this);
126 curl.setopt (Curl.Option.URL, _url);
127 if (aeolus_cookie != null)
128 curl.setopt (Curl.Option.COOKIE, "MIKINIMEDIA=%s; Quirinus[adacAeolus]=%s;".printf (mikini_cookie, aeolus_cookie));
129 else if (mikini_cookie != null)
130 curl.setopt (Curl.Option.COOKIE, "MIKINIMEDIA=%s".printf (mikini_cookie));
131 var res = curl.perform ();
133 if (callback != null)
140 StringBuilder result;
142 [CCode (instance_pos = -1)]
143 size_t write_callback (void *buffer, size_t size, size_t nmemb) {
144 // if (cancellable != null && cancellable.is_cancelled ())
147 result.append_len ((string) buffer, (ssize_t) (size * nmemb));
152 private async Html.Doc* get_html_document (string url) {
154 callback = get_html_document.callback;
155 result = new StringBuilder ();
157 Thread.create(download_thread, false);
158 } catch (ThreadError e) {
159 critical ("Failed to create download thread\n");
165 return Html.Doc.read_memory ((char[]) result.str, (int) result.len,
166 url, null, Html.ParserOption.NOERROR | Html.ParserOption.NOWARNING);
169 private string username;
170 private string password;
171 private string mikini_cookie;
172 private string aeolus_cookie;
174 public void set_cookie (string value) {
175 aeolus_cookie = value;
178 public void set_credentials (string _username, string _password) {
179 username = _username;
180 password = _password;
183 public void login (string? _username, string? _password) {
184 set_credentials (_username, _password);
187 if (login_callback != null)
192 public bool logged_in = false;
193 private SourceFunc login_callback = null;
194 public async bool login_async () {
197 if (login_callback != null || username == null || password == null)
199 login_callback = login_async.callback;
200 result = new StringBuilder ();
202 Thread.create(login_thread, false);
203 } catch (ThreadError e) {
204 critical ("Failed to create login thread\n");
209 login_callback = null;
214 void *login_thread () {
215 result = new StringBuilder ();
216 curl.setopt (Curl.Option.URL, HTTP_BASE_URI);
217 curl.setopt (Curl.Option.WRITEFUNCTION, write_callback);
218 curl.setopt (Curl.Option.WRITEDATA, this);
219 curl.setopt (Curl.Option.COOKIEFILE, "");
220 var res = curl.perform ();
223 curl.getinfo (Curl.Info.COOKIELIST, out cookies);
224 unowned Curl.SList cookie = cookies;
225 while (cookie != null) {
226 if (cookie.data != null) {
227 var c = cookie.data.split ("\t");
229 print ("%s=%s\n", c[5], c[6]);
230 if (c[5] == "MIKINIMEDIA")
231 mikini_cookie = c[6];
233 cookie = cookie.next;
236 result = new StringBuilder ();
237 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);
238 curl.setopt (Curl.Option.POSTFIELDS, postdata);
239 curl.setopt (Curl.Option.URL, HTTPS_BASE_URI + "/users/login/");
240 curl.setopt (Curl.Option.SSL_VERIFYPEER, 0);
241 res = curl.perform ();
242 // print ("%s\n", result.str);
245 curl.getinfo (Curl.Info.COOKIELIST, out cookies);
247 while (cookie != null) {
248 if (cookie.data != null) {
249 var c = cookie.data.split ("\t");
251 print ("%s=%s\n", c[5], c[6]);
252 // "Quirinus[adacAeolus]"
253 if (c[5] == "Quirinus[adacAeolus]") {
254 aeolus_cookie = c[6];
258 cookie = cookie.next;
261 if (result.str.contains ("<div id=\"flashMessage\" class=\"message\">Die eingegebenen Zugangsdaten konnten nicht gefunden werden. Bitte versuchen Sie es erneut.</div>")) {
262 print ("LOGIN FAILED\n");
263 aeolus_cookie = null;
267 if (login_callback != null)
268 Idle.add (login_callback);
272 private void save_city_list () {
273 FileStream list_file = FileStream.open ("/home/user/.cache/beifahrer/city_list", "w");
274 if (list_file == null)
277 foreach (unowned City city in city_list) {
278 if (city.north != 0.0 || city.south != 0.0 || city.east != 0.0 || city.west != 0.0)
279 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);
280 else if (city.latitude != 0.0 || city.longitude != 0.0)
281 list_file.printf ("%d\t%s\t%f\t%f\n", city.number, city.name, city.latitude, city.longitude);
283 list_file.printf ("%d\t%s\n", city.number, city.name);
287 private bool load_city_list () {
288 FileStream list_file = FileStream.open ("/home/user/.cache/beifahrer/city_list", "r");
289 if (list_file == null)
290 list_file = FileStream.open ("/usr/share/beifahrer/city_list", "r");
291 if (list_file == null)
294 city_list = new List<City> ();
295 string line = list_file.read_line ();
296 while (line != null) {
297 var split_line = line.split ("\t");
298 if (split_line.length < 2)
300 int number = split_line[0].to_int ();
301 weak string name = split_line[1];
303 var city = new City (number, name);
304 if (split_line.length >= 4) {
305 city.latitude = split_line[2].to_double ();
306 city.longitude = split_line[3].to_double ();
308 if (split_line.length >= 8) {
309 city.north = split_line[4].to_double ();
310 city.south = split_line[5].to_double ();
311 city.east = split_line[6].to_double ();
312 city.west = split_line[7].to_double ();
314 city_list.append ((owned) city);
316 line = list_file.read_line ();
322 public unowned List<City>? get_city_list () {
323 if (city_list != null)
326 if (load_city_list ())
332 public async unowned List<City>? download_city_list () {
333 var doc = yield get_html_document (HTTP_BASE_URI);
335 stderr.printf ("Error: parsing failed\n");
339 var form = search_tag_by_id (doc->children, "form", "search_national_form");
341 stderr.printf ("Error: does not contain search_national_form\n");
345 var select = search_tag_by_name (form->children, "select", "city_from");
346 if (select == null) {
347 stderr.printf ("Error: does not contain city_from\n");
351 city_list = new List<City> ();
352 for (var n = select->children; n != null; n = n->next) {
353 if (n->name == "option" && n->children != null && n->children->name == "text") {
354 int number = n->get_prop ("value").to_int ();
355 // Skip 0 "Alle St.dte"
358 var city = new City(number,
359 n->children->content);
360 city_list.append ((owned) city);
364 // TODO: get coordinates
371 private int get_city_number (string name) {
372 foreach (unowned City city in city_list) {
373 if (city.name == name)
379 public unowned City find_nearest_city (double latitude, double longitude) {
380 unowned City result = null;
381 double min_distance = 0.0;
382 bool in_result = false;
384 foreach (unowned City city in city_list) {
385 double lat = latitude - city.latitude;
386 double lng = longitude - city.longitude;
387 double distance = lat * lat + lng * lng;
388 bool in_city = ((city.south <= latitude <= city.north) &&
389 (city.west <= longitude <= city.east));
391 if ((result == null) ||
392 (in_city && !in_result) ||
393 (in_city && in_result && distance / city.bb_area () < min_distance / result.bb_area ()) ||
394 (!in_city && !in_result && distance < min_distance)) {
396 min_distance = distance;
404 public string? get_lift_list_url (string city_from, int radius_from, string city_to, int radius_to, Date date, int tolerance = 0) {
405 if (city_list == null)
408 int num_from = get_city_number (city_from);
412 int num_to = get_city_number (city_to);
416 string url = HTTP_BASE_URI + "/mitfahrclub/%s/%s/b.html".printf (
421 url += "?type=b&city_from=%d&radius_from=%d&city_to=%d&radius_to=%d".printf (
428 url += "&date=date&day=%d&month=%d&year=%d&tolerance=%d&smoking=&avg_speed=&".printf (
438 public async List<Lift>? get_lift_list (string city_from, int radius_from, string city_to, int radius_to, Date date, int tolerance = 0) {
439 var doc = yield get_html_document (get_lift_list_url (city_from, radius_from, city_to, radius_to, date, tolerance));
441 stderr.printf ("Error: parsing failed\n");
445 var table = search_tag_by_class (doc->children, "table", "list p_15");
447 stderr.printf ("Error: does not contain list p_15 table\n");
451 var list = new List<Lift> ();
452 for (var n = table->children; n != null; n = n->next) {
453 if (n->name == "tr") {
454 var lift = parse_lift_row (n->children);
455 if (lift.city_from != null) // Skip the title row
456 list.append ((owned) lift);
461 var div = table->next;
462 if (div != null && div->get_prop ("class") == "error-message") {
463 if (div->children == null || div->children->content == null ||
464 !div->children->content.has_prefix ("Es sind leider noch keine Einträge vorhanden.")) {
465 stderr.printf ("Got an unknown error message!\n");
466 if (div->children != null && div->children->content != null)
467 stderr.printf ("\"%s\"\n", div->children->content);
474 Lift parse_lift_row (Xml.Node* node) {
475 var lift = new Lift ();
477 for (var n = node; n != null; n = n->next) {
478 if (n->name == "td") {
479 var n2 = n->children;
481 if (n2->name == "a") {
482 var href = n2->get_prop ("href");
483 if (href != null && lift.href == null)
485 var n3 = n2->children;
487 if (n3->name == "text")
490 lift.city_from = n3->content;
493 lift.city_to = n3->content;
496 parse_date (n3->content, out lift.time);
499 parse_time (n3->content.strip (), out lift.time);
502 lift.places = n3->content.to_int ();
505 lift.price = n3->content;
508 print ("TEXT:%s\n", n3->content);
511 if (n3->name == "span") {
512 string class = n3->get_prop ("class");
513 if (class == "icon_smoker")
514 lift.flags |= LiftFlags.SMOKER;
515 else if (class == "icon_non_smoker")
516 lift.flags |= LiftFlags.NON_SMOKER;
517 else if (class == "icon_adac")
518 lift.flags |= LiftFlags.ADAC_MEMBER;
519 else if (class == "icon_women")
520 lift.flags |= LiftFlags.WOMEN_ONLY;
521 else if (class != null)
522 print ("SPAN %s\n", class);
535 public string get_lift_details_url (Lift lift) {
536 return HTTP_BASE_URI + lift.href;
539 public async bool update_lift_details (Lift lift) {
540 var doc = yield get_html_document (get_lift_details_url (lift));
542 stderr.printf ("Error: parsing failed\n");
546 var table = search_tag_by_class (doc->children, "table", "lift");
548 stderr.printf ("Error: does not contain lift table\n");
553 for (n = table->children; n != null; n = n->next) {
554 if (n->name == "tr") {
555 var n2 = n->children;
556 if (n2 == null || n2->name != "td" ||
557 n2->children == null || n2->children->name != "text")
560 string text = n2->children->content;
562 if (text != "Strecke & Infos" && text != " " && !text.has_prefix ("\xc2\xa0") &&
564 text != "Freie Pl\xc3\xa4tze" &&
568 text != "Telefon 2" &&
569 text != "E-Mail 1" &&
571 text != "Beschreibung")
578 // Skip text between td nodes
579 if (n2->name == "text")
582 if (n2 == null || n2->name != "td" || n2->children == null)
585 if (n2->children->name == "img") {
586 // FIXME: email image
587 lift.email_image_uri = n2->children->get_prop ("src");
591 if (n2->children->name == "div" && text == "Beschreibung") {
592 var n3 = n2->children->children;
593 lift.description = "";
595 if (n3->name == "text")
596 lift.description += n3->content.strip () + "\n";
600 } else if (n2->children->name != "text") {
604 var text1 = n2->children->content.strip ();
606 if (text == "Freie Pl\xc3\xa4tze")
607 lift.places = text1.to_int ();
608 else if (text == "Name")
610 else if (text == "Handy")
612 else if (text == "Telefon")
614 else if (text == "Telefon 2")
616 else if (text == "E-Mail 1")
618 else if (text != "Strecke & Infos" && text != " " &&
619 !text.has_prefix ("\xc2\xa0") && text != "Datum" &&
627 // Skip text between td nodes
628 if (n2->name == "text")
631 if (n2 == null || n2->name != "td" ||
632 n2->children == null)
635 if (n2->children->name == "span" &&
636 n2->children->get_prop ("class") == "icon_non_smoker") {
637 lift.flags |= LiftFlags.NON_SMOKER;
639 } else if (n2->children->name == "span" &&
640 n2->children->get_prop ("class") == "icon_smoker") {
641 lift.flags |= LiftFlags.SMOKER;
643 } else if (n2->children->name != "text")
646 var text2 = n2->children->content.strip ();
649 lift.city_from = text2;
650 else if (text1.has_prefix ("\xc3\xbc"))
651 lift.city_via.append (text2);
652 else if (text1 == "nach")
653 lift.city_to = text2;
654 else if (text1 == "Datum")
655 parse_date (text2, out lift.time);
656 else if (text1 == "Uhrzeit")
657 parse_time (text2, out lift.time);
658 else if (text1 == "Raucher")
659 print ("Raucher: %s\n", text2);
660 else if (text1 == "Fahrpreis")
662 else if (text1 == "ADAC-Mitglied" && text2 != "nein")
663 lift.flags |= LiftFlags.ADAC_MEMBER;
667 // The paragraph after the table contains the date of last modification
669 for (n = p->children; n != null; n = n->next) {
670 if (n->name != "text")
673 var s = n->content.strip ();
674 if (s.has_prefix ("Letztmalig aktualisiert am "))
675 lift.modified = s.offset (27).dup (); // "Do 15.04.2010 20:32"
681 public string get_my_information_url () {
682 return HTTPS_BASE_URI + "/users/view";
685 public async MyInformation get_my_information () {
686 var doc = yield get_html_document (get_my_information_url ());
688 stderr.printf ("Error: parsing failed\n");
692 var table = search_tag_by_class (doc->children, "table", "user");
694 stderr.printf ("Error: does not contain user table\n");
698 var my_info = new MyInformation ();
701 for (n = table->children; n != null; n = n->next) {
702 if (n->name == "tr") {
703 var n2 = n->children;
704 if (n2 == null || n2->name != "td" ||
705 n2->children == null || n2->children->name != "text")
708 string text = n2->children->content;
711 if (n2 != null && n2->name == "text")
713 if (n2 == null || n2->name != "td" ||
714 n2->children == null || n2->children->name != "text")
717 string content = n2->children->content;
721 my_info.gender = (content == "Herr") ? MyInformation.Gender.MALE : MyInformation.Gender.FEMALE;
724 my_info.title = content;
727 my_info.first_name = content;
730 my_info.last_name = content;
733 // my_info.birthday = ...
735 case "registriert seit":
736 // my_info.registered_since = content;
739 // print ("\t%s=%s\n", text, content);
746 if (n2 != null && n2->name == "text")
748 if (n2 == null || n2->name != "td")
751 if (n2->children != null && n2->children->name != "text")
752 content = n2->children->content;
758 my_info.street = content;
762 my_info.zip_code = "";
763 my_info.city = content;
766 my_info.country = content;
769 my_info.phone1 = content;
772 my_info.phone2 = content;
775 my_info.phone3 = content;
778 my_info.cell = content;
781 my_info.email1 = content;
784 my_info.email2 = content;
788 my_info.smoker = false;
790 case "ADAC-Mitglied":
792 my_info.adac_member = false;
795 // print ("\"%s\"=\"%s\"\n", text, content);
802 <tr class="head top">
803 <td width="150" class="label">Anrede</td>
804 <td width="400">Herr</td>
807 <td class="label">Titel</td>
816 public string get_my_offers_url () {
817 return HTTP_BASE_URI + "/lifts/mysinglelifts";
820 public async List<Lift>? get_my_offers () {
821 var doc = yield get_html_document (get_my_offers_url ());
823 stderr.printf ("Error: parsing failed\n");
827 var table = search_tag_by_class (doc->children, "table", "list");
829 stderr.printf ("Error: does not contain user table\n");
833 var list = new List<Lift> ();
834 for (var n = table->children; n != null; n = n->next) {
835 if (n->name == "tr") {
836 var lift = parse_offer_row (n);
837 if (lift != null) // Skip the title row
838 list.append ((owned) lift);
842 if (table->next != null && table->next->name == "div") {
843 var text = get_child_text_content (table->next);
845 print ("\"%s\"\n", text);
846 if (text == "Sie haben derzeit keine einmaligen Fahrten eingetragen") {
847 print ("NO ENTRIES\n");
855 Lift? parse_offer_row (Xml.Node *tr) {
856 var lift = new Lift ();
859 var td = get_next_td (tr->children);
864 td = get_next_td (td->next);
870 td = get_next_td (td->next);
873 var text = get_child_text_content (td);
877 if (text != "Mitfahrer")
880 // point of departure
881 td = get_next_td (td->next);
884 text = get_child_text_content (td);
887 lift.city_from = text;
890 td = get_next_td (td->next);
893 text = get_child_text_content (td);
899 td = get_next_td (td->next);
902 text = get_child_text_content (td);
905 parse_date (text, out lift.time);
908 td = get_next_td (td->next);
911 text = get_child_text_content (td);
914 parse_time (text, out lift.time);
917 td = get_next_td (td->next);
920 var a = td->children;
921 if (a == null || a->name != "a")
923 text = a->get_prop ("class");
924 if (text == "status icon icon_ajax_active")
925 lift.flags |= LiftFlags.ACTIVE;
930 Xml.Node* get_next_td (Xml.Node *n) {
939 unowned string get_child_text_content (Xml.Node *n) {
940 if (n->children != null && n->children->name == "text")
941 return n->children->content;
946 Xml.Node* search_tag_by_property (Xml.Node* node, string tag, string prop, string val) requires (node != null) {
947 for (var n = node; n != null; n = n->next) {
948 if (n->name == tag && n->get_prop (prop) == val)
950 if (n->children != null) {
951 var found = search_tag_by_property (n->children, tag, prop, val);
959 Xml.Node* search_tag_by_id (Xml.Node* node, string tag, string id) requires (node != null) {
960 return search_tag_by_property (node, tag, "id", id);
963 Xml.Node* search_tag_by_name (Xml.Node* node, string tag, string name) requires (node != null) {
964 return search_tag_by_property (node, tag, "name", name);
967 Xml.Node* search_tag_by_class (Xml.Node* node, string tag, string @class) requires (node != null) {
968 return search_tag_by_property (node, tag, "class", @class);
971 void parse_date (string date, out Time time) {
973 if (date.length == 12) // "Mo, 01.02.03"
974 date = date.offset (4);
975 else if (date.length == 11) // "Mo 01.02.03"
976 date = date.offset (3);
977 if (date.length != 8) // "01.02.03"
979 var res = date.scanf ("%02d.%02d.%02d", out time.day, out time.month, out year);
980 time.year = year + 2000;
983 void parse_time (string time, out Time result) {
984 var res = time.scanf ("%d.%02d Uhr", out result.hour, out result.minute);