SMOKER = 1,
NON_SMOKER = 2,
ADAC_MEMBER = 4,
- WOMEN_ONLY = 8
+ WOMEN_ONLY = 8,
+ ACTIVE = 16,
}
public class Lift : Object {
public string city_from;
public string city_to;
- public string date;
- public string time;
+ public Time time;
public int places;
public string price;
public LiftFlags flags;
public string phone;
public string phone2;
public string email;
+ public string email_image_uri;
public string description;
public string modified;
+
+ public Lift () {
+ time.hour = -1;
+ }
+}
+
+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";
- string response;
- int size;
+ Curl.EasyHandle curl;
List<City> city_list = null;
- static size_t write_memory_cb (void* ptr, size_t size, size_t nmemb, void* data) {
- unowned AdacMitfahrclub self = (AdacMitfahrclub) data;
+ public AdacMitfahrclub () {
+ 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);
+ }
- self.response += ((string) ptr).ndup (size * nmemb);
- self.size += (int) (size * nmemb);
+ 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 size * nmemb;
+ return null;
}
- private Html.Doc* get_html_document (string url) {
- var handle = new Curl.EasyHandle ();
+ 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));
- handle.setopt (Curl.Option.URL, url);
- handle.setopt (Curl.Option.WRITEFUNCTION, write_memory_cb);
- handle.setopt (Curl.Option.WRITEDATA, (void*) this);
+ return size * nmemb;
+ }
- this.response = "";
- this.size = 0;
+ private async Html.Doc* get_html_document (string url) {
+ _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;
+ }
- handle.perform ();
+ yield;
- return Html.Doc.read_memory ((char[]) this.response, this.size,
+ 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)
if (load_city_list ())
return city_list;
- var doc = get_html_document (BASE_URI);
+ return null;
+ }
+
+ public async unowned List<City>? download_city_list () {
+ var doc = yield get_html_document (HTTP_BASE_URI);
if (doc == null) {
- print ("Error: parsing failed");
- print ("%s\n", this.response);
+ stderr.printf ("Error: parsing failed\n");
return null;
}
var form = search_tag_by_id (doc->children, "form", "search_national_form");
if (form == null) {
- print ("Error: does not contain search_national_form");
- print ("%s\n", this.response);
+ stderr.printf ("Error: does not contain search_national_form\n");
return null;
}
var select = search_tag_by_name (form->children, "select", "city_from");
if (select == null) {
- print ("Error: does not contain city_from");
- print ("%s\n", this.response);
+ stderr.printf ("Error: does not contain city_from\n");
return null;
}
return result;
}
- public List<Lift>? get_lift_list (string city_from, string city_to, Date date) {
+ 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 (
+ string url = HTTP_BASE_URI + "/mitfahrclub/%s/%s/b.html".printf (
city_from,
city_to
);
- url += "?type=b&city_from=%d&radius_from=0&city_to=%d&radius_to=0".printf (
+ url += "?type=b&city_from=%d&radius_from=%d&city_to=%d&radius_to=%d".printf (
num_from,
- num_to
+ radius_from,
+ num_to,
+ radius_to
);
- int tolerance = 0;
-
url += "&date=date&day=%d&month=%d&year=%d&tolerance=%d&smoking=&avg_speed=&".printf (
date.get_day (),
date.get_month (),
tolerance
);
- var doc = 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) {
- print ("Error: parsing failed");
- print ("%s\n", this.response);
+ stderr.printf ("Error: parsing failed\n");
return null;
}
var table = search_tag_by_class (doc->children, "table", "list p_15");
if (table == null) {
- print ("Error: does not contain list p_15 table");
- print ("%s\n", this.response);
+ stderr.printf ("Error: does not contain list p_15 table\n");
return null;
}
lift.city_to = n3->content;
break;
case 2:
- lift.date = n3->content;
+ parse_date (n3->content, out lift.time);
break;
case 3:
- lift.time = n3->content;
+ parse_time (n3->content.strip (), out lift.time);
break;
case 4:
lift.places = n3->content.to_int ();
return lift;
}
- public Lift? get_lift_details (string lift_url) {
- var lift = new Lift ();
- lift.href = lift_url;
- if (update_lift_details (lift))
- return lift;
- else
- return null;
+ public string get_lift_details_url (Lift lift) {
+ return HTTP_BASE_URI + lift.href;
}
- public bool update_lift_details (Lift lift) {
- string url = BASE_URI + lift.href;
-
- var doc = 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) {
- print ("Error: parsing failed");
- print ("%s\n", this.response);
+ stderr.printf ("Error: parsing failed\n");
return false;
}
var table = search_tag_by_class (doc->children, "table", "lift");
if (table == null) {
- print ("Error: does not contain lift table");
- print ("%s\n", this.response);
+ stderr.printf ("Error: does not contain lift table\n");
return false;
}
- for (var n = table->children; n != null; n = n->next) {
+ 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" ||
if (n2->children->name == "img") {
// FIXME: email image
- // n2->children->get_prop ("src"))
+ lift.email_image_uri = n2->children->get_prop ("src");
continue;
}
var text1 = n2->children->content.strip ();
- if (text == "Datum")
- lift.date = text1;
- else if (text == "Freie Pl\xc3\xa4tze")
+ if (text == "Freie Pl\xc3\xa4tze")
lift.places = text1.to_int ();
else if (text == "Name")
lift.name = text1;
else if (text1 == "nach")
lift.city_to = text2;
else if (text1 == "Datum")
- lift.date = text2;
+ parse_date (text2, out lift.time);
else if (text1 == "Uhrzeit")
- lift.time = text2;
+ parse_time (text2, out lift.time);
else if (text1 == "Raucher")
print ("Raucher: %s\n", text2);
else if (text1 == "Fahrpreis")
// The paragraph after the table contains the date of last modification
var p = table->next;
- for (var n = p->children; n != null; n = n->next) {
+ for (n = p->children; n != null; n = n->next) {
if (n->name != "text")
continue;
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)
Xml.Node* search_tag_by_class (Xml.Node* node, string tag, string @class) requires (node != null) {
return search_tag_by_property (node, tag, "class", @class);
}
+
+ void parse_date (string date, out Time time) {
+ int year;
+ 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) // "01.02.03"
+ return;
+ var res = date.scanf ("%02d.%02d.%02d", out time.day, out time.month, out year);
+ time.year = year + 2000;
+ }
+
+ void parse_time (string time, out Time result) {
+ var res = time.scanf ("%d.%02d Uhr", out result.hour, out result.minute);
+ }
}