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,
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;
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;
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);
}
}