以news的方式看phpbb的討論,
用perl script把phpbb模擬成news主機,
目前只提供"READ",還不行"POST",
感謝 Simon 熱情的分享!
下載修改設定後直接執行就可以,
然後最好用粉陽春的news reader去看文章,
我用Outook Express 5試也行,
中文部份只要變更Outlook Express的編碼,
主題及內文都可以正常顯示...
假如想把phpBB跟netnews作結合,
可以參考這個東東去修改:
http://stitt.org/usenetgrab/不然就要改vbb那個news gateway的模組了!
#!/usr/bin/perl -w -T
#
# Usenet hack for phpbb and derivatives
#
# This script makes your phpbb forums available to people using a standard
# usenet news reader like rtin, or xnews.
#
# There are still problems with outlook express, and mozilla, although
# these _almost_ work as expected.
#
# This script is free. You are welcome to do whatever you want with it as
# long as you don't try and sell it or claim it as your own.
# The copyright is retained by the author.
#
# Author:
# Singularo (simon@iseek.ws)
#
# CHANGELOG
# 16/01/02 - Initial Release
# 17/01/02 - Added phpbb2 support
# 18/01/02 - Fixed so it works with Outlook Express
# Added "taint" flag to perl command line
# Silently ignore blank commands
#
package MyNews;
use strict;
use vars qw(@ISA);
use NetServer::Generic;
use Net::Server::Fork;
use DBI;
use Mysql;
my $use_dbi = 1;
my $use_net_server = 1;
@ISA = qw(Net::Server::Fork);
my $posting_allowed = 1; # Change to 0 to stop posting
my $debug = 1; # Turn on debuging
my $timeout = (20 * 60); # Connection inactivity timeout - 20 mins
my $allow_user_kill = 0; # Allow a user to kill the server
###########################################################################
# Performance options
# -------------------------------------------------------------------------
# Max Number of posts to select at once.
# Since this was developed on a P200, and running a mysql select for 27000
# records eats up all my memory, and the news reader hangs up, this reduces
# the number of posts fetched at once.
# On a big machine, you can probably make it bigger. Check the memory
# usage on news.pl with ps/top after you make a change.
#
# I think its caused by mysql sending the data back to the perl script, and
# of course, 27000+records is big.
#
my $granularity = 2000;
#
# Put references (threading) into posts/xover
# Requires and extra query for each article
# Try with it on at first, set it to 0 if speed is slow
#
my $do_xref = 1;
#
# Fake the size of articles in xover (reduces the query result size hugely)
# Try with it off at first, set it to 1 if speed is slow
#
my $fake_size = 0;
my $db = ""; # database name
my $db_host = "localhost"; # hostnmae
my $db_username = "root"; # username
my $db_password = ""; # password
#
# News mode - 1 = phpbb, 2 = phpbb2
my $news_mode = 1;
my $sitename = ""; # your site
my $fullsitename = ""; # full site name
my $localip = ""; # your ip
my $timezone = "-600"; # your timezone
my $server_port = 1190;
# You should not have to modify anything below here.
my $dbh;
my $curr_group;
my $curr_group_id;
my $curr_article;
my $curr_group_arts;
my $sep = "\.";
my %username_cache = ();
my $server_cb;
if ($use_net_server) {
$server_cb = sub {
my $self = $_[0];
my $cmd;
$dbh = db_connect();
greeting();
while (defined (my $tmp = <STDIN>)) {
$tmp =~ s/\r?\n$//;
$cmd = $tmp;
next if ($cmd =~ /^$/i);
console("$cmd") if ($debug > 0);
nntp_command($self, $cmd);
};
db_disconnect();
};
my (%config) = ("port" => $server_port,
"callback" => $server_cb,
"hostname" => "",
"mode" => "forking",
"timeout" => $timeout,
);
my ($server) = new NetServer::Generic(%config);
print "Starting server using NetServer::Generic\n";
$server->run();
} else {
# See Net::Server manpage for explanation of the parameters
MyNews->run(
#log_level => '4',
#log_file => '/var/log/news',
port => $server_port,
#host => '*',
#user => 'nobody',
#group => 'nobody',
#background => 0,
);
print "Starting server using NetServer::Generic\n";
}
exit 0;
sub process_request {
my $self = $_[0];
my $cmd;
$dbh = db_connect();
greeting();
eval {
local $SIG{ALRM} = sub { console("Timed Out!\n"); die; };
my $previous_alarm = alarm($timeout);
while (<STDIN>) {
s/\r?\n$//;
$cmd = $_;
next if ($cmd =~ /^$/i);
console("$cmd") if ($debug > 0);
nntp_command($self, $cmd);
alarm($timeout);
}
alarm($previous_alarm);
};
db_disconnect();
}
sub nntp_command {
my $self = $_[0];
my $cmd = $_[1];
# Process commands entered by the user
if ($cmd =~ /^(?:quit|exit|bye)/i) {
console("QUIT/EXIT/BYE Processing") if ($debug > 5);
reply_msg("205", "bye");
db_disconnect();
exit 0;
return;
}
if ($cmd =~ /^kill/i) {
console("KILL Processing") if ($debug > 5);
reply_msg("205", "bye");
db_disconnect();
if ($allow_user_kill) {
$self->server_close();
}
return;
}
if ($cmd =~ /^help/i) {
console("HELP Processing") if ($debug > 5);
reply_msg("100", "In your dreams");
return;
}
if ($cmd =~ /^mode reader/i) {
console("MODE READER Processing") if ($debug > 5);
greeting();
return;
}
# Select and list a particular group
if ($cmd =~ /^listgroup/i) {
$cmd =~ s/listgroup//i;
$cmd =~ s/\s+//g;
console("LISTGROUP Processing") if ($debug > 5);
my $group = $cmd;
$group =~ s/\_/ /g;
my $query = ("select forum_posts, forum_id " .
"from phpbb_forums " .
"where forum_name = '$group'");
my $query_results = db_fetch($query);
if (db_rows($query_results) > 0) {
($curr_group_arts, $curr_group_id) =
$query_results->fetchrow_array();
reply_msg("211", "list of article numbers follows");
} else {
reply_msg("411", "no such news group");
}
$curr_group = $group;
$curr_article = 1;
$query = ("select post_id " .
"from phpbb_posts " .
"where forum_id = $curr_group_id " .
"order by post_id");
$query_results = db_fetch($query);
while (my ($post_id) = $query_results->fetchrow_array()) {
reply_msg("", "$post_id");
}
reply_msg("", ".");
return;
}
# List available newsgroups
if ($cmd =~ /^list/i) {
$cmd =~ s/list\s//i;
$cmd =~ s/list//i;
console("LIST Processing") if ($debug > 5);
my $output_descriptions;
if ($cmd =~ /^overview.fmt/i) {
$cmd =~ s/overview.fmt//i;
reply_msg("215", "information follows");
reply_msg("", "Subject:");
reply_msg("", "From:");
reply_msg("", "Date:");
reply_msg("", "Message-ID:");
reply_msg("", "References:");
reply_msg("", "Bytes:");
reply_msg("", "Lines:");
reply_msg("", "Xref:Full");
reply_msg("", ".");
return;
}
if ($cmd =~ /^newsgroups/i) {
$cmd =~ s/newsgroups\s//i;
$output_descriptions = 1;
}
if ($cmd =~ /^extensions/i) {
reply_msg("202", "Extension list follows");
reply_msg("", "XOVER");
reply_msg("", "LISTGROUP");
reply_msg("", ".");
return;
}
$cmd =~ s/active\s//i;
reply_msg("215", "list follows");
my $query = ("select forum_name, forum_desc, forum_posts " .
"from phpbb_forums");
my ($match) = $cmd;
if ($match) {
$query .= (" where forum_name like '$match%'");
}
my $query_results = db_fetch($query);
while (my ($group, $group_desc, $number_arts) =
$query_results->fetchrow_array()) {
$group =~ s/ /\_/g;
if ($output_descriptions) {
reply_msg("", "$group, $group_desc");
} else {
reply_msg("", "$group $number_arts 1 y");
}
}
reply_msg("", ".");
return;
}
# List new newsgroups - cant do with phpbb
if ($cmd =~ /^newgroups/i) {
console("NEWGROUPS Processing") if ($debug > 5);
reply_msg("231", "list of new newsgroups follows");
reply_msg("", ".");
return;
}
# List of new messages
if ($cmd =~ /^newnews/i) {
$cmd =~ s/newnews\s//i;
$cmd =~ s/newnews//i;
console("NEWNEWS Processing") if ($debug > 5);
my ($groups, $tdate, $ttime, $extra1, $extra2) = split(/ /, $cmd);
my ($year) = substr($tdate, 0, 2);
my ($month) = substr($tdate, 2, 2);
my ($day) = substr($tdate, 4, 2);
my ($hour) = substr($ttime, 0, 2);
my ($min) = substr($ttime, 2, 2);
my ($sec) = substr($ttime, 4, 2);
my $search_date_time = sprintf("%s-%s-%s %s:%s:%s",
$year, $month, $day,
$hour, $min, $sec);
my $query = ("select post_id, topic_id, forum_id, poster_id " .
"from phpbb_posts ");
if ($news_mode == 1) {
$query .= "where post_time > '$search_date_time' ";
} elsif ($news_mode == 2) {
$query .= "where post_time > unix_timestamp($search_date_time) ";
}
$query .= ("order by post_id");
my $query_results = db_fetch($query);
if (db_rows($query_results) > 0) {
while (my ($post_id, $topic_id, $forum_id, $poster_id) =
$query_results->fetchrow_array()) {
my $article_id =
art_id_encode($post_id, $topic_id, $forum_id, $poster_id);
reply_msg("", "<$article_id>");
}
}
reply_msg("", ".");
return;
}
# Select a particular newsgroup
if ($cmd =~ /^group/i) {
$cmd =~ s/group//i;
$cmd =~ s/^\s+//g;
$cmd =~ s/\_/ /g;
console("GROUP Processing") if ($debug > 5);
my $group = $cmd;
my $query = ("select forum_posts, forum_id " .
"from phpbb_forums " .
"where forum_name = '$group'");
my $query_results = db_fetch($query);
if (db_rows($query_results) > 0) {
($curr_group_arts, $curr_group_id) =
$query_results->fetchrow_array();
$query = ("select post_id " .
"from phpbb_posts " .
"where forum_id = $curr_group_id " .
"order by post_id desc " .
"limit 1");
$query_results = db_fetch($query);
my ($group_max_art_no) = $query_results->fetchrow_array();
reply_msg("211", "$curr_group_arts 1 $group_max_art_no $group");
} else {
reply_msg("411", "no such news group");
}
$curr_group = $group;
$curr_article = 1;
return;
}
if ($cmd =~ /^next/i or $cmd =~ /^last/i) {
console("NEXT/LAST Processing") if ($debug > 5);
if (!$curr_group) {
reply_msg("412", "no newsgroup selected");
}
if (!$curr_article) {
reply_msg("420", "no current article has been selected");
}
if ((($curr_article < $curr_group_arts) && $cmd =~ /^next/i)
or (($curr_article > 1) && $cmd =~ /^last/i)) {
my $query;
if ($cmd =~ /^next/i) {
$query = ("select post_id, topic_id, forum_id, poster_id " .
"from phpbb_posts " .
"where forum_id = $curr_group_id " .
"and post_id > $curr_article " .
"order by post_id " .
"limit 1");
} else {
$query = ("select post_id, topic_id, forum_id, poster_id " .
"from phpbb_posts " .
"where forum_id = $curr_group_id " .
"and post_id < $curr_article " .
"order by post_id desc " .
"limit 1");
}
my $query_results = db_fetch($query);
if (db_rows($query_results) > 0) {
my ($post_id, $topic_id, $forum_id, $poster_id) =
$query_results->fetchrow_array();
my $article_id =
art_id_encode($post_id, $topic_id, $forum_id, $poster_id);
my $article_number = $post_id;
$curr_article = $article_number;
reply_msg("233",
"$article_number <$article_id> " .
"request text seperately");
} else {
reply_msg("421", "no $cmd article in this group");
}
} else {
reply_msg("421", "no $cmd article in this group");
}
return;
}
if (($cmd =~ /^article/i) or
($cmd =~ /^body/i) or
($cmd =~ /^head/i) or
($cmd =~ /^stat/i)) {
console("ARTICLE Processing") if ($debug > 5);
my $mode = 0;
$cmd =~ s/article\s//i;
if ($cmd =~ /^body/i) {
console("BODY Only Processing") if ($debug > 5);
$cmd =~ s/body\s//i;
$mode = 1;
}
if ($cmd =~ /^head/i) {
console("HEAD Only Processing") if ($debug > 5);
$cmd =~ s/head\s//i;
$mode = 2;
}
if ($cmd =~ /^stat/i) {
console("STAT Only Processing") if ($debug > 5);
$cmd =~ s/stat\s//i;
$mode = 3;
}
my $article = $cmd;
my ($post_id, $topic_id, $forum_id, $poster_id);
if (!$forum_id) {
$forum_id = $curr_group_id;
}
if ($article =~ /\@/) {
# Remove square brackets
$article =~ s/\<//g;
$article =~ s/\>//g;
# We've been given an article id
# Convert it to an article, forum, etc number
($post_id, $topic_id, $forum_id, $poster_id) =
art_id_decode($article);
#$curr_group_id = $forum_id;
$article = $post_id;
}
# Build the query, taking into account differing requirements
my $query = ("select p.post_id, p.topic_id, p.forum_id, " .
"p.poster_id, t.post_text, s.topic_title, ");
if ($news_mode == 1) {
$query .= "date_format(p.post_time, '%a, %d %b %Y %T') ";
} elsif ($news_mode == 2) {
$query .= "from_unixtime(p.post_time, '%a, %d %b %Y %T') ";
}
$query .= ("from phpbb_posts as p, phpbb_posts_text as t, " .
"phpbb_topics as s " .
"where p.post_id = t.post_id " .
"and p.topic_id = s.topic_id " .
"and p.forum_id = $forum_id " .
"and p.post_id >= $article " .
"order by p.post_id " .
"limit 1");
my $query_results = db_fetch($query);
if (db_rows($query_results) > 0) {
my ($post_id, $topic_id, $forum_id, $poster_id,
$post_text, $subject, $post_time) =
$query_results->fetchrow_array();
my ($user_email, $username) =
split(/:/, get_userdetails($poster_id));
my $article_id =
art_id_encode($post_id, $topic_id, $forum_id, $poster_id);
my $article_number = $post_id;
# $curr_article = $article_number;
if ($mode == 3) {
reply_msg("223",
"$article_number <$article_id> " .
"article exists");
return;
}
my $posted_text = remove_bb_code($post_text);
my $posted_text_len = length($posted_text);
my $posted_text_lines = ($posted_text =~ s/\n/\n/g);
if (!$posted_text_lines) {
$posted_text_lines = 1;
}
if ($mode != 1) {
reply_msg("220",
"$article_number <$article_id> " .
"article retrieved - head and body follow");
reply_msg("", "Path: $fullsitename!$sitename");
reply_msg("", "From: $user_email ($username)");
reply_msg("", "Newsgroups: $curr_group");
reply_msg("", "Followup-To: $curr_group");
reply_msg("", "Subject: $subject");
reply_msg("", "X-Newsreader: phpbb");
reply_msg("", "X-PhpBB-extra: ");
reply_msg("", "Date: $post_time $timezone");
reply_msg("", "Organization: $sitename");
reply_msg("", "Message-ID: <$article_id>");
reply_msg("", "NNTP-Posting-Host: $localip");
reply_msg("", "Content-Type: text/plain; charset=ISO-8859-1");
reply_msg("", "Content-Transfer-Encoding: 8bit");
reply_msg("", "NNTP-Posting-Date: $post_time GMT");
if ($do_xref) {
# Get other references and put them in
$query = ("select post_id, topic_id, " .
"forum_id, poster_id " .
"from phpbb_posts " .
"where forum_id = $forum_id " .
"and topic_id = $topic_id " .
"and post_id < $post_id " .
"order by post_id " .
"limit 1");
my $query_results2 = db_fetch($query);
if (db_rows($query_results2) > 0) {
my ($post_id, $topic_id, $forum_id, $poster_id) =
$query_results2->fetchrow_array();
my $article_id2 =
art_id_encode($post_id, $topic_id, $forum_id,
$poster_id);
reply_msg("", "References: <$article_id2>");
}
}
reply_msg("", "Bytes: " . $posted_text_len);
reply_msg("", "Lines: " . ($posted_text_lines + 1));
reply_msg("", "Xref: $fullsitename $curr_group:$article_number");
if ($mode == 2) { # Head mode
reply_msg("", ".");
return;
}
reply_msg("", "");
} else {
reply_msg("222",
"$article_number <$article_id> " .
"article retrieved - body follows");
}
chomp $posted_text;
reply_msg("", $posted_text);
reply_msg("", ".");
} else {
reply_msg("430", "no such article found");
}
return;
}
if ($cmd =~ /^xover/i) {
$cmd =~ s/xover\s//i;
$cmd =~ s/xover//i;
console("XOVER Processing") if ($debug > 5);
my $stop = 0;
my $start = 0;
if (!$curr_group_id) {
reply_msg("412", "no newsgroup selected");
return;
}
reply_msg("224", "XOVER follows");
if ($cmd =~ /-/) {
($start, $stop) = split(/-/, $cmd);
my $i = $start;
my $j = $stop;
my $stopstr;
if (($j - $i) > $granularity) {
while ($i < $stop) {
$j = $i + $granularity - 1; # 1 less than the granularity
if ($j > $stop) {
$j = $stop;
}
$stopstr = "and p.post_id <= $j ";
send_xover_data($i, $stopstr);
$i += $granularity;
}
} else {
$stopstr = "and p.post_id <= $stop ";
send_xover_data($start, $stopstr);
}
} elsif ($cmd) {
$start = $cmd;
send_xover_data($start, "");
} else {
$start = 1;
send_xover_data($start, "");
}
reply_msg("", ".");
return;
}
console("Command: $cmd not recognised");
reply_msg("500", "unimplemented");
};
sub send_xover_data {
my $start = $_[0];
my $stop = $_[1];
my $query = ("select p.post_id, p.topic_id, p.forum_id, " .
"p.poster_id, ");
if (!$fake_size) {
$query .= "t.post_text, ";
} else {
$query .= "\"\", ";
}
$query .= "s.topic_title, ";
if ($news_mode == 1) {
$query .= "date_format(p.post_time, '%a, %d %b %Y %T') ";
} elsif ($news_mode == 2) {
$query .= "from_unixtime(p.post_time, '%a, %d %b %Y %T') ";
}
$query .= ("from phpbb_posts as p, phpbb_posts_text as t, " .
"phpbb_topics as s " .
"where p.post_id = t.post_id " .
"and p.topic_id = s.topic_id " .
"and p.forum_id = $curr_group_id " .
"and p.post_id >= $start " .
$stop .
"order by p.post_id");
console("Query: $query") if ($debug > 1);
my $query_results = db_fetch($query);
while (my ($post_id, $topic_id, $forum_id, $poster_id,
$post_text, $subject, $post_time) =
$query_results->fetchrow_array()) {
my $posted_text;
my $posted_text_len;
my $posted_text_lines;
my ($user_email, $username) =
split(/:/, get_userdetails($poster_id));
my $article_id =
art_id_encode($post_id, $topic_id, $forum_id, $poster_id);
my $article_id2;
my $article_number = $post_id;
if (!$fake_size) {
$posted_text = remove_bb_code($post_text);
$posted_text_len = length($posted_text);
$posted_text_lines = ($posted_text =~ s/\n/\n/g);
if (!$posted_text_lines) {
$posted_text_lines = 1;
}
} else {
$posted_text = "";
$posted_text_len = int(rand(100) + 1);
$posted_text_lines = int(rand(10) + 1);
}
if ($do_xref) {
# Get other references and put them in
$query = ("select post_id, topic_id, forum_id, poster_id " .
"from phpbb_posts " .
"where forum_id = $forum_id " .
"and topic_id = $topic_id " .
"and post_id < $post_id " .
"order by post_id " .
"limit 1");
my $query_results2 = db_fetch($query);
if (db_rows($query_results2) > 0) {
my ($post_id, $topic_id, $forum_id, $poster_id) =
$query_results2->fetchrow_array();
$article_id2 =
art_id_encode($post_id, $topic_id, $forum_id, $poster_id);
$article_id2 = "<" . $article_id2 . ">";
} else {
$article_id2 = " ";
}
} else {
$article_id2 = " ";
}
reply_msg("",
"$article_number\t" .
"$subject\t" .
"$username\t" .
"$post_time $timezone\t" .
"$article_id2\t" .
"Xref:$sitename $curr_group:$article_number\t" .
"$posted_text_len\t" .
"$posted_text_lines");
}
return;
}
sub get_userdetails {
my $poster_id = $_[0];
my $user_email;
my $username;
if (!defined($username_cache{$poster_id})) {
my $query = ("select u.user_email, u.username " .
"from phpbb_users as u " .
"where u.user_id = $poster_id");
my $query_results = db_fetch($query);
if (db_rows($query_results) > 0) {
($user_email, $username) =
$query_results->fetchrow_array();
if (!$user_email) {
$user_email = "unknown\@$sitename";
}
$username_cache{$poster_id} = "$user_email:$username";
} else {
$user_email = "unknown\@$sitename";
$username = "unknown";
}
} else {
($user_email, $username) =
split(/:/, $username_cache{$poster_id});
}
return "$user_email:$username";
}
sub remove_bb_code {
my $text = $_[0];
$text =~ s/&/&/ig;
$text =~ s/"/\"/ig;
$text =~ s/</</ig;
$text =~ s/>/>/ig;
$text =~ s/\[size=[0-9]+\]|\[\/size\]//ig;
$text =~ s/\[color=(\"\#)?[A-Za-z0-9]+(\")?\]|\[\/color\]//ig;
$text =~ s/\[url(=)?(\")?//ig;
$text =~ s/(\")?\](.+)\[\/url\]/$2/gi;
$text =~ s/\[email(=)?(\")?//ig;
$text =~ s/(\")?\](.+)\[\/email\]/$2/gi;
$text =~ s/\[font=(\"\#)?[A-Za-z]+(\")?\]|\[\/font\]//ig;
$text =~ s/\[list(=)?[1Aa]?\]|\[\/list(=)?[1Aa]?\]//ig;
$text =~ s/\[\*\]/ - /ig;
$text =~ s/\[(\/)?code\]//ig;
$text =~ s/\r/\n/g;
# strip out html linefeeds and replace them with real ones
#$text =~ s/<BR>/\n/gi;
$text =~ s/<BR>\n/\n/gi;
$text =~ s/<BR>/\n/gi;
# strip out all other html
$text =~ s/<.*?>//gi;
return $text;
}
sub art_id_encode {
my $post_id = $_[0];
my $topic_id = $_[1];
my $forum_id = $_[2];
my $poster_id = $_[3];
return ("$post_id$sep$topic_id$sep$forum_id$sep$poster_id\@$sitename");
}
sub art_id_decode {
my $article_id = $_[0];
# Strip sitename
$article_id =~ s/\@.*//g;
return split(/$sep/, $article_id);
}
sub greeting {
if ($posting_allowed) {
print STDOUT "200 NNTP Server - Posting Allowed\n";
} else {
print STDOUT "201 NNTP Server - No Posting Allowed\n";
}
}
sub reply_msg {
my ($code) = $_[0];
my ($string) = $_[1];
if ($code) {
print STDOUT "$code $string\r\n";
# console("$code $string") if ($debug > 0);
} else {
print STDOUT "$string\r\n";
# console("$string") if ($debug > 0);
}
}
sub db_connect {
my $dbh;
if ($use_dbi) {
console("Connecting to database using DBI") if ($debug > 5);
my $dsn = "DBI:mysql:database=$db;host=$db_host";
$dbh = DBI->connect($dsn,
$db_username,
$db_password,
{
RaiseError => 1,
PrintError => 1,
}
) or die ("Cant connect to $db using " .
"$db_username\@$db_host");
} else {
console("Connecting to database using Mysql module") if ($debug > 5);
$dbh = Mysql->connect($db_host,
$db,
$db_username,
$db_password);
$dbh->selectdb($db);
}
return $dbh;
}
sub db_disconnect {
if ($use_dbi) {
$dbh->disconnect;
} else {
undef $dbh;
}
}
sub db_fetch {
my $sql = $_[0];
my $qry;
console($sql) if ($debug > 1);
if (!defined($dbh)) {
$dbh = db_connect();
}
if ($use_dbi) {
console("Preparing Query") if ($debug > 1);
$qry = $dbh->prepare($sql)
|| console("$!");
console("Executing Query") if ($debug > 1);
$qry->execute()
|| console("Query failed ($sql) $!");
console("Returning Query") if ($debug > 1);
} else {
$qry = $dbh->query($sql);
}
return $qry;
}
sub db_rows {
my $self = $_[0];
if ($use_dbi) {
return $self->rows;
} else {
return $self->numrows();
}
}
sub console {
my $message = $_[0];
print STDERR "$message\n";
}