Adac Mitfahrclub: add my offers parser
[beifahrer] / src / adac-mitfahrclub.vala
1 /* This file is part of Beifahrer.
2  *
3  * Copyright (C) 2010 Philipp Zabel
4  *
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.
9  *
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.
14  *
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/>.
17  */
18
19 [Compact]
20 public class City {
21         public int number;
22         public string name;
23         public double latitude;
24         public double longitude;
25         public double north;
26         public double south;
27         public double east;
28         public double west;
29
30         public City (int _number, string _name) {
31                 number = _number;
32                 name = _name;
33                 latitude = 0.0;
34                 longitude = 0.0;
35         }
36
37         internal double bb_area () {
38                 return (north - south) * (east - west);
39         }
40 }
41
42 public enum LiftFlags {
43         SMOKER = 1,
44         NON_SMOKER = 2,
45         ADAC_MEMBER = 4,
46         WOMEN_ONLY = 8,
47         ACTIVE = 16,
48 }
49
50 public class Lift : Object {
51         public string city_from;
52         public string city_to;
53         public Time time;
54         public int places;
55         public string price;
56         public LiftFlags flags;
57
58         public string href;
59
60         public List<string> city_via;
61         public string name;
62         public string cell;
63         public string phone;
64         public string phone2;
65         public string email;
66         public string email_image_uri;
67         public string description;
68         public string modified;
69
70         public Lift () {
71                 time.hour = -1;
72         }
73 }
74
75 public class MyInformation {
76         public enum Gender {
77                 MALE,
78                 FEMALE
79         }
80
81         public Gender gender;
82         public string title;
83         public string first_name;
84         public string last_name;
85         public Date birthday;
86         public Date registered_since;
87
88         // Address
89         public string street;
90         public string number;
91         public string zip_code;
92         public string city;
93         public string country;
94
95         // Contact
96         public string phone1;
97         public string phone2;
98         public string phone3;
99         public string cell;
100         public string email1;
101         public string email2;
102
103         public bool smoker;
104         public bool adac_member;
105 }
106
107 public class AdacMitfahrclub {
108         const string HTTP_BASE_URI = "http://mitfahrclub.adac.de";
109         const string HTTPS_BASE_URI = "https://mitfahrclub.adac.de";
110
111         Curl.EasyHandle curl;
112         List<City> city_list = null;
113
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);
119         }
120
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 ();
132
133                 if (callback != null)
134                         Idle.add (callback);
135                 callback = null;
136
137                 return null;
138         }
139
140         StringBuilder result;
141
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 ())
145         //              return 0;
146
147                 result.append_len ((string) buffer, (ssize_t) (size * nmemb));
148
149                 return size * nmemb;
150         }
151
152         private async Html.Doc* get_html_document (string url) {
153                 _url = url;
154                 callback = get_html_document.callback;
155                 result = new StringBuilder ();
156                 try {
157                         Thread.create(download_thread, false);
158                 } catch (ThreadError e) {
159                         critical ("Failed to create download thread\n");
160                         return null;
161                 }
162
163                 yield;
164
165                 return Html.Doc.read_memory ((char[]) result.str, (int) result.len,
166                                              url, null, Html.ParserOption.NOERROR | Html.ParserOption.NOWARNING);
167         }
168
169         private string username;
170         private string password;
171         private string mikini_cookie;
172         private string aeolus_cookie;
173
174         public void set_cookie (string value) {
175                 aeolus_cookie = value;
176         }
177
178         public void set_credentials (string _username, string _password) {
179                 username = _username;
180                 password = _password;
181         }
182
183         public void login (string? _username, string? _password) {
184                 set_credentials (_username, _password);
185                 if (logged_in)
186                         return;
187                 if (login_callback != null)
188                         return;
189                 login_thread ();
190         }
191
192         public bool logged_in = false;
193         private SourceFunc login_callback = null;
194         public async bool login_async () {
195                 if (logged_in)
196                         return true;
197                 if (login_callback != null || username == null || password == null)
198                         return false;
199                 login_callback = login_async.callback;
200                 result = new StringBuilder ();
201                 try {
202                         Thread.create(login_thread, false);
203                 } catch (ThreadError e) {
204                         critical ("Failed to create login thread\n");
205                         return false;
206                 }
207
208                 yield;
209                 login_callback = null;
210
211                 return logged_in;
212         }
213
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 ();
221
222                 Curl.SList cookies;
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");
228                                 if (c.length > 5)
229                                         print ("%s=%s\n", c[5], c[6]);
230                                         if (c[5] == "MIKINIMEDIA")
231                                                 mikini_cookie = c[6];
232                         }
233                         cookie = cookie.next;
234                 }
235
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);
243
244                 cookies = null;
245                 curl.getinfo (Curl.Info.COOKIELIST, out cookies);
246                 cookie = cookies;
247                 while (cookie != null) {
248                         if (cookie.data != null) {
249                                 var c = cookie.data.split ("\t");
250                                 if (c.length > 5)
251                                         print ("%s=%s\n", c[5], c[6]);
252                                         // "Quirinus[adacAeolus]"
253                                         if (c[5] == "Quirinus[adacAeolus]") {
254                                                 aeolus_cookie = c[6];
255                                                 logged_in = true;
256                                         }
257                         }
258                         cookie = cookie.next;
259                 }
260
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;
264                         logged_in = false;
265                 }
266
267                 if (login_callback != null)
268                         Idle.add (login_callback);
269                 return null;
270         }
271
272         private void save_city_list () {
273                 FileStream list_file = FileStream.open ("/home/user/.cache/beifahrer/city_list", "w");
274                 if (list_file == null)
275                         return;
276
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);
282                         else
283                                 list_file.printf ("%d\t%s\n", city.number, city.name);
284                 }
285         }
286
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)
292                         return false;
293
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)
299                                 continue;
300                         int number = split_line[0].to_int ();
301                         weak string name = split_line[1];
302
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 ();
307                         }
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 ();
313                         }
314                         city_list.append ((owned) city);
315
316                         line = list_file.read_line ();
317                 }
318
319                 return true;
320         }
321
322         public unowned List<City>? get_city_list () {
323                 if (city_list != null)
324                         return city_list;
325
326                 if (load_city_list ())
327                         return city_list;
328
329                 return null;
330         }
331
332         public async unowned List<City>? download_city_list () {
333                 var doc = yield get_html_document (HTTP_BASE_URI);
334                 if (doc == null) {
335                         stderr.printf ("Error: parsing failed\n");
336                         return null;
337                 }
338
339                 var form = search_tag_by_id (doc->children, "form", "search_national_form");
340                 if (form == null) {
341                         stderr.printf ("Error: does not contain search_national_form\n");
342                         return null;
343                 }
344
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");
348                         return null;
349                 }
350
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"
356                                 if (number == 0)
357                                         continue;
358                                 var city = new City(number,
359                                                     n->children->content);
360                                 city_list.append ((owned) city);
361                         }
362                 }
363
364                 // TODO: get coordinates
365
366                 save_city_list ();
367
368                 return city_list;
369         }
370
371         private int get_city_number (string name) {
372                 foreach (unowned City city in city_list) {
373                         if (city.name == name)
374                                 return city.number;
375                 }
376                 return 0;
377         }
378
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;
383
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));
390
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)) {
395                                 result = city;
396                                 min_distance = distance;
397                                 in_result = in_city;
398                         }
399                 }
400
401                 return result;
402         }
403
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)
406                         get_city_list ();
407
408                 int num_from = get_city_number (city_from);
409                 if (num_from == 0)
410                         return null;
411
412                 int num_to = get_city_number (city_to);
413                 if (num_to == 0)
414                         return null;
415
416                 string url = HTTP_BASE_URI + "/mitfahrclub/%s/%s/b.html".printf (
417                         city_from,
418                         city_to
419                 );
420
421                 url += "?type=b&city_from=%d&radius_from=%d&city_to=%d&radius_to=%d".printf (
422                         num_from,
423                         radius_from,
424                         num_to,
425                         radius_to
426                 );
427
428                 url += "&date=date&day=%d&month=%d&year=%d&tolerance=%d&smoking=&avg_speed=&".printf (
429                         date.get_day (),
430                         date.get_month (),
431                         date.get_year (),
432                         tolerance
433                 );
434
435                 return url;
436         }
437
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));
440                 if (doc == null) {
441                         stderr.printf ("Error: parsing failed\n");
442                         return null;
443                 }
444
445                 var table = search_tag_by_class (doc->children, "table", "list p_15");
446                 if (table == null) {
447                         stderr.printf ("Error: does not contain list p_15 table\n");
448                         return null;
449                 }
450
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);
457                         }
458                 }
459
460                 // Error message?
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);
468                         }
469                 }
470
471                 return list;
472         }
473
474         Lift parse_lift_row (Xml.Node* node) {
475                 var lift = new Lift ();
476                 int i = 0;
477                 for (var n = node; n != null; n = n->next) {
478                         if (n->name == "td") {
479                                 var n2 = n->children;
480                                 if (n2 != null) {
481                                         if (n2->name == "a") {
482                                                 var href = n2->get_prop ("href");
483                                                 if (href != null && lift.href == null)
484                                                         lift.href = href;
485                                                 var n3 = n2->children;
486                                                 while (n3 != null) {
487                                                         if (n3->name == "text")
488                                                                 switch (i) {
489                                                                 case 0:
490                                                                         lift.city_from = n3->content;
491                                                                         break;
492                                                                 case 1:
493                                                                         lift.city_to = n3->content;
494                                                                         break;
495                                                                 case 2:
496                                                                         parse_date (n3->content, out lift.time);
497                                                                         break;
498                                                                 case 3:
499                                                                         parse_time (n3->content.strip (), out lift.time);
500                                                                         break;
501                                                                 case 4:
502                                                                         lift.places = n3->content.to_int ();
503                                                                         break;
504                                                                 case 5:
505                                                                         lift.price = n3->content;
506                                                                         break;
507                                                                 default:
508                                                                         print ("TEXT:%s\n", n3->content);
509                                                                         break;
510                                                                 }
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);
523                                                         }
524                                                         n3 = n3->next;
525                                                 }
526                                         }
527                                 }
528                                 i++;
529                         }
530                 }
531
532                 return lift;
533         }
534
535         public string get_lift_details_url (Lift lift) {
536                 return HTTP_BASE_URI + lift.href;
537         }
538
539         public async bool update_lift_details (Lift lift) {
540                 var doc = yield get_html_document (get_lift_details_url (lift));
541                 if (doc == null) {
542                         stderr.printf ("Error: parsing failed\n");
543                         return false;
544                 }
545
546                 var table = search_tag_by_class (doc->children, "table", "lift");
547                 if (table == null) {
548                         stderr.printf ("Error: does not contain lift table\n");
549                         return false;
550                 }
551
552                 Xml.Node* 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")
558                                         continue;
559
560                                 string text = n2->children->content;
561
562                                 if (text != "Strecke & Infos" && text != "&nbsp;" && !text.has_prefix ("\xc2\xa0") &&
563                                     text != "Datum" &&
564                                     text != "Freie Pl\xc3\xa4tze" &&
565                                     text != "Name" &&
566                                     text != "Handy" &&
567                                     text != "Telefon" &&
568                                     text != "Telefon 2" &&
569                                     text != "E-Mail 1" &&
570                                     text != "Details" &&
571                                     text != "Beschreibung")
572                                         continue;
573
574                                 n2 = n2->next;
575                                 if (n2 == null)
576                                         continue;
577
578                                 // Skip text between td nodes
579                                 if (n2->name == "text")
580                                         n2 = n2->next;
581
582                                 if (n2 == null || n2->name != "td" || n2->children == null)
583                                         continue;
584
585                                 if (n2->children->name == "img") {
586                                         // FIXME: email image
587                                          lift.email_image_uri = n2->children->get_prop ("src");
588                                         continue;
589                                 }
590
591                                 if (n2->children->name == "div" && text == "Beschreibung") {
592                                         var n3 = n2->children->children;
593                                         lift.description = "";
594                                         while (n3 != null) {
595                                                 if (n3->name == "text")
596                                                         lift.description += n3->content.strip () + "\n";
597                                                 n3 = n3->next;
598                                         }
599                                         continue;
600                                 } else if (n2->children->name != "text") {
601                                         continue;
602                                 }
603
604                                 var text1 = n2->children->content.strip ();
605
606                                 if (text == "Freie Pl\xc3\xa4tze")
607                                         lift.places = text1.to_int ();
608                                 else if (text == "Name")
609                                         lift.name = text1;
610                                 else if (text == "Handy")
611                                         lift.cell = text1;
612                                 else if (text == "Telefon")
613                                         lift.phone = text1;
614                                 else if (text == "Telefon 2")
615                                         lift.phone2 = text1;
616                                 else if (text == "E-Mail 1")
617                                         lift.email = text1;
618                                 else if (text != "Strecke & Infos" && text != "&nbsp;" &&
619                                     !text.has_prefix ("\xc2\xa0") && text != "Datum" &&
620                                     text != "Details")
621                                         continue;
622
623                                 n2 = n2->next;
624                                 if (n2 == null)
625                                         continue;
626
627                                 // Skip text between td nodes
628                                 if (n2->name == "text")
629                                         n2 = n2->next;
630
631                                 if (n2 == null || n2->name != "td" ||
632                                     n2->children == null)
633                                         continue;
634
635                                 if (n2->children->name == "span" &&
636                                     n2->children->get_prop ("class") == "icon_non_smoker") {
637                                         lift.flags |= LiftFlags.NON_SMOKER;
638                                         continue;
639                                 } else if (n2->children->name == "span" &&
640                                     n2->children->get_prop ("class") == "icon_smoker") {
641                                         lift.flags |= LiftFlags.SMOKER;
642                                         continue;
643                                 } else if (n2->children->name != "text")
644                                         continue;
645
646                                 var text2 = n2->children->content.strip ();
647
648                                 if (text1 == "von")
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")
661                                         lift.price = text2;
662                                 else if (text1 == "ADAC-Mitglied" && text2 != "nein")
663                                         lift.flags |= LiftFlags.ADAC_MEMBER;
664                         }
665                 }
666
667                 // The paragraph after the table contains the date of last modification
668                 var p = table->next;
669                 for (n = p->children; n != null; n = n->next) {
670                         if (n->name != "text")
671                                 continue;
672
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"
676                 }
677
678                 return true;
679         }
680
681         public string get_my_information_url () {
682                 return HTTPS_BASE_URI + "/users/view";
683         }
684
685         public async MyInformation get_my_information () {
686                 var doc = yield get_html_document (get_my_information_url ());
687                 if (doc == null) {
688                         stderr.printf ("Error: parsing failed\n");
689                         return null;
690                 }
691
692                 var table = search_tag_by_class (doc->children, "table", "user");
693                 if (table == null) {
694                         stderr.printf ("Error: does not contain user table\n");
695                         return null;
696                 }
697
698                 var my_info = new MyInformation ();
699
700                 Xml.Node* n;
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")
706                                         continue;
707
708                                 string text = n2->children->content;
709
710                                 n2 = n2->next;
711                                 if (n2 != null && n2->name == "text")
712                                         n2 = n2->next;
713                                 if (n2 == null || n2->name != "td" ||
714                                     n2->children == null || n2->children->name != "text")
715                                         continue;
716
717                                 string content = n2->children->content;
718
719                                 switch (text) {
720                                 case "Anrede":
721                                         my_info.gender = (content == "Herr") ? MyInformation.Gender.MALE : MyInformation.Gender.FEMALE;
722                                         continue;
723                                 case "Titel":
724                                         my_info.title = content;
725                                         continue;
726                                 case "Vorname":
727                                         my_info.first_name = content;
728                                         continue;
729                                 case "Name":
730                                         my_info.last_name = content;
731                                         continue;
732                                 case "Geburtsdatum":
733                         //              my_info.birthday = ...
734                                         continue;
735                                 case "registriert seit":
736                         //              my_info.registered_since = content;
737                                         continue;
738                         //      default:
739                         //              print ("\t%s=%s\n", text, content);
740                         //              break;
741                                 }
742
743                                 text = content;
744
745                                 n2 = n2->next;
746                                 if (n2 != null && n2->name == "text")
747                                         n2 = n2->next;
748                                 if (n2 == null || n2->name != "td")
749                                         continue;
750
751                                 if (n2->children != null && n2->children->name != "text")
752                                         content = n2->children->content;
753                                 else
754                                         content = "";
755
756                                 switch (text) {
757                                 case "Straße, Nr.":
758                                         my_info.street = content;
759                                         my_info.number = "";
760                                         continue;
761                                 case "PLZ, Ort":
762                                         my_info.zip_code = "";
763                                         my_info.city = content;
764                                         continue;
765                                 case "Land":
766                                         my_info.country = content;
767                                         continue;
768                                 case "Telefon 1":
769                                         my_info.phone1 = content;
770                                         continue;
771                                 case "Telefon 2":
772                                         my_info.phone2 = content;
773                                         continue;
774                                 case "Telefon 3":
775                                         my_info.phone3 = content;
776                                         continue;
777                                 case "Handy":
778                                         my_info.cell = content;
779                                         continue;
780                                 case "Email 1":
781                                         my_info.email1 = content;
782                                         continue;
783                                 case "Email 2":
784                                         my_info.email2 = content;
785                                         continue;
786                                 case "Raucher":
787                                         // FIXME
788                                         my_info.smoker = false;
789                                         continue;
790                                 case "ADAC-Mitglied":
791                                         // FIXME
792                                         my_info.adac_member = false;
793                                         continue;
794                         //      default:
795                         //              print ("\"%s\"=\"%s\"\n", text, content);
796                         //              break;
797                                 }
798                         }
799                 }
800
801 /*
802                         <tr class="head top">
803                                 <td width="150" class="label">Anrede</td>
804                                 <td width="400">Herr</td>
805                         </tr>
806                         <tr class="head">
807                                 <td class="label">Titel</td>
808
809                                 <td>--</td>
810                         </tr>
811                         ...
812 */
813                 return my_info;
814         }
815
816         public string get_my_offers_url () {
817                 return HTTP_BASE_URI + "/lifts/mysinglelifts";
818         }
819
820         public async List<Lift>? get_my_offers () {
821                 var doc = yield get_html_document (get_my_offers_url ());
822                 if (doc == null) {
823                         stderr.printf ("Error: parsing failed\n");
824                         return null;
825                 }
826
827                 var table = search_tag_by_class (doc->children, "table", "list");
828                 if (table == null) {
829                         stderr.printf ("Error: does not contain user table\n");
830                         return null;
831                 }
832
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);
839                         }
840                 }
841
842                 if (table->next != null && table->next->name == "div") {
843                         var text = get_child_text_content (table->next);
844                         if (text != null) {
845                                 print ("\"%s\"\n", text);
846                                 if (text == "Sie haben derzeit keine einmaligen Fahrten eingetragen") {
847                                         print ("NO ENTRIES\n");
848                                 }
849                         }
850                 }
851
852                 return list;
853         }
854
855         Lift? parse_offer_row (Xml.Node *tr) {
856                 var lift = new Lift ();
857
858                 // checkbox
859                 var td = get_next_td (tr->children);
860                 if (td == null)
861                         return null;
862
863                 // action
864                 td = get_next_td (td->next);
865                 if (td == null)
866                         return null;
867                 // FIXME: get uri
868
869                 // type
870                 td = get_next_td (td->next);
871                 if (td == null)
872                         return null;
873                 var text = get_child_text_content (td);
874                 if (text == null)
875                         return null;
876                 // FIXME ==
877                 if (text != "Mitfahrer")
878                         return null;
879
880                 // point of departure
881                 td = get_next_td (td->next);
882                 if (td == null)
883                         return null;
884                 text = get_child_text_content (td);
885                 if (text == null)
886                         return null;
887                 lift.city_from = text;
888
889                 // point of arrival
890                 td = get_next_td (td->next);
891                 if (td == null)
892                         return null;
893                 text = get_child_text_content (td);
894                 if (text == null)
895                         return null;
896                 lift.city_to = text;
897
898                 // date
899                 td = get_next_td (td->next);
900                 if (td == null)
901                         return null;
902                 text = get_child_text_content (td);
903                 if (text == null)
904                         return null;
905                 parse_date (text, out lift.time);
906
907                 // time
908                 td = get_next_td (td->next);
909                 if (td == null)
910                         return null;
911                 text = get_child_text_content (td);
912                 if (text == null)
913                         return null;
914                 parse_time (text, out lift.time);
915
916                 // active?
917                 td = get_next_td (td->next);
918                 if (td == null)
919                         return null;
920                 var a = td->children;
921                 if (a == null || a->name != "a")
922                         return null;
923                 text = a->get_prop ("class");
924                 if (text == "status icon icon_ajax_active")
925                         lift.flags |= LiftFlags.ACTIVE;
926
927                 return lift;
928         }
929
930         Xml.Node* get_next_td (Xml.Node *n) {
931                 while (n != null) {
932                         if (n->name == "td")
933                                 return n;
934                         n = n->next;
935                 }
936                 return null;
937         }
938
939         unowned string get_child_text_content (Xml.Node *n) {
940                 if (n->children != null && n->children->name == "text")
941                         return n->children->content;
942                 else
943                         return null;
944         }
945
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)
949                                 return n;
950                         if (n->children != null) {
951                                 var found = search_tag_by_property (n->children, tag, prop, val);
952                                 if (found != null)
953                                         return found;
954                         }
955                 }
956                 return null;
957         }
958
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);
961         }
962
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);
965         }
966
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);
969         }
970
971         void parse_date (string date, out Time time) {
972                 int year;
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"
978                         return;
979                 var res = date.scanf ("%02d.%02d.%02d", out time.day, out time.month, out year);
980                 time.year = year + 2000;
981         }
982
983         void parse_time (string time, out Time result) {
984                 var res = time.scanf ("%d.%02d Uhr", out result.hour, out result.minute);
985         }
986 }