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 login (string? _username, string? _password) {
178 username = _username;
179 password = _password;
182 if (login_callback != null)
187 public bool logged_in = false;
188 private SourceFunc login_callback = null;
189 public async bool login_async (string? _username, string? _password) {
190 username = _username;
191 password = _password;
194 if (login_callback != null)
196 login_callback = login_async.callback;
197 result = new StringBuilder ();
199 Thread.create(login_thread, false);
200 } catch (ThreadError e) {
201 critical ("Failed to create login thread\n");
206 login_callback = null;
211 void *login_thread () {
212 result = new StringBuilder ();
213 curl.setopt (Curl.Option.URL, HTTP_BASE_URI);
214 curl.setopt (Curl.Option.WRITEFUNCTION, write_callback);
215 curl.setopt (Curl.Option.WRITEDATA, this);
216 curl.setopt (Curl.Option.COOKIEFILE, "");
217 var res = curl.perform ();
220 curl.getinfo (Curl.Info.COOKIELIST, out cookies);
221 unowned Curl.SList cookie = cookies;
222 while (cookie != null) {
223 if (cookie.data != null) {
224 var c = cookie.data.split ("\t");
226 print ("%s=%s\n", c[5], c[6]);
227 if (c[5] == "MIKINIMEDIA")
228 mikini_cookie = c[6];
230 cookie = cookie.next;
233 result = new StringBuilder ();
234 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);
235 curl.setopt (Curl.Option.POSTFIELDS, postdata);
236 curl.setopt (Curl.Option.URL, HTTPS_BASE_URI + "/users/login/");
237 curl.setopt (Curl.Option.SSL_VERIFYPEER, 0);
238 res = curl.perform ();
239 // print ("%s\n", result.str);
242 curl.getinfo (Curl.Info.COOKIELIST, out cookies);
244 while (cookie != null) {
245 if (cookie.data != null) {
246 var c = cookie.data.split ("\t");
248 print ("%s=%s\n", c[5], c[6]);
249 // "Quirinus[adacAeolus]"
250 if (c[5] == "Quirinus[adacAeolus]") {
251 aeolus_cookie = c[6];
255 cookie = cookie.next;
258 if (result.str.contains ("<div id=\"flashMessage\" class=\"message\">Die eingegebenen Zugangsdaten konnten nicht gefunden werden. Bitte versuchen Sie es erneut.</div>")) {
259 print ("LOGIN FAILED\n");
260 aeolus_cookie = null;
264 if (login_callback != null)
265 Idle.add (login_callback);
269 private void save_city_list () {
270 FileStream list_file = FileStream.open ("/home/user/.cache/beifahrer/city_list", "w");
271 if (list_file == null)
274 foreach (unowned City city in city_list) {
275 if (city.north != 0.0 || city.south != 0.0 || city.east != 0.0 || city.west != 0.0)
276 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);
277 else if (city.latitude != 0.0 || city.longitude != 0.0)
278 list_file.printf ("%d\t%s\t%f\t%f\n", city.number, city.name, city.latitude, city.longitude);
280 list_file.printf ("%d\t%s\n", city.number, city.name);
284 private bool load_city_list () {
285 FileStream list_file = FileStream.open ("/home/user/.cache/beifahrer/city_list", "r");
286 if (list_file == null)
287 list_file = FileStream.open ("/usr/share/beifahrer/city_list", "r");
288 if (list_file == null)
291 city_list = new List<City> ();
292 string line = list_file.read_line ();
293 while (line != null) {
294 var split_line = line.split ("\t");
295 if (split_line.length < 2)
297 int number = split_line[0].to_int ();
298 weak string name = split_line[1];
300 var city = new City (number, name);
301 if (split_line.length >= 4) {
302 city.latitude = split_line[2].to_double ();
303 city.longitude = split_line[3].to_double ();
305 if (split_line.length >= 8) {
306 city.north = split_line[4].to_double ();
307 city.south = split_line[5].to_double ();
308 city.east = split_line[6].to_double ();
309 city.west = split_line[7].to_double ();
311 city_list.append ((owned) city);
313 line = list_file.read_line ();
319 public unowned List<City>? get_city_list () {
320 if (city_list != null)
323 if (load_city_list ())
329 public async unowned List<City>? download_city_list () {
330 var doc = yield get_html_document (HTTP_BASE_URI);
332 stderr.printf ("Error: parsing failed\n");
336 var form = search_tag_by_id (doc->children, "form", "search_national_form");
338 stderr.printf ("Error: does not contain search_national_form\n");
342 var select = search_tag_by_name (form->children, "select", "city_from");
343 if (select == null) {
344 stderr.printf ("Error: does not contain city_from\n");
348 city_list = new List<City> ();
349 for (var n = select->children; n != null; n = n->next) {
350 if (n->name == "option" && n->children != null && n->children->name == "text") {
351 int number = n->get_prop ("value").to_int ();
352 // Skip 0 "Alle St.dte"
355 var city = new City(number,
356 n->children->content);
357 city_list.append ((owned) city);
361 // TODO: get coordinates
368 private int get_city_number (string name) {
369 foreach (unowned City city in city_list) {
370 if (city.name == name)
376 public unowned City find_nearest_city (double latitude, double longitude) {
377 unowned City result = null;
378 double min_distance = 0.0;
379 bool in_result = false;
381 foreach (unowned City city in city_list) {
382 double lat = latitude - city.latitude;
383 double lng = longitude - city.longitude;
384 double distance = lat * lat + lng * lng;
385 bool in_city = ((city.south <= latitude <= city.north) &&
386 (city.west <= longitude <= city.east));
388 if ((result == null) ||
389 (in_city && !in_result) ||
390 (in_city && in_result && distance / city.bb_area () < min_distance / result.bb_area ()) ||
391 (!in_city && !in_result && distance < min_distance)) {
393 min_distance = distance;
401 public async List<Lift>? get_lift_list (string city_from, int radius_from, string city_to, int radius_to, Date date, int tolerance = 0) {
402 if (city_list == null)
405 int num_from = get_city_number (city_from);
407 stderr.printf ("Unknown city: %s\n", city_to);
411 int num_to = get_city_number (city_to);
413 stderr.printf ("Unknown city: %s\n", city_to);
417 string url = HTTP_BASE_URI + "/mitfahrclub/%s/%s/b.html".printf (
422 url += "?type=b&city_from=%d&radius_from=%d&city_to=%d&radius_to=%d".printf (
429 url += "&date=date&day=%d&month=%d&year=%d&tolerance=%d&smoking=&avg_speed=&".printf (
436 var doc = yield get_html_document (url);
438 stderr.printf ("Error: parsing failed\n");
442 var table = search_tag_by_class (doc->children, "table", "list p_15");
444 stderr.printf ("Error: does not contain list p_15 table\n");
448 var list = new List<Lift> ();
449 for (var n = table->children; n != null; n = n->next) {
450 if (n->name == "tr") {
451 var lift = parse_lift_row (n->children);
452 if (lift.city_from != null) // Skip the title row
453 list.append ((owned) lift);
458 var div = table->next;
459 if (div != null && div->get_prop ("class") == "error-message") {
460 if (div->children == null || div->children->content == null ||
461 !div->children->content.has_prefix ("Es sind leider noch keine Einträge vorhanden.")) {
462 stderr.printf ("Got an unknown error message!\n");
463 if (div->children != null && div->children->content != null)
464 stderr.printf ("\"%s\"\n", div->children->content);
471 Lift parse_lift_row (Xml.Node* node) {
472 var lift = new Lift ();
474 for (var n = node; n != null; n = n->next) {
475 if (n->name == "td") {
476 var n2 = n->children;
478 if (n2->name == "a") {
479 var href = n2->get_prop ("href");
480 if (href != null && lift.href == null)
482 var n3 = n2->children;
484 if (n3->name == "text")
487 lift.city_from = n3->content;
490 lift.city_to = n3->content;
493 parse_date (n3->content, out lift.time);
496 parse_time (n3->content.strip (), out lift.time);
499 lift.places = n3->content.to_int ();
502 lift.price = n3->content;
505 print ("TEXT:%s\n", n3->content);
508 if (n3->name == "span") {
509 string class = n3->get_prop ("class");
510 if (class == "icon_smoker")
511 lift.flags |= LiftFlags.SMOKER;
512 else if (class == "icon_non_smoker")
513 lift.flags |= LiftFlags.NON_SMOKER;
514 else if (class == "icon_adac")
515 lift.flags |= LiftFlags.ADAC_MEMBER;
516 else if (class == "icon_women")
517 lift.flags |= LiftFlags.WOMEN_ONLY;
518 else if (class != null)
519 print ("SPAN %s\n", class);
532 public async bool update_lift_details (Lift lift) {
533 string url = HTTP_BASE_URI + lift.href;
535 var doc = yield get_html_document (url);
537 stderr.printf ("Error: parsing failed\n");
541 var table = search_tag_by_class (doc->children, "table", "lift");
543 stderr.printf ("Error: does not contain lift table\n");
548 for (n = table->children; n != null; n = n->next) {
549 if (n->name == "tr") {
550 var n2 = n->children;
551 if (n2 == null || n2->name != "td" ||
552 n2->children == null || n2->children->name != "text")
555 string text = n2->children->content;
557 if (text != "Strecke & Infos" && text != " " && !text.has_prefix ("\xc2\xa0") &&
559 text != "Freie Pl\xc3\xa4tze" &&
563 text != "Telefon 2" &&
564 text != "E-Mail 1" &&
566 text != "Beschreibung")
573 // Skip text between td nodes
574 if (n2->name == "text")
577 if (n2 == null || n2->name != "td" || n2->children == null)
580 if (n2->children->name == "img") {
581 // FIXME: email image
582 lift.email_image_uri = n2->children->get_prop ("src");
586 if (n2->children->name == "div" && text == "Beschreibung") {
587 var n3 = n2->children->children;
588 lift.description = "";
590 if (n3->name == "text")
591 lift.description += n3->content.strip () + "\n";
595 } else if (n2->children->name != "text") {
599 var text1 = n2->children->content.strip ();
601 if (text == "Freie Pl\xc3\xa4tze")
602 lift.places = text1.to_int ();
603 else if (text == "Name")
605 else if (text == "Handy")
607 else if (text == "Telefon")
609 else if (text == "Telefon 2")
611 else if (text == "E-Mail 1")
613 else if (text != "Strecke & Infos" && text != " " &&
614 !text.has_prefix ("\xc2\xa0") && text != "Datum" &&
622 // Skip text between td nodes
623 if (n2->name == "text")
626 if (n2 == null || n2->name != "td" ||
627 n2->children == null)
630 if (n2->children->name == "span" &&
631 n2->children->get_prop ("class") == "icon_non_smoker") {
632 lift.flags |= LiftFlags.NON_SMOKER;
634 } else if (n2->children->name == "span" &&
635 n2->children->get_prop ("class") == "icon_smoker") {
636 lift.flags |= LiftFlags.SMOKER;
638 } else if (n2->children->name != "text")
641 var text2 = n2->children->content.strip ();
644 lift.city_from = text2;
645 else if (text1.has_prefix ("\xc3\xbc"))
646 lift.city_via.append (text2);
647 else if (text1 == "nach")
648 lift.city_to = text2;
649 else if (text1 == "Datum")
650 parse_date (text2, out lift.time);
651 else if (text1 == "Uhrzeit")
652 parse_time (text2, out lift.time);
653 else if (text1 == "Raucher")
654 print ("Raucher: %s\n", text2);
655 else if (text1 == "Fahrpreis")
657 else if (text1 == "ADAC-Mitglied" && text2 != "nein")
658 lift.flags |= LiftFlags.ADAC_MEMBER;
662 // The paragraph after the table contains the date of last modification
664 for (n = p->children; n != null; n = n->next) {
665 if (n->name != "text")
668 var s = n->content.strip ();
669 if (s.has_prefix ("Letztmalig aktualisiert am "))
670 lift.modified = s.offset (27).dup (); // "Do 15.04.2010 20:32"
676 public async MyInformation get_my_information () {
677 string url = HTTPS_BASE_URI + "/users/view";
679 var doc = yield get_html_document (url);
681 stderr.printf ("Error: parsing failed\n");
685 var table = search_tag_by_class (doc->children, "table", "user");
687 stderr.printf ("Error: does not contain user table\n");
691 var my_info = new MyInformation ();
694 for (n = table->children; n != null; n = n->next) {
695 if (n->name == "tr") {
696 var n2 = n->children;
697 if (n2 == null || n2->name != "td" ||
698 n2->children == null || n2->children->name != "text")
701 string text = n2->children->content;
704 if (n2 != null && n2->name == "text")
706 if (n2 == null || n2->name != "td" ||
707 n2->children == null || n2->children->name != "text")
710 string content = n2->children->content;
714 my_info.gender = (content == "Herr") ? MyInformation.Gender.MALE : MyInformation.Gender.FEMALE;
717 my_info.title = content;
720 my_info.first_name = content;
723 my_info.last_name = content;
726 // my_info.birthday = ...
728 case "registriert seit":
729 // my_info.registered_since = content;
732 // print ("\t%s=%s\n", text, content);
739 if (n2 != null && n2->name == "text")
741 if (n2 == null || n2->name != "td")
744 if (n2->children != null && n2->children->name != "text")
745 content = n2->children->content;
751 my_info.street = content;
755 my_info.zip_code = "";
756 my_info.city = content;
759 my_info.country = content;
762 my_info.phone1 = content;
765 my_info.phone2 = content;
768 my_info.phone3 = content;
771 my_info.cell = content;
774 my_info.email1 = content;
777 my_info.email2 = content;
781 my_info.smoker = false;
783 case "ADAC-Mitglied":
785 my_info.adac_member = false;
788 // print ("\"%s\"=\"%s\"\n", text, content);
795 <tr class="head top">
796 <td width="150" class="label">Anrede</td>
797 <td width="400">Herr</td>
800 <td class="label">Titel</td>
809 Xml.Node* search_tag_by_property (Xml.Node* node, string tag, string prop, string val) requires (node != null) {
810 for (var n = node; n != null; n = n->next) {
811 if (n->name == tag && n->get_prop (prop) == val)
813 if (n->children != null) {
814 var found = search_tag_by_property (n->children, tag, prop, val);
822 Xml.Node* search_tag_by_id (Xml.Node* node, string tag, string id) requires (node != null) {
823 return search_tag_by_property (node, tag, "id", id);
826 Xml.Node* search_tag_by_name (Xml.Node* node, string tag, string name) requires (node != null) {
827 return search_tag_by_property (node, tag, "name", name);
830 Xml.Node* search_tag_by_class (Xml.Node* node, string tag, string @class) requires (node != null) {
831 return search_tag_by_property (node, tag, "class", @class);
834 void parse_date (string date, out Time time) {
836 if (date.length == 11)
837 date = date.offset (3);
838 if (date.length != 8)
840 var res = date.scanf ("%02d.%02d.%02d", out time.day, out time.month, out year);
841 time.year = year + 2000;
844 void parse_time (string time, out Time result) {
845 var res = time.scanf ("%d.%02d Uhr", out result.hour, out result.minute);