Beefy Boxes and Bandwidth Generously Provided by pair Networks
Problems? Is your data what you think it is?
 
PerlMonks  

Weather warnings from www.meteoalarm.eu

by walto (Pilgrim)
on May 27, 2010 at 15:10 UTC ( [id://841938]=CUFP: print w/replies, xml ) Need Help??

www.meteoalarm.eu offers data for extreme weather situations in 30 european countries. I wrote this object oriented interface to retrieve data from the website.
Although I did my best to provide valid data there can be no guarantee for the reliability of the data from the module. I wrote the module only for informational purposes and it is not meant to use it for anything critical.
#!/usr/bin/perl # # package Meteoalarm; use strict; use warnings; use Carp; use LWP; use HTML::Entities; use HTML::TreeBuilder; use utf8; binmode STDOUT, ":encoding(UTF-8)"; our $VERSION = "0.05"; sub new { my $class = shift; my $self = {}; my %passed_params = @_; $self->{'user_agent'} = _make_user_agent( $passed_params{'user_agent'} ); bless( $self, $class ); return $self; } sub countries { my $self = shift; my %passed_params = @_; my %type = ( 'all' => 0, 'wind' => 1, 'snow' => 2, 'ice' => 2, 'snow/ice' => 2, 'thunderstorm' => 3, 'fog' => 4, 'extreme high temperature' => 5, 'extreme low temperature' => 6, 'coastal event' => 7, 'forestfire' => 8, 'avalanches' => 9, 'rain' => 10, 'unnamed' => 11, 'flood' => 12, 'rainflood' => 13 ); if ( !$passed_params{type} ) { $passed_params{type} = 0; } elsif ( !$type{ $passed_params{type} } ) { $passed_params{type} = 0; } else { $passed_params{type} = $type{ $passed_params{type} }; } my %day = ( 'today' => 0, 'tomorrow' => 1 ); if ( !$passed_params{day} ) { $passed_params{day} = 0; } elsif ( !$day{ $passed_params{day} } ) { $passed_params{day} = 0; } else { $passed_params{day} = $day{ $passed_params{day} }; } my $url = _make_country_url( $passed_params{day}, $passed_params{type} ); my $content = _fetch_content( $url, $self->{'user_agent'} ); my $country_warnings = _parse_country_warnings($content); return $country_warnings; } sub regions { my ($self) = shift; my %passed_params = @_; my %day = ( 'today' => 0, 'tomorrow' => 1 ); if ( !$passed_params{day} ) { $passed_params{day} = 0; } elsif ( !$day{ $passed_params{day} } ) { $passed_params{day} = 0; } else { $passed_params{day} = $day{ $passed_params{day} }; } my %type = ( 'all' => 0, 'wind' => 1, 'snow' => 2, 'ice' => 2, 'snow/ice' => 2, 'thunderstorm' => 3, 'fog' => 4, 'extreme high temperature' => 5, 'extreme low temperature' => 6, 'coastal event' => 7, 'forestfire' => 8, 'avalanches' => 9, 'rain' => 10, 'unnamed' => 11, 'flood' => 12, 'rainflood' => 13 ); if ( !$passed_params{type} ) { $passed_params{type} = 0; } elsif ( !$type{ $passed_params{type} } ) { $passed_params{type} = 0; } else { $passed_params{type} = $type{ $passed_params{type} }; } croak "Invalid country_code: $passed_params{country_code}" unless $passed_params{country_code}; my $url = 'http://www.meteoalarm.eu/en_UK/' . $passed_params{day} . '/' . $passed_params{type} . '/' . $passed_params{country_code} . '.html'; my $content = _fetch_content( $url, $self->{'user_agent'} ); my $region_warnings = _parse_region_warnings($content); return $region_warnings; } sub details { my $self = shift; my %passed_params = @_; my %country_codes = ( 'AT' => 10, 'BE' => 801, 'BG' => 28, 'CH' => 8, 'CY' => 1, 'CZ' => 14, 'DE' => 808, 'DK' => 8, 'EE' => 805, 'ES' => 831, 'FI' => 813, 'FR' => 94, 'GR' => 16, 'HR' => 8, 'HU' => 7, 'IE' => 804, 'IS' => 11, 'IT' => 20, 'LU' => 2, 'LV' => 804, 'ME' => 3, 'MK' => 6, 'MT' => 1, 'NL' => 807, 'NO' => 814, 'PL' => 802, 'PT' => 26, 'RO' => 42, 'RS' => 11, 'SE' => 813, 'SI' => 5, 'SK' => 16, 'UK' => 16 ); my ( $region, $code ) = $passed_params{region_code} =~ /^([ABCDEFGHILMNPRSU][A-Z])(\d\d\d)/; $code =~ s /^0//; croak "Invalid region_code: $passed_params{region_code}" unless ( $country_codes{$region} and ( $code <= $country_codes{$region} ) ); my $details; my %type = ( 'all' => 0, 'wind' => 1, 'snow' => 2, 'ice' => 2, 'snow/ice' => 2, 'thunderstorm' => 3, 'fog' => 4, 'extreme high temperature' => 5, 'extreme low temperature' => 6, 'coastal event' => 7, 'forestfire' => 8, 'avalanches' => 9, 'rain' => 10, 'unnamed' => 11, 'flood' => 12, 'rainflood' => 13 ); if ( !$passed_params{type} ) { $passed_params{type} = 0; } elsif ( !$type{ $passed_params{type} } ) { $passed_params{type} = 0; } else { $passed_params{type} = $type{ $passed_params{type} }; } my %day = ( 'today' => 0, 'tomorrow' => 1 ); if ( !$passed_params{day} ) { $passed_params{day} = 0; } elsif ( !$day{ $passed_params{day} } ) { $passed_params{day} = 0; } else { $passed_params{day} = $day{ $passed_params{day} }; } my $url = 'http://www.meteoalarm.eu/en_UK/' . $passed_params{day} . '/' . $passed_params{type} . '/' . $passed_params{region_code} . '.html'; my $content = _fetch_content( $url, $self->{'user_agent'} ); $details = _parse_details($content); return $details; } sub codes { my $self = shift; my @codes; my @countries_short; if (@_) { @countries_short = @_; } else { @countries_short = qw(AT BE BG CH CY CZ DE DK EE ES FI FR GR HR HU IE IS IT LU +LV ME MK MT NL NO PL PT RO RS SE SI SK UK); } foreach my $country_short (@countries_short) { my $url = 'http://www.meteoalarm.eu/index2.php?country=' . $country_short . '&day=0&lang='; my $content = _fetch_content( $url, $self->{'user_agent'} ); push @codes, _parse_codes($content); } return @codes; } sub _make_country_url { my ( $day, $type ) = @_; my $url = 'http://www.meteoalarm.eu/en_UK/' . $day . '/' . $type . '/EU-Europe.html'; return $url; } sub _fetch_content { my ( $url, $user_agent ) = @_; my $ua = LWP::UserAgent->new; $ua->agent($user_agent); my $res = $ua->request( HTTP::Request->new( GET => $url ) ); croak " Can't fetch http://meteoalarm.eu: $res->status_line \n" unless ( $res->is_success ); return $res->decoded_content; } sub _parse_country_warnings { my $content = shift; my $p = HTML::TreeBuilder->new_from_content($content); my (%data); my @cells = $p->look_down( _tag => q{td}, class => qr/^col[12]$/ ); for my $cell (@cells) { my @src; my $div = $cell->look_down( _tag => q{div} ); my $id = $div->id; my $alt = $div->attr(q{alt}); $data{$id}{fullname} = $alt; my @weather_events = $div->look_down( _tag => 'span', class => qr{warn awt} ); $data{$id}{warnings} = _parse_weather_events( \@weather_events ); # # get tendency # my $tendency = $div->look_down( _tag => 'div', class => qr{tendenz awt nt l\d} ); if ( $tendency->{class} ) { $tendency->{class} =~ /tendenz awt nt l(\d)/; $data{$id}{tendency} = $1; } } return \%data; } sub _parse_region_warnings { my $content = shift; my $p = HTML::TreeBuilder->new_from_content($content); my (%data); my @cells = $p->look_down( _tag => q{td}, class => qr/^col[12]$/ ); for my $cell (@cells) { my @src; my $div = $cell->look_down( _tag => q{div} ); my $id = $div->id; my @fullname = $div->look_down( _tag => 'span', class => 'area' ) ->content_list; my $fullname = $fullname[0]; $data{$id}{fullname} = $fullname; my @weather_events = $div->look_down( _tag => 'span', class => qr{warn2 awt} ); $data{$id}{warnings} = _parse_weather_events( \@weather_events ); # # get tendency # my $tendency = $div->look_down( _tag => 'div', class => qr{tendenz awt nt l\d} ); if ( $tendency->{class} ) { $tendency->{class} =~ /tendenz awt nt l(\d)/; $data{$id}{tendency} = $1; } } return \%data; } sub _parse_weather_events { my $events = shift; my %weather_to_text = ( # lower case for constistency 1 => 'wind', 2 => 'snow/ice', 3 => 'thunderstorm', 4 => 'fog', 5 => 'extreme high temperature', 6 => 'extreme low temperature', 7 => 'coastal event', 8 => 'forestfire', 9 => 'avalanches', 10 => 'rain', 11 => 'unnamed', 12 => 'flood', 13 => 'rainflood' ); my %literal_warnings; for my $event (@$events) { #print $event->{class}, "\n"; $event->{class} =~ /warn\d* awt l(\d+) t(\d+)/; my $warn_level = $1; my $weather = $2; $literal_warnings{ $weather_to_text{$weather} } = $warn_level; } return \%literal_warnings; } sub _parse_details { my $content = shift; my (%data); my $p = HTML::TreeBuilder->new_from_content( decode_entities $content); $data{fullname} = $p->look_down( _tag => q{h1} )->as_text; if ( $p->look_down( _tag => q{div}, class => q{warnbox awt nt l l1} ) ) { $data{warnings} = 'no warnings'; } else { my @warnboxes = $p->look_down( _tag => q{div}, class => qr/warnbox awt/ ); for my $warnbox (@warnboxes) { my ($as_txt); my @info_divs = $warnbox->look_down( _tag => q{div}, class => q{info} ); $as_txt = $info_divs[0]->as_text; my ( $from, $until ) = $as_txt =~ /valid from (.*) Until (.*)$/; $as_txt = $info_divs[1]->as_text; my ( $warning, $level ) = $as_txt =~ /(.+?)\s+Awareness Level:\s+(.*)/; $warning =~ s/s$//; my $text = $warnbox->look_down( _tag => q{div}, class => q{text} )->as_text; $data{warnings}{ lc $warning } = { #lower case for constistency level => $level, from => $from, until => $until, text => $text, }; } } return \%data; } sub _parse_codes { my $content = shift; my $p = HTML::TreeBuilder->new_from_content($content); my (%data); my @cells = $p->look_down( _tag => 'td', class => 'flags' ); for my $cell (@cells) { my @divs = $cell->look_down( _tag => 'div', class => 'countrys' ); for my $div (@divs) { my $id = $div->id; my $fullname = $div->look_down( _tag => 'span', class => 'area' ) ->as_text; $data{$fullname} = $id; } } return \%data; } sub _make_user_agent { my $ua = shift; $ua = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Fire +fox/25.0' unless ($ua); return $ua; } sub _extract_details_fullname { my $content = shift; my $region; if ( $content =~ /<h1>Weather warnings: (.+?)<\/h1>/ ) { $region = $1; decode_entities($region); if ( $region =~ /.??<.*<\/a>/ ) { $region =~ s/.??<.*<\/a>//; } } else { carp "Can't get region name\n"; } return $region; } 1; __END__ =head1 NAME B<Meteoalarm> - OO Interface for www.meteoalarm.eu =head1 SYNOPSIS This Module gets weather warnings from www.meteoalarm.eu. For further reading of terms and conditions see http://www.meteoalarm. +eu/terms.php?lang=en_UK use Meteoalarm; my $meteo = Meteoalarm->new( 'user_agent' => 'Meteobot 0.001' ); my $countries = $meteo -> countries ('type' => 'all', 'day' => 'today' +); foreach my $country_code (sort keys %{$countries}){ print "Country: $countries->{$country_code}->{'fullname'}\n"; print "Tendency = $countries->{$country_code}->{tendency}\n" if ( +$countries->{$country_code}->{'tendency'}); if (keys %{$countries->{$country_code}->{'warnings'}}){ foreach my $warning (keys %{$countries->{$country_code}->{'warning +s'}}){ print "Event: $warning, severity: $countries->{$country_co +de}->{'warnings'}->{$warning}\n"; } } else {print "No Warnings\n";} } my $regions = $meteo->regions( 'country_code' => 'PT', 'day' => 'today +', 'type' => 'all' ); foreach my $code ( sort keys %{$regions} ) { print "Region : $regions->{$code}->{'fullname'}: region_code = $co +de\n" if ( keys %{ $regions->{$code}->{'warnings'} } ); print "Tendency = $regions->{$code}->{tendency}\n" if ( $regions-> +{$code}->{'tendency'}); foreach my $type ( keys %{ $regions->{$code}->{'warnings'} } ) { print "$type Severity: $regions->{$code}->{'warnings'}->{$type}\n"; } } my $details = $meteo->details( 'region_code' => 'UK010', 'day' => 'tod +ay'); my $name = $details->{'fullname'}; print "$name\n"; if ( $details->{warnings} eq 'no warnings' ) { print $details->{warnings}, "\n"; } else { foreach my $warning ( keys %{ $details->{'warnings'} } ) { print "$warning\n"; foreach my $detail ( keys %{ $details->{'warnings'}->{$warning +} } ) { print "$detail: $details->{'warnings'}->{$warning}->{$deta +il}\n"; } } } my $codes = $meteo->codes('FR'); my @codes = $meteo->codes(); foreach my $code (@codes) { foreach my $region ( sort keys %{$code} ) { print "Region name: $region, region code: $code->{$region}\n"; } } =head1 DESCRIPTION $meteo -> countries returns hashref of warnings for all countries. $meteo -> regions returns hashref of warnings for all regions in a spe +cified country $meteo -> details returns hashref of detailled warnings for a specifie +d region $meteo -> codes returns arrayref of hash of name and region code of a +country =head1 METHODS =head1 new( ) creates a new meteoalarm object =head2 Optional Arguments: new( 'user_agent' => 'Meteobot 0.001'); changes the user agent string =head1 my $country = $meteo -> countries (); =head2 Optional Arguments: 'day' => 'today' || 'tomorrow' if day is not defined, default value is today 'type' => 'all' || 'wind' || 'snow' || 'ice' || 'snow/ice' || +'snow' || 'ice' || 'thunderstorm' || 'fog' || 'extreme high temperature' +|| 'extreme low temperature' || 'coastal event' || 'fores +tfire' || 'avalanches' || 'rain' if type is not defined, default type is all =head1 $regions = $meteo -> regions ('country_code' => 'DE'); country_code is a 2 letter abbreviation =head2 Optional arguments: day=> 'today' || 'tomorrow' if day is not defined, default value is today =head1 $details = $meteo->details ('region_code' => 'ES005'); region_code consits of 2 letters for the country and 3 digits =head2 Optional arguments: day=> 'today' || 'tomorrow' if day is not defined, default value is today =head1 $code = $meteo -> codes (); Returns arrayref of hash for region names and codes for all countries =head2 Optional Arguments $code = $meteo -> codes ('PL'); Countycode for a specific country =cut
Update: With the detailed advice from wfsp I changed the evaluation of the html code from regexp to HTML::Treebuilder. That allows more robust parsing of the data. There is still plenty of space for more improvements (I am not happy with
sub _parse_details{ ...
). I did not want to split the object to sub classes, so I kept the original structure of one object with a couple of methods in subs.

Update2: added wfsp suggestion for sub _parse_details and better utf8 handling

Update3: changed sub _parse_details to process no warnings pages, changed $VERSION = 0.02

Update4: changed URL for in sub _make_country_url to correctly parse countries, changed to $VERSION = 0.03

2014-01-04 Update5: The website http://www.meteoalarm got a makeover. That made adaptations of of code necessary, changed to $VERSION = 0.04

2014-01-06 Update6: New features added, changed to $VERSION = 0.05

Replies are listed 'Best First'.
Re: Weather warnings from www.meteoalarm.eu
by wfsp (Abbot) on May 27, 2010 at 19:39 UTC
    It might be worth considering separating out extracting the data from the HTML. It could make the flow of the logic a bit easier. I would also recommend using a parser rather than regexes which can get a bit tricky on HTML.

    I was unable to find a page on the website that corresponded to your regexes so I have taken a guess at what it might look like. If you could post a link to an actual page you're dealing with we might have more to go on. For instance, this uses HTML::TokeParser::Simple to do a single pass examining every token and extracting data as appropriate (it covers similar ground to the regex in your _extract_details method).

    If the page is well structured it may be more appropriate to consider something like HTML::TreeBuilder which is more powerful and could simplify proceedings greatly.

    #! /usr/bin/perl use strict; use warnings; use Data::Dumper; { package Meteoalarm::Parser; use HTML::TokeParser::Simple; sub new { my $class = shift; my $content = shift; my $p = HTML::TokeParser::Simple->new(string => $content); my $self = { parser => $p, }; bless($self, $class); return $self; } sub parse { my $self = shift; my (%data, $txt); my $t = $self->find_img() or return; $txt = $self->get_div_txt(q{info}); ($data{from}, $data{until}) = $txt =~ /^valid from (.*)Until(.*)$/ +; $txt = $self->get_div_txt(q{info}); ($data{type}, $data{level}) = $txt =~ /^(.*)Awareness Level: (.*)$ +/; $self->{data} = \%data; return 1; } sub find_img{ my $self = shift; my $p = $self->{parser}; while (my $t = $p->get_token){ return $t if $t->is_start_tag(q{img}); } return; } sub get_div_txt{ my $self = shift; my $div_class = shift; my $p = $self->{parser}; my $txt; while (my $t = $p->get_token){ if ( $t->is_start_tag(q{div}) and $t->get_attr(q{class}) and $t->get_attr(q{class}) eq $div_class ){ $p->get_token; $txt = $p->get_phrase; return $txt; } } return; } sub get_data{ my $self = shift; return $self->{data}; } } # script my $content = do{local $/; <DATA>}; my $mp = Meteoalarm::Parser->new($content); while ($mp->parse){ my $data = $mp->get_data; print Dumper $data; } __DATA__ <img src="my.jpeg"> <!-- possible stuff --> <div class="info"> <b>valid from</b> from date 1 <b>Until</b> until date 1 </div> <div class="info"> <b>type 1</b> Awareness Level: <b>awareness level 1</b> </div> <div class="text"> text </div> <!-- possible stuff --> <img src="my_other.jpeg"> <!-- possible stuff --> <div class="info"> <b>valid from</b> from date 2 <b>Until</b> until date 2 </div> <div class="info"> <b>type 2</b> Awareness Level: <b>awareness level 2</b> </div> <div class="text"> text </div> <!-- and so on -->
    $VAR1 = { 'level' => 'awareness level 1', 'until' => ' until date 1', 'from' => 'from date 1 ', 'type' => 'type 1 ' }; $VAR1 = { 'level' => 'awareness level 2', 'until' => ' until date 2', 'from' => 'from date 2 ', 'type' => 'type 2 ' };
    update: added output
      I never got a handle on the HTML::TokeParser module so I try to get the data with regexp. But you are right: this is not to proper way to do it.
      To get the warnings of all countries the script evaluates http://www.meteoalarm.eu/
      I changed your
      sub find_img{ my @images; my $content = shift; my $p = HTML::TokeParser::Simple->new(string => $content); while (my $t = $p->get_token){ push @images, $t if $t->is_start_tag(q{img}); } return \@images; }
      because there are many images on the page. So again I would need some routine (regexp) to filter out unwanted images.
        Ok, from looking at the link we can simplify things enourmously.

        The data we are after are in cells with class col1 or col2. We can loop over those and extract what we need. You will need to tweak as appropriate but hopefully it will give you the idea.

        #! /usr/bin/perl use strict; use warnings; use Data::Dumper; # meteoalarm.html is the source from the website open my $fh, q{<}, q{meteoalarm.html} or die qq{cant open file to read: $!\n}; my $content = do{local $/; <$fh>}; my $mp = Meteoalarm::Parser->new($content); my $data = $mp->parse; print Dumper $data; package Meteoalarm::Parser; use HTML::TreeBuilder; use Data::Dumper; sub new { my $class = shift; my $content = shift; my $p = HTML::TreeBuilder->new_from_content($content); my $self = { parser => $p, }; bless($self, $class); return $self; } sub parse { my $self = shift; my $p = $self->{parser}; my (%data); my @cells = $p->look_down(_tag => q{td}, class => qr/^col[12]$/); for my $cell (@cells){ my $div = $cell->look_down(_tag => q{div}); my $id = $div->id; my $alt = $div->attr(q{alt}); my $img = $div->look_down(_tag => q{img}); my $src = $img?$img->attr(q{src}):q{}; $data{$id}{fullname} = $alt; $data{$id}{warning} = $src; } return \%data; }
        output (extract)
        $VAR1 = { 'UK' => { 'warning' => '', 'fullname' => 'United Kingdom' }, 'CY' => { 'warning' => '', 'fullname' => 'Cyprus' }, 'IE' => { 'warning' => 'Bilder/wf/wf_23.jpg', 'fullname' => 'Ireland' }, 'IS' => { 'warning' => '', 'fullname' => 'Iceland' }, 'NL' => { 'warning' => '', 'fullname' => 'Netherlands' }, 'BE' => { 'warning' => '', 'fullname' => 'Belgium' }, 'AT' => { 'warning' => 'Bilder/wf/wf_23.jpg', 'fullname' => 'Austria' }, };
Re: Weather warnings from www.meteoalarm.eu
by wfsp (Abbot) on May 30, 2010 at 12:53 UTC
    ...I'm not happy with sub _parse_details{ }...
    Give us a link to a page that method will parse and we can have a look.
        Another take on _parse_details
        #! /usr/bin/perl use strict; use warnings; use Data::Dumper; use HTML::TreeBuilder; # meteoalarm.html contains the source of your link open my $fh, q{<}, q{meteoalarm.html} or die qq{cant open file to read: $!\n}; my $content = do{local $/; <$fh>}; my $data = _parse_details($content); print Dumper $data; sub _parse_details { my $content =shift; my (%data); my $p = HTML::TreeBuilder->new_from_content( $content ); $data{fullname} = $p->look_down( _tag => q{h1} )->as_text; my @warnbox_divs = $p->look_down( _tag => q{div}, class => qr/warnbox wb\d/ ); for my $div (@warnbox_divs) { my ($as_txt); my @info_divs = $div->look_down( _tag => q{div}, class => q{info} ); $as_txt = $info_divs[0]->as_text; my ($from, $until) = $as_txt =~ /valid from (.*) Until (.*)$/; $as_txt = $info_divs[1]->as_text; my ($warning, $level) = $as_txt =~ /([^\s]+)\s+Awareness Level:\s+(.*)/; my $text = $div->look_down( _tag => q{div}, class => q{text} )->as_text; $data{warnings}{$warning} = { level => $level, from => $from, until => $until, text => $text, }; } return \%data; }
        Output. I shortened the 'text' value for clarity.
        $VAR1 = { 'warnings' => { 'Wind ' => { 'until' => '30.05.2010 22:00 CET ', 'level' => 'Yellow  ', 'text' => 'Allm㧬ich zunehmender S�wind mit .....', 'from' => '30.05.2010 06:05 CET' }, 'Rain ' => { 'until' => '01.06.2010 10:00 CET ', 'level' => 'Orange  ', 'text' => 'Zeitweise schauerartig verst㱫ter Dauerregen ....', 'from' => '30.05.2010 07:17 CET' }, 'Thunderstorms ' => { 'until' => '30.05.2010 15:00 CET ', 'level' => 'Yellow  ', 'text' => 'Loklae Gewitter. Dabei vereinzelt Sturmb�bis ....', 'from' => '30.05.2010 13:30 CET' } }, 'fullname' => 'Weather warnings: Baden-Württemberg  ' };
        As you can see, you have to be aware you're handling UTF8 (which, because it uses HTML::Entities) is what HTML::TreeBuilder returns). Beware of decoding anything twice. And don't trip up on the non breaking spaces (&nbsp;) that are scrattered librally throughout the source.
Re: Weather warnings from www.meteoalarm.eu
by walto (Pilgrim) on Jan 06, 2014 at 15:47 UTC
    The website http://www.meteoalarm.eu got a makeover and new features like a tendency for weather events and queries for specific weather events were added.
    The updated code reflects to the changes and new features.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: CUFP [id://841938]
Approved by ww
Front-paged by planetscape
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others drinking their drinks and smoking their pipes about the Monastery: (3)
As of 2024-03-29 14:38 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found