/* This file is part of Beifahrer. * * Copyright (C) 2010 Philipp Zabel * * Beifahrer is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Beifahrer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Beifahrer. If not, see . */ [Compact] public class City { public int number; public string name; public double latitude; public double longitude; public double north; public double south; public double east; public double west; public City (int _number, string _name) { number = _number; name = _name; latitude = 0.0; longitude = 0.0; } internal double bb_area () { return (north - south) * (east - west); } } public enum LiftFlags { SMOKER = 1, NON_SMOKER = 2, ADAC_MEMBER = 4, WOMEN_ONLY = 8, ACTIVE = 16, } public class Lift : Object { public string city_from; public string city_to; public Time time; public int places; public string price; public LiftFlags flags; public string href; public List city_via; public string name; public string cell; 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 HTTP_BASE_URI = "http://mitfahrclub.adac.de"; const string HTTPS_BASE_URI = "https://mitfahrclub.adac.de"; Curl.EasyHandle curl; List city_list = null; 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); } 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) { _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[]) 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 ("
Die eingegebenen Zugangsdaten konnten nicht gefunden werden. Bitte versuchen Sie es erneut.
")) { 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) return; foreach (unowned City city in city_list) { if (city.north != 0.0 || city.south != 0.0 || city.east != 0.0 || city.west != 0.0) 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); else if (city.latitude != 0.0 || city.longitude != 0.0) list_file.printf ("%d\t%s\t%f\t%f\n", city.number, city.name, city.latitude, city.longitude); else list_file.printf ("%d\t%s\n", city.number, city.name); } } private bool load_city_list () { FileStream list_file = FileStream.open ("/home/user/.cache/beifahrer/city_list", "r"); if (list_file == null) list_file = FileStream.open ("/usr/share/beifahrer/city_list", "r"); if (list_file == null) return false; city_list = new List (); string line = list_file.read_line (); while (line != null) { var split_line = line.split ("\t"); if (split_line.length < 2) continue; int number = split_line[0].to_int (); weak string name = split_line[1]; var city = new City (number, name); if (split_line.length >= 4) { city.latitude = split_line[2].to_double (); city.longitude = split_line[3].to_double (); } if (split_line.length >= 8) { city.north = split_line[4].to_double (); city.south = split_line[5].to_double (); city.east = split_line[6].to_double (); city.west = split_line[7].to_double (); } city_list.append ((owned) city); line = list_file.read_line (); } return true; } public unowned List? get_city_list () { if (city_list != null) return city_list; if (load_city_list ()) return city_list; return null; } public async unowned List? download_city_list () { var doc = yield get_html_document (HTTP_BASE_URI); if (doc == null) { stderr.printf ("Error: parsing failed\n"); return null; } var form = search_tag_by_id (doc->children, "form", "search_national_form"); if (form == null) { 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) { stderr.printf ("Error: does not contain city_from\n"); return null; } city_list = new List (); for (var n = select->children; n != null; n = n->next) { if (n->name == "option" && n->children != null && n->children->name == "text") { int number = n->get_prop ("value").to_int (); // Skip 0 "Alle St.dte" if (number == 0) continue; var city = new City(number, n->children->content); city_list.append ((owned) city); } } // TODO: get coordinates save_city_list (); return city_list; } private int get_city_number (string name) { foreach (unowned City city in city_list) { if (city.name == name) return city.number; } return 0; } public unowned City find_nearest_city (double latitude, double longitude) { unowned City result = null; double min_distance = 0.0; bool in_result = false; foreach (unowned City city in city_list) { double lat = latitude - city.latitude; double lng = longitude - city.longitude; double distance = lat * lat + lng * lng; bool in_city = ((city.south <= latitude <= city.north) && (city.west <= longitude <= city.east)); if ((result == null) || (in_city && !in_result) || (in_city && in_result && distance / city.bb_area () < min_distance / result.bb_area ()) || (!in_city && !in_result && distance < min_distance)) { result = city; min_distance = distance; in_result = in_city; } } return result; } 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) return null; int num_to = get_city_number (city_to); if (num_to == 0) return null; string url = HTTP_BASE_URI + "/mitfahrclub/%s/%s/b.html".printf ( city_from.replace ("/", "_"), city_to ); url += "?type=b&city_from=%d&radius_from=%d&city_to=%d&radius_to=%d".printf ( num_from, radius_from, num_to, radius_to ); url += "&date=date&day=%d&month=%d&year=%d&tolerance=%d&smoking=&avg_speed=&".printf ( date.get_day (), date.get_month (), date.get_year (), tolerance ); return url; } public async List? 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; } var table = search_tag_by_class (doc->children, "table", "list p_15"); if (table == null) { stderr.printf ("Error: does not contain list p_15 table\n"); return null; } var list = new List (); for (var n = table->children; n != null; n = n->next) { if (n->name == "tr") { var lift = parse_lift_row (n->children); if (lift.city_from != null) // Skip the title row list.append ((owned) lift); } } // Error message? var div = table->next; if (div != null && div->get_prop ("class") == "error-message") { if (div->children == null || div->children->content == null || !div->children->content.has_prefix ("Es sind leider noch keine Einträge vorhanden.")) { stderr.printf ("Got an unknown error message!\n"); if (div->children != null && div->children->content != null) stderr.printf ("\"%s\"\n", div->children->content); } } return list; } Lift parse_lift_row (Xml.Node* node) { var lift = new Lift (); int i = 0; for (var n = node; n != null; n = n->next) { if (n->name == "td") { var n2 = n->children; if (n2 != null) { if (n2->name == "a") { var href = n2->get_prop ("href"); if (href != null && lift.href == null) lift.href = href; var n3 = n2->children; while (n3 != null) { if (n3->name == "text") switch (i) { case 0: lift.city_from = n3->content; break; case 1: lift.city_to = n3->content; break; case 2: parse_date (n3->content, out lift.time); break; case 3: parse_time (n3->content.strip (), out lift.time); break; case 4: lift.places = n3->content.to_int (); break; case 5: lift.price = n3->content.replace (" EUR", " €"); break; default: print ("TEXT:%s\n", n3->content); break; } if (n3->name == "span") { string class = n3->get_prop ("class"); if (class == "icon_smoker") lift.flags |= LiftFlags.SMOKER; else if (class == "icon_non_smoker") lift.flags |= LiftFlags.NON_SMOKER; else if (class == "icon_adac") lift.flags |= LiftFlags.ADAC_MEMBER; else if (class == "icon_women") lift.flags |= LiftFlags.WOMEN_ONLY; else if (class != null) print ("SPAN %s\n", class); } n3 = n3->next; } } } i++; } } return lift; } public string get_lift_details_url (Lift lift) { return HTTP_BASE_URI + lift.href; } 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; } var table = search_tag_by_class (doc->children, "table", "lift"); if (table == null) { stderr.printf ("Error: does not contain lift table\n"); return false; } 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; if (text != "Strecke & Infos" && text != " " && !text.has_prefix ("\xc2\xa0") && text != "Datum" && text != "Freie Pl\xc3\xa4tze" && text != "Name" && text != "Handy" && text != "Telefon" && text != "Telefon 2" && text != "E-Mail 1" && text != "Details" && text != "Beschreibung") continue; n2 = n2->next; if (n2 == null) continue; // Skip text between td nodes if (n2->name == "text") n2 = n2->next; if (n2 == null || n2->name != "td" || n2->children == null) continue; if (n2->children->name == "img") { // FIXME: email image lift.email_image_uri = n2->children->get_prop ("src"); continue; } if (n2->children->name == "div" && text == "Beschreibung") { var n3 = n2->children->children; lift.description = ""; while (n3 != null) { if (n3->name == "text") lift.description += n3->content.strip () + "\n"; n3 = n3->next; } continue; } else if (n2->children->name != "text") { continue; } var text1 = n2->children->content.strip (); if (text == "Freie Pl\xc3\xa4tze") lift.places = text1.to_int (); else if (text == "Name") lift.name = text1; else if (text == "Handy") lift.cell = text1; else if (text == "Telefon") lift.phone = text1; else if (text == "Telefon 2") lift.phone2 = text1; else if (text == "E-Mail 1") lift.email = text1; else if (text != "Strecke & Infos" && text != " " && !text.has_prefix ("\xc2\xa0") && text != "Datum" && text != "Details") continue; n2 = n2->next; if (n2 == null) continue; // Skip text between td nodes if (n2->name == "text") n2 = n2->next; if (n2 == null || n2->name != "td" || n2->children == null) continue; if (n2->children->name == "span" && n2->children->get_prop ("class") == "icon_non_smoker") { lift.flags |= LiftFlags.NON_SMOKER; continue; } else if (n2->children->name == "span" && n2->children->get_prop ("class") == "icon_smoker") { lift.flags |= LiftFlags.SMOKER; continue; } else if (n2->children->name != "text") continue; var text2 = n2->children->content.strip (); if (text1 == "von") lift.city_from = text2; else if (text1.has_prefix ("\xc3\xbc")) lift.city_via.append (text2); else if (text1 == "nach") lift.city_to = text2; else if (text1 == "Datum") parse_date (text2, out lift.time); else if (text1 == "Uhrzeit") parse_time (text2, out lift.time); else if (text1 == "Raucher") print ("Raucher: %s\n", text2); else if (text1 == "Fahrpreis") lift.price = text2.replace (" EUR", " €"); else if (text1 == "ADAC-Mitglied" && text2 != "nein") lift.flags |= LiftFlags.ADAC_MEMBER; } } // The paragraph after the table contains the date of last modification var p = table->next; for (n = p->children; n != null; n = n->next) { if (n->name != "text") continue; var s = n->content.strip (); if (s.has_prefix ("Letztmalig aktualisiert am ")) lift.modified = s.offset (27).dup (); // "Do 15.04.2010 20:32" } 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; } } } /* Anrede Herr Titel -- ... */ return my_info; } public string get_my_offers_url () { return HTTP_BASE_URI + "/lifts/mysinglelifts"; } public async List? 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 (); 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) return n; if (n->children != null) { var found = search_tag_by_property (n->children, tag, prop, val); if (found != null) return found; } } return null; } Xml.Node* search_tag_by_id (Xml.Node* node, string tag, string id) requires (node != null) { return search_tag_by_property (node, tag, "id", id); } Xml.Node* search_tag_by_name (Xml.Node* node, string tag, string name) requires (node != null) { return search_tag_by_property (node, tag, "name", name); } 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); } }