Switch from libsoup to libcurl, add login and my information support
[beifahrer] / src / adac-mitfahrclub.vala
index a9a8ace..b78fe54 100644 (file)
@@ -71,40 +71,201 @@ 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 login (string? _username, string? _password) {
+               username = _username;
+               password = _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 (string? _username, string? _password) {
+               username = _username;
+               password = _password;
+               if (logged_in)
+                       return true;
+               if (login_callback != 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)
@@ -166,7 +327,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;
@@ -253,7 +414,7 @@ public class AdacMitfahrclub {
                        return null;
                }
 
-               string url = BASE_URI + "/mitfahrclub/%s/%s/b.html".printf (
+               string url = HTTP_BASE_URI + "/mitfahrclub/%s/%s/b.html".printf (
                        city_from,
                        city_to
                );
@@ -369,7 +530,7 @@ public class AdacMitfahrclub {
        }
 
        public async bool update_lift_details (Lift lift) {
-                string url = BASE_URI + lift.href;
+                string url = HTTP_BASE_URI + lift.href;
 
                var doc = yield get_html_document (url);
                if (doc == null) {
@@ -512,6 +673,139 @@ public class AdacMitfahrclub {
                return true;
        }
 
+       public async MyInformation get_my_information () {
+                string url = HTTPS_BASE_URI + "/users/view";
+
+               var doc = yield get_html_document (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;
+       }
+
        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)