Debian packaging: 0.0.4-1
[beifahrer] / src / adac-mitfahrclub.vala
index b359e05..810a861 100644 (file)
@@ -43,7 +43,8 @@ public enum LiftFlags {
        SMOKER = 1,
        NON_SMOKER = 2,
        ADAC_MEMBER = 4,
-       WOMEN_ONLY = 8
+       WOMEN_ONLY = 8,
+       ACTIVE = 16,
 }
 
 public class Lift : Object {
@@ -62,6 +63,7 @@ public class Lift : Object {
        public string phone;
        public string phone2;
        public string email;
+       public string email_image_uri;
        public string description;
        public string modified;
 
@@ -70,40 +72,203 @@ public class Lift : Object {
        }
 }
 
-public class CallbackMessage : Soup.Message {
-       public SourceFunc callback;
-
-       public CallbackMessage (string url, SourceFunc? _callback) {
-               method = "GET";
-               callback = _callback;
-               set_uri (new Soup.URI (url));
+public class MyInformation {
+       public enum Gender {
+               MALE,
+               FEMALE
        }
+
+       public Gender gender;
+       public string title;
+       public string first_name;
+       public string last_name;
+       public Date birthday;
+       public Date registered_since;
+
+       // Address
+       public string street;
+       public string number;
+       public string zip_code;
+       public string city;
+       public string country;
+
+       // Contact
+       public string phone1;
+       public string phone2;
+       public string phone3;
+       public string cell;
+       public string email1;
+       public string email2;
+
+       public bool smoker;
+       public bool adac_member;
 }
 
 public class AdacMitfahrclub {
-       const string BASE_URI = "http://mitfahrclub.adac.de";
+       const string HTTP_BASE_URI = "http://mitfahrclub.adac.de";
+       const string HTTPS_BASE_URI = "https://mitfahrclub.adac.de";
 
-       Soup.SessionAsync session;
+       Curl.EasyHandle curl;
        List<City> city_list = null;
 
        public AdacMitfahrclub () {
-               session = new Soup.SessionAsync ();
+               curl = new Curl.EasyHandle ();
+               // FIXME: Fremantle SDK doesn't come with certs
+               curl.setopt (Curl.Option.SSL_VERIFYPEER, 0);
+       //      curl.setopt (Curl.Option.VERBOSE, 1);
        }
 
-       private void message_finished (Soup.Session session, Soup.Message message) {
-               Idle.add (((CallbackMessage) message).callback);
+       private string _url = null;
+       private SourceFunc callback = null;
+       void* download_thread () {
+               curl.setopt (Curl.Option.WRITEFUNCTION, write_callback);
+               curl.setopt (Curl.Option.WRITEDATA, this);
+               curl.setopt (Curl.Option.URL, _url);
+               if (aeolus_cookie != null)
+                       curl.setopt (Curl.Option.COOKIE, "MIKINIMEDIA=%s; Quirinus[adacAeolus]=%s;".printf (mikini_cookie, aeolus_cookie));
+               else if (mikini_cookie != null)
+                       curl.setopt (Curl.Option.COOKIE, "MIKINIMEDIA=%s".printf (mikini_cookie));
+               var res = curl.perform ();
+
+               if (callback != null)
+                       Idle.add (callback);
+               callback = null;
+
+               return null;
+       }
+
+       StringBuilder result;
+
+       [CCode (instance_pos = -1)]
+       size_t write_callback (void *buffer, size_t size, size_t nmemb) {
+       //      if (cancellable != null && cancellable.is_cancelled ())
+       //              return 0;
+
+               result.append_len ((string) buffer, (ssize_t) (size * nmemb));
+
+               return size * nmemb;
        }
 
        private async Html.Doc* get_html_document (string url) {
-               var message = new CallbackMessage (url, get_html_document.callback);
-               session.queue_message (message, message_finished);
+               _url = url;
+               callback = get_html_document.callback;
+               result = new StringBuilder ();
+                try {
+                        Thread.create(download_thread, false);
+                } catch (ThreadError e) {
+                        critical ("Failed to create download thread\n");
+                        return null;
+                }
 
                yield;
 
-               return Html.Doc.read_memory ((char[]) message.response_body.data, (int) message.response_body.length,
+               return Html.Doc.read_memory ((char[]) result.str, (int) result.len,
                                             url, null, Html.ParserOption.NOERROR | Html.ParserOption.NOWARNING);
        }
 
+       private string username;
+       private string password;
+       private string mikini_cookie;
+       private string aeolus_cookie;
+
+       public void set_cookie (string value) {
+               aeolus_cookie = value;
+       }
+
+       public void set_credentials (string _username, string _password) {
+               username = _username;
+               password = _password;
+       }
+
+       public void login (string? _username, string? _password) {
+               set_credentials (_username, _password);
+               if (logged_in)
+                       return;
+               if (login_callback != null)
+                       return;
+               login_thread ();
+       }
+
+       public bool logged_in = false;
+       private SourceFunc login_callback = null;
+       public async bool login_async () {
+               if (logged_in)
+                       return true;
+               if (login_callback != null || username == null || password == null)
+                       return false;
+               login_callback = login_async.callback;
+               result = new StringBuilder ();
+                try {
+                        Thread.create(login_thread, false);
+                } catch (ThreadError e) {
+                        critical ("Failed to create login thread\n");
+                        return false;
+                }
+
+               yield;
+               login_callback = null;
+
+               return logged_in;
+       }
+
+       void *login_thread () {
+               result = new StringBuilder ();
+               curl.setopt (Curl.Option.URL, HTTP_BASE_URI);
+               curl.setopt (Curl.Option.WRITEFUNCTION, write_callback);
+               curl.setopt (Curl.Option.WRITEDATA, this);
+               curl.setopt (Curl.Option.COOKIEFILE, "");
+               var res = curl.perform ();
+
+               Curl.SList cookies;
+               curl.getinfo (Curl.Info.COOKIELIST, out cookies);
+               unowned Curl.SList cookie = cookies;
+               while (cookie != null) {
+                       if (cookie.data != null) {
+                               var c = cookie.data.split ("\t");
+                               if (c.length > 5)
+                                       print ("%s=%s\n", c[5], c[6]);
+                                       if (c[5] == "MIKINIMEDIA")
+                                               mikini_cookie = c[6];
+                       }
+                       cookie = cookie.next;
+               }
+
+               result = new StringBuilder ();
+               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);
+               curl.setopt (Curl.Option.POSTFIELDS, postdata);
+               curl.setopt (Curl.Option.URL, HTTPS_BASE_URI + "/users/login/");
+               curl.setopt (Curl.Option.SSL_VERIFYPEER, 0);
+               res = curl.perform ();
+       //      print ("%s\n", result.str);
+
+               cookies = null;
+               curl.getinfo (Curl.Info.COOKIELIST, out cookies);
+               cookie = cookies;
+               while (cookie != null) {
+                       if (cookie.data != null) {
+                               var c = cookie.data.split ("\t");
+                               if (c.length > 5)
+                                       print ("%s=%s\n", c[5], c[6]);
+                                       // "Quirinus[adacAeolus]"
+                                       if (c[5] == "Quirinus[adacAeolus]") {
+                                               aeolus_cookie = c[6];
+                                               logged_in = true;
+                                       }
+                       }
+                       cookie = cookie.next;
+               }
+
+               if (result.str.contains ("<div id=\"flashMessage\" class=\"message\">Die eingegebenen Zugangsdaten konnten nicht gefunden werden. Bitte versuchen Sie es erneut.</div>")) {
+                       print ("LOGIN FAILED\n");
+                       aeolus_cookie = null;
+                       logged_in = false;
+               }
+
+               if (login_callback != null)
+                       Idle.add (login_callback);
+               return null;
+       }
+
        private void save_city_list () {
                FileStream list_file = FileStream.open ("/home/user/.cache/beifahrer/city_list", "w");
                if (list_file == null)
@@ -165,7 +330,7 @@ public class AdacMitfahrclub {
        }
 
        public async unowned List<City>? download_city_list () {
-               var doc = yield get_html_document (BASE_URI);
+               var doc = yield get_html_document (HTTP_BASE_URI);
                if (doc == null) {
                        stderr.printf ("Error: parsing failed\n");
                        return null;
@@ -236,24 +401,20 @@ public class AdacMitfahrclub {
                return result;
        }
 
-       public async List<Lift>? get_lift_list (string city_from, int radius_from, string city_to, int radius_to, Date date, int tolerance = 0) {
+       public string? get_lift_list_url (string city_from, int radius_from, string city_to, int radius_to, Date date, int tolerance = 0) {
                if (city_list == null)
                        get_city_list ();
 
                int num_from = get_city_number (city_from);
-               if (num_from == 0) {
-                       stderr.printf ("Unknown city: %s\n", city_to);
+               if (num_from == 0)
                        return null;
-               }
 
                int num_to = get_city_number (city_to);
-               if (num_to == 0) {
-                       stderr.printf ("Unknown city: %s\n", city_to);
+               if (num_to == 0)
                        return null;
-               }
 
-               string url = BASE_URI + "/mitfahrclub/%s/%s/b.html".printf (
-                       city_from,
+               string url = HTTP_BASE_URI + "/mitfahrclub/%s/%s/b.html".printf (
+                       city_from.replace ("/", "_"),
                        city_to
                );
 
@@ -271,7 +432,11 @@ public class AdacMitfahrclub {
                        tolerance
                );
 
-               var doc = yield get_html_document (url);
+               return url;
+       }
+
+       public async List<Lift>? get_lift_list (string city_from, int radius_from, string city_to, int radius_to, Date date, int tolerance = 0) {
+               var doc = yield get_html_document (get_lift_list_url (city_from, radius_from, city_to, radius_to, date, tolerance));
                if (doc == null) {
                        stderr.printf ("Error: parsing failed\n");
                        return null;
@@ -337,7 +502,7 @@ public class AdacMitfahrclub {
                                                                        lift.places = n3->content.to_int ();
                                                                        break;
                                                                case 5:
-                                                                       lift.price = n3->content;
+                                                                       lift.price = n3->content.replace (" EUR", " €");
                                                                        break;
                                                                default:
                                                                        print ("TEXT:%s\n", n3->content);
@@ -367,10 +532,12 @@ public class AdacMitfahrclub {
                return lift;
        }
 
-       public async bool update_lift_details (Lift lift) {
-                string url = BASE_URI + lift.href;
+       public string get_lift_details_url (Lift lift) {
+               return HTTP_BASE_URI + lift.href;
+       }
 
-               var doc = yield get_html_document (url);
+       public async bool update_lift_details (Lift lift) {
+               var doc = yield get_html_document (get_lift_details_url (lift));
                if (doc == null) {
                        stderr.printf ("Error: parsing failed\n");
                        return false;
@@ -417,7 +584,7 @@ public class AdacMitfahrclub {
 
                                if (n2->children->name == "img") {
                                        // FIXME: email image
-                                       // n2->children->get_prop ("src"))
+                                        lift.email_image_uri = n2->children->get_prop ("src");
                                        continue;
                                }
 
@@ -491,7 +658,7 @@ public class AdacMitfahrclub {
                                else if (text1 == "Raucher")
                                        print ("Raucher: %s\n", text2);
                                else if (text1 == "Fahrpreis")
-                                       lift.price = text2;
+                                       lift.price = text2.replace (" EUR", " €");
                                else if (text1 == "ADAC-Mitglied" && text2 != "nein")
                                        lift.flags |= LiftFlags.ADAC_MEMBER;
                        }
@@ -511,6 +678,271 @@ public class AdacMitfahrclub {
                return true;
        }
 
+       public string get_my_information_url () {
+                return HTTPS_BASE_URI + "/users/view";
+       }
+
+       public async MyInformation get_my_information () {
+               var doc = yield get_html_document (get_my_information_url ());
+               if (doc == null) {
+                       stderr.printf ("Error: parsing failed\n");
+                       return null;
+               }
+
+               var table = search_tag_by_class (doc->children, "table", "user");
+               if (table == null) {
+                       stderr.printf ("Error: does not contain user table\n");
+                       return null;
+               }
+
+               var my_info = new MyInformation ();
+
+               Xml.Node* n;
+               for (n = table->children; n != null; n = n->next) {
+                       if (n->name == "tr") {
+                               var n2 = n->children;
+                               if (n2 == null || n2->name != "td" ||
+                                   n2->children == null || n2->children->name != "text")
+                                       continue;
+
+                               string text = n2->children->content;
+
+                               n2 = n2->next;
+                               if (n2 != null && n2->name == "text")
+                                       n2 = n2->next;
+                               if (n2 == null || n2->name != "td" ||
+                                   n2->children == null || n2->children->name != "text")
+                                       continue;
+
+                               string content = n2->children->content;
+
+                               switch (text) {
+                               case "Anrede":
+                                       my_info.gender = (content == "Herr") ? MyInformation.Gender.MALE : MyInformation.Gender.FEMALE;
+                                       continue;
+                               case "Titel":
+                                       my_info.title = content;
+                                       continue;
+                               case "Vorname":
+                                       my_info.first_name = content;
+                                       continue;
+                               case "Name":
+                                       my_info.last_name = content;
+                                       continue;
+                               case "Geburtsdatum":
+                       //              my_info.birthday = ...
+                                       continue;
+                               case "registriert seit":
+                       //              my_info.registered_since = content;
+                                       continue;
+                       //      default:
+                       //              print ("\t%s=%s\n", text, content);
+                       //              break;
+                               }
+
+                               text = content;
+
+                               n2 = n2->next;
+                               if (n2 != null && n2->name == "text")
+                                       n2 = n2->next;
+                               if (n2 == null || n2->name != "td")
+                                       continue;
+
+                               if (n2->children != null && n2->children->name != "text")
+                                       content = n2->children->content;
+                               else
+                                       content = "";
+
+                               switch (text) {
+                               case "Straße, Nr.":
+                                       my_info.street = content;
+                                       my_info.number = "";
+                                       continue;
+                               case "PLZ, Ort":
+                                       my_info.zip_code = "";
+                                       my_info.city = content;
+                                       continue;
+                               case "Land":
+                                       my_info.country = content;
+                                       continue;
+                               case "Telefon 1":
+                                       my_info.phone1 = content;
+                                       continue;
+                               case "Telefon 2":
+                                       my_info.phone2 = content;
+                                       continue;
+                               case "Telefon 3":
+                                       my_info.phone3 = content;
+                                       continue;
+                               case "Handy":
+                                       my_info.cell = content;
+                                       continue;
+                               case "Email 1":
+                                       my_info.email1 = content;
+                                       continue;
+                               case "Email 2":
+                                       my_info.email2 = content;
+                                       continue;
+                               case "Raucher":
+                                       // FIXME
+                                       my_info.smoker = false;
+                                       continue;
+                               case "ADAC-Mitglied":
+                                       // FIXME
+                                       my_info.adac_member = false;
+                                       continue;
+                       //      default:
+                       //              print ("\"%s\"=\"%s\"\n", text, content);
+                       //              break;
+                               }
+                       }
+               }
+
+/*
+                       <tr class="head top">
+                               <td width="150" class="label">Anrede</td>
+                               <td width="400">Herr</td>
+                       </tr>
+                       <tr class="head">
+                               <td class="label">Titel</td>
+
+                               <td>--</td>
+                       </tr>
+                       ...
+*/
+               return my_info;
+       }
+
+       public string get_my_offers_url () {
+               return HTTP_BASE_URI + "/lifts/mysinglelifts";
+       }
+
+       public async List<Lift>? get_my_offers () {
+               var doc = yield get_html_document (get_my_offers_url ());
+               if (doc == null) {
+                       stderr.printf ("Error: parsing failed\n");
+                       return null;
+               }
+
+               var table = search_tag_by_class (doc->children, "table", "list");
+               if (table == null) {
+                       stderr.printf ("Error: does not contain user table\n");
+                       return null;
+               }
+
+               var list = new List<Lift> ();
+               for (var n = table->children; n != null; n = n->next) {
+                       if (n->name == "tr") {
+                               var lift = parse_offer_row (n);
+                               if (lift != null) // Skip the title row
+                                       list.append ((owned) lift);
+                       }
+               }
+
+               if (table->next != null && table->next->name == "div") {
+                       var text = get_child_text_content (table->next);
+                       if (text != null) {
+                               print ("\"%s\"\n", text);
+                               if (text == "Sie haben derzeit keine einmaligen Fahrten eingetragen") {
+                                       print ("NO ENTRIES\n");
+                               }
+                       }
+               }
+
+               return list;
+       }
+
+       Lift? parse_offer_row (Xml.Node *tr) {
+               var lift = new Lift ();
+
+               // checkbox
+               var td = get_next_td (tr->children);
+               if (td == null)
+                       return null;
+
+               // action
+               td = get_next_td (td->next);
+               if (td == null)
+                       return null;
+               // FIXME: get uri
+
+               // type
+               td = get_next_td (td->next);
+               if (td == null)
+                       return null;
+               var text = get_child_text_content (td);
+               if (text == null)
+                       return null;
+               // FIXME ==
+               if (text != "Mitfahrer")
+                       return null;
+
+               // point of departure
+               td = get_next_td (td->next);
+               if (td == null)
+                       return null;
+               text = get_child_text_content (td);
+               if (text == null)
+                       return null;
+               lift.city_from = text;
+
+               // point of arrival
+               td = get_next_td (td->next);
+               if (td == null)
+                       return null;
+               text = get_child_text_content (td);
+               if (text == null)
+                       return null;
+               lift.city_to = text;
+
+               // date
+               td = get_next_td (td->next);
+               if (td == null)
+                       return null;
+               text = get_child_text_content (td);
+               if (text == null)
+                       return null;
+               parse_date (text, out lift.time);
+
+               // time
+               td = get_next_td (td->next);
+               if (td == null)
+                       return null;
+               text = get_child_text_content (td);
+               if (text == null)
+                       return null;
+               parse_time (text, out lift.time);
+
+               // active?
+               td = get_next_td (td->next);
+               if (td == null)
+                       return null;
+               var a = td->children;
+               if (a == null || a->name != "a")
+                       return null;
+               text = a->get_prop ("class");
+               if (text == "status icon icon_ajax_active")
+                       lift.flags |= LiftFlags.ACTIVE;
+
+               return lift;
+       }
+
+       Xml.Node* get_next_td (Xml.Node *n) {
+               while (n != null) {
+                       if (n->name == "td")
+                               return n;
+                       n = n->next;
+               }
+               return null;
+       }
+
+       unowned string get_child_text_content (Xml.Node *n) {
+               if (n->children != null && n->children->name == "text")
+                       return n->children->content;
+               else
+                       return null;
+       }
+
        Xml.Node* search_tag_by_property (Xml.Node* node, string tag, string prop, string val) requires (node != null) {
                for (var n = node; n != null; n = n->next) {
                        if (n->name == tag && n->get_prop (prop) == val)
@@ -538,9 +970,11 @@ public class AdacMitfahrclub {
 
        void parse_date (string date, out Time time) {
                int year;
-               if (date.length == 11)
+               if (date.length == 12)          // "Mo, 01.02.03"
+                       date = date.offset (4);
+               else if (date.length == 11)     // "Mo 01.02.03"
                        date = date.offset (3);
-               if (date.length != 8)
+               if (date.length != 8)           // "01.02.03"
                        return;
                var res = date.scanf ("%02d.%02d.%02d", out time.day, out time.month, out year);
                time.year = year + 2000;