Difference between revisions of "Notifier.pl"

From Organic Design wiki
m
m
 
(10 intermediate revisions by the same user not shown)
Line 1: Line 1:
 +
{{legacy}}
 +
<source lang="perl">
 
#!/usr/bin/perl
 
#!/usr/bin/perl
 
# notifier.pl - Multiprotocol notification daemon for OrganicDesign
 
# notifier.pl - Multiprotocol notification daemon for OrganicDesign
# Started 2008-06-20, based on wikid.pl{{perl}}
+
# Started 2008-06-20, based on wikid.pl
 
# - See http://www.organicdesign.co.nz/odserver.pl
 
# - See http://www.organicdesign.co.nz/odserver.pl
 
# - Licenced under LGPL (http://www.gnu.org/copyleft/lesser.html)
 
# - Licenced under LGPL (http://www.gnu.org/copyleft/lesser.html)
 
# - Author: http://www.organicdesign.co.nz/nad
 
# - Author: http://www.organicdesign.co.nz/nad
  
use POSIX qw(strftime setsid);
+
use POSIX qw(setsid);
 
use FindBin qw($Bin);
 
use FindBin qw($Bin);
 
use Getopt::Long;
 
use Getopt::Long;
Line 16: Line 18:
 
use strict;
 
use strict;
  
# Set up configuration defaults and get args
+
# Set up global configuration from args and defaults
our %config = (join ' ', @ARGV);
+
%::config = (join ' ', @ARGV);
GetOptions(\%config, 'user=s', 'pass=s', 'rules=s');
+
GetOptions(\%::config, 'user=s', 'pass=s', 'rules=s');
$::user  = exists $config{user}  ? $config{user}  : die "--user arg missing!";
+
$::user  = exists $::config{user}  ? $::config{user}  : die "--user arg missing!";
$::pass  = exists $config{pass}  ? $config{pass}  : die "--pass arg missing!";
+
$::pass  = exists $::config{pass}  ? $::config{pass}  : '';
$::rules  = exists $config{rules} ? $config{rules} : die "--rules arg missing!";
+
$::rules  = exists $::config{rules} ? $::config{rules} : 'http://www.organicdesign.co.nz/wiki/index.php?title=Config:Notifier?action=raw';
 
$::dir    = $Bin;
 
$::dir    = $Bin;
 
$::log    = "$::dir/$::user.log";
 
$::log    = "$::dir/$::user.log";
Line 27: Line 29:
 
$0        = "$daemon::$::user";
 
$0        = "$daemon::$::user";
  
# Run as a daemon (see daemonise.pl article for more details and references regarding perl daemons)
+
# Converts the process into a daemon
open STDIN, '/dev/null';
+
# - handles --install, --remove, --stop
open STDOUT, ">>../$::log";
+
daemonise();
open STDERR, ">>../$::log";
 
defined ( my $pid = fork ) or die "Can't fork: $!";
 
exit if $pid;
 
setsid or die "Can't start a new session: $!";
 
umask 0;
 
  
# Set up a global user-agent
+
# Retrieve rules
$::client = LWP::UserAgent->new(
+
$::rules = getResource($::rules) or die "rules missing or unreadable!";
 +
 
 +
# Global user-agent for making HTTP connections
 +
$::http = LWP::UserAgent->new(
 
cookie_jar => {},
 
cookie_jar => {},
 
agent      => 'Mozilla/5.0',
 
agent      => 'Mozilla/5.0',
Line 45: Line 45:
 
);
 
);
  
# Connect to an IRC channels
+
# Login to channels and wikis found in rules
$::select = new IO::Select;
+
foreach (split '/^/', $::rules) {
ircConnect('irc.freenode.net', $::user, $::pass, '#organicdesign');
+
if (/^\*?\s*schedule\s*:\s*(.+?)\s*^\*?\s*source\s*:\s*(.+?)\s*^\*?\s*match\s*:\s*(.+?)\s*^\*?\s*target\s*:\s*(.+?)\s*^\*?\s*message\s*:\s*(.+?)\s*^\*?\s*comment\s*:\s*(.+?)\s*/msi) {
 +
($schedule, $source, $match, $target, $message, $comment) = ($1, $2, $3, $4, $5, $6);
 +
($scheme,,, $host, $path) = parseURL($source);
 +
if    ($scheme eq 'irc')      { ircConnect($host, $path, $::user, $::pass) }
 +
elsif ($scheme eq 'mediawiki') { wikiLogin( $host.$path,  $::user, $::pass) }
 +
}
 +
}
  
# Retrieve rules
+
# Main schedule & socket loop
 
+
while (1) {
# Login to all resources found in rules which require authentication (each IRC server needs its own stream)
 
wikiLogin($::wiki, $::name, $::password);
 
 
 
# Add a message to the wiki-changes unless --quiet was specified
 
logAdd("$::daemon has started and is authenticated as \"$::user\"");
 
  
# Main schedule & socket loop
+
# Refresh the rules every minute or so
while(1) {
 
  
 
# Check if any of the cron items match the current localtime
 
# Check if any of the cron items match the current localtime
Line 71: Line 71:
 
# Handle any IO on the IRC channels we're connected to
 
# Handle any IO on the IRC channels we're connected to
 
ircChannels();
 
ircChannels();
 
 
}
 
}
  
Line 77: Line 76:
 
sub logAdd {
 
sub logAdd {
 
my $entry = shift;
 
my $entry = shift;
my $subname;
+
open LOGH, '>>', $::log or die "Can't open $::log for writing!";
$subname = $::subname ? "[$subname]\t" : '';
+
print LOGH localtime()." : $entry\n";  
open LOGH,'>>',$::log or die "Can't open $::log for writing!";
 
print LOGH localtime()." : $subname$entry\n";  
 
 
close LOGH;
 
close LOGH;
 
return $entry;
 
return $entry;
Line 88: Line 85:
 
sub readFile {
 
sub readFile {
 
my $file = shift;
 
my $file = shift;
if (open FH,'<',$file) {
+
if (open FH, '<', $file) {
 
binmode FH;
 
binmode FH;
 
sysread FH, (my $out), -s $file;
 
sysread FH, (my $out), -s $file;
Line 99: Line 96:
 
sub writeFile {
 
sub writeFile {
 
my $file = shift;
 
my $file = shift;
if (open FH,'>',$file) {
+
if (open FH, '>', $file) {
 
binmode FH;
 
binmode FH;
 
print FH shift;
 
print FH shift;
Line 109: Line 106:
 
# Return an array of scheme, user, password, host and path for a URL
 
# Return an array of scheme, user, password, host and path for a URL
 
sub parseURL {
 
sub parseURL {
return ($1, $3, $4, $5, $6) if shift =~ /^(.+?):\/*((.+?):(.+?)@)?(.+?)(\/.+)?$/;
+
return ($1, $3, $4, $5, $6) if shift =~ /^(.+?):\/*((.+?):(.+?)@)?(.+?)\/(.+)?$/;
 
}
 
}
  
Line 116: Line 113:
 
($scheme, $user, $pass, $host, $path) = parseURL(shift);
 
($scheme, $user, $pass, $host, $path) = parseURL(shift);
 
if ($scheme eq 'file') {
 
if ($scheme eq 'file') {
 +
return readFile($path ? "/$host/$path" : "/$host");
 
}
 
}
 
elsif ($scheme =~ /^https?$/) {
 
elsif ($scheme =~ /^https?$/) {
}
+
my $response = $::http->get($url);
elsif ($scheme eq 'mediawiki') {
+
return $response->content if $response->is_success;
 
}
 
}
 
elsif ($scheme =~ /^pop3?$/i) {
 
elsif ($scheme =~ /^pop3?$/i) {
 
}
 
}
else { logAdd "Unsupported scheme ($scheme) in getResource" }
+
else { return '' }
 
}
 
}
  
Line 131: Line 129:
 
($scheme, $user, $pass, $host, $path) = parseURL($url);
 
($scheme, $user, $pass, $host, $path) = parseURL($url);
 
if ($scheme eq 'file') {
 
if ($scheme eq 'file') {
 +
writeFile($path ? "/$host/$path" : "/$host", $content);
 
}
 
}
 
elsif ($scheme eq 'mediawiki') {
 
elsif ($scheme eq 'mediawiki') {
 +
wikiPageEdit($1, $2, $content, $comment, 0) if $url =~ /(.+?)\?title=(.+)/;
 +
}
 
}
 
}
 
elsif ($scheme eq 'mailto') {
 
elsif ($scheme eq 'mailto') {
 
}
 
}
 
elsif ($scheme eq 'irc') {
 
elsif ($scheme eq 'irc') {
 +
print $::channels{$path} $content;
 
}
 
}
else { logAdd "Unsupported scheme ($scheme) in putResource" }
+
else { return '' }
 
}
 
}
  
 
# Connect to an IRC channel
 
# Connect to an IRC channel
 
sub ircConnect {
 
sub ircConnect {
($host, $user, $pass, $channel) = @_;
+
($host, $channel, $user, $pass) = @_;
 
 
 +
# Connect to IRC server
 
$handle = IO::Socket::INET->new( PeerAddr => $host, PeerPort => 6667, Proto => 'tcp' )
 
$handle = IO::Socket::INET->new( PeerAddr => $host, PeerPort => 6667, Proto => 'tcp' )
 
or logAdd "could not connect to $channel";
 
or logAdd "could not connect to $channel";
  
$::select->add($handle);
+
# Add the stream to data structures creating if necessary
 +
%::channels = () unless defined %::channels;
 +
$::socks = new IO::Select unless defined $::socks;
 +
$::socks->add($handle);
 +
$::channels{fileno $handle} = ( channel => $channel, buffer => '' );
 +
$::channels{$channel} = $handle;
  
 +
# Identify nick
 
while (<$handle>) {
 
while (<$handle>) {
 
if (/(NOTICE AUTH).*(checking ident)/i) {
 
if (/(NOTICE AUTH).*(checking ident)/i) {
print $handle "NICK $user\nUSER $user 0 0 :odbot.pl\n";
+
print $handle "NICK $user\nUSER $user 0 0 :$::daemon\n";
 
last;
 
last;
 
}
 
}
 
}
 
}
  
 +
# Wait for initialisation message and respond to PING if necessary
 
while (<$handle>) {
 
while (<$handle>) {
  
Line 169: Line 179:
 
}
 
}
  
# Wait for a few secs and join the channel
+
# Wait for a while and join the channel
 
sleep 1;
 
sleep 1;
 
print $handle "JOIN $channel\n";
 
print $handle "JOIN $channel\n";
Line 178: Line 188:
  
 
# Loop thru readable handles
 
# Loop thru readable handles
for my $handle ($::select->can_read(1)) {
+
for my $handle ($::socks->can_read(0.5)) {
  
 
# Read some data from this handle
 
# Read some data from this handle
 
if (sysread $handle, my $data, 100) {
 
if (sysread $handle, my $data, 100) {
  
# Append the data to the appropriate stream
 
 
my $stream = fileno $handle;
 
my $stream = fileno $handle;
$streams{$stream} = exists($streams{$stream}) ? $streams{$stream}.$data : $data;
+
$channel = $::channels{$stream}{channel};
 +
 
 +
# Append the data to this streams buffer
 +
$::channels{$stream}{buffer} .= $data;
  
 
# Remove and process any complete messages in the stream
 
# Remove and process any complete messages in the stream
if ($streams{$stream} =~ s/^(.*\r?\n)//s) {
+
if ($::channels{$stream}{buffer} =~ s/^(.*\r?\n)//s) {
 
for (split /\r?\n/, $1) {
 
for (split /\r?\n/, $1) {
  
Line 197: Line 209:
 
$text =~ s/[\r\n]//g;
 
$text =~ s/[\r\n]//g;
 
print $handle "PONG $text\n";
 
print $handle "PONG $text\n";
next;
 
 
}
 
}
  
# Extract info and tidy
+
# Not a ping
($nick, $type, $chan) = split(/ /, $_);
+
else {
($nick, $host) = split(/!/, $nick);
+
 
$nick =~ s/://;
+
# Extract info and tidy
$text =~ s/[:\r\n]+//;
+
($nick, $type, $chan) = split(/ /, $_);
 +
($nick, $host) = split(/!/, $nick);
 +
$nick =~ s/://;
 +
$text =~ s/[:\r\n]+//;
  
# Process if the line is a message in the channel
+
# Process if the line is a message in the channel
if ($chan eq $channel) {
+
if ($chan eq $channel) {
$ts = localtime();
+
$ts = localtime();
$ts = $1 if $ts =~ /(\d\d:\d\d:\d\d)/;
+
$ts = $1 if $ts =~ /(\d\d:\d\d:\d\d)/;
print "($ts) $nick: $text\n";
+
logAdd "($ts) $channel/$nick: $text\n";
  
# Respond if message is known
+
# Respond if message is known
if ($text =~ /(^|\W)$user(\W|$)/i) {
+
if ($text =~ /(^|\W)$::user(\W|$)/i) {
$msg = "Yo $nick, you talking to me?";
+
$msg = "Yo $nick, you talking to me?";
print $handle "PRIVMSG $channel :$msg\n";
+
print $handle "PRIVMSG $channel :$msg\n";
print "($ts) $user: $msg\n";
+
logAdd "($ts) $channel/$user: $msg\n";
 +
}
 
}
 
}
 
}
 
}
Line 225: Line 240:
 
# Handle has closed (this shouldn't happen, it means the channel must have kicked us)
 
# Handle has closed (this shouldn't happen, it means the channel must have kicked us)
 
else {
 
else {
$::select->remove($handle);
+
$::socks->remove($handle);
delete $streams{$stream};
 
 
$handle->close();
 
$handle->close();
logAdd "Stream$stream disconnected.");
+
delete $::channels{$stream};
 +
delete $::channels{$channel};
 +
logAdd "$channel disconnected.");
 +
}
 +
}
 +
}
 +
 
 +
# Converts the process into a daemon and also handle --install and --remove
 +
sub daemonise {
 +
 
 +
# Install the service into init.d and rc2-5.d
 +
if (exists $::config{install}) {
 +
# todo: check if notifier of this name not already running
 +
my $fn = "$::daemon-".lc $::user;
 +
my $target = "/etc/init.d/$fn.sh";
 +
writeFile $target, "#!/bin/sh\n/usr/bin/perl $::dir/$::daemon --user=$::user --pass=$::pass --rules=$::rules\n";
 +
symlink $target, "/etc/rc$_.d/S99$fn" for 2..5;
 +
chmod 0755, "/etc/init.d/$fn.sh";
 +
logAdd "$fn.sh added to /etc/init.d";
 +
}
 +
 
 +
# Remove the named service and exit
 +
# - remove shell script from init.d and links from rc[2-5].d
 +
if (exists $::config{remove}) {
 +
my $fn = "$::daemon-".lc $::user;
 +
unlink "/etc/rc$_.d/S99$fn" for 2..5;
 +
unlink "/etc/init.d/$fn.sh";
 +
logAdd "$fn.sh removed from /etc/init.d";
 +
exit;
 +
}
 +
 
 +
# Stop daemon and exit
 +
elsif (exists $::config{stop}) {
 +
logAdd "Stopping $::daemon::$::user... (not done yet)";
 +
# - find currently running notifier of this name and kill it
 +
exit;
 +
}
 +
 
 +
# Start daemon normally
 +
# - todo: check if notifier of this name not already running
 +
open STDIN, '/dev/null';
 +
open STDOUT, ">>$::log";
 +
open STDERR, ">>$::log";
 +
defined (my $pid = fork) or die "Can't fork: $!";
 +
exit if $pid;
 +
setsid or die "Can't start a new session: $!";
 +
umask 0;
 +
logAdd "$::daemon::$::user started successfully\n";
 +
}
 +
 
 +
# Login to a MediaWiki
 +
# todo: check if logged in first
 +
sub wikiLogin {
 +
my ($wiki, $user, $pass) = @_;
 +
my $url = "$wiki?title=Special:Userlogin";
 +
my $success = 0;
 +
my $retries = 1;
 +
while ($retries--) {
 +
my $html = '';
 +
if ($::http->get($url)->is_success) {
 +
my %form = (wpName => $user, wpPassword => $pass, wpLoginattempt => 'Log in', wpRemember => '1');
 +
my $response = $::http->post("$url&action=submitlogin&type=login", \%form);
 +
$html = $response->content;
 +
$success = $response->is_success && $html =~ /You are now logged in/;
 +
}
 +
if ($success) {
 +
logAdd "$user successfully logged in to $wiki.";
 +
$retries = 0;
 +
}
 +
else {
 +
if ($html =~ /<div class="errorbox">\s*(<h2>.+?<\/h2>\s*)?(.+?)\s*<\/div>/is) { logAdd "ERROR: $2" }
 +
else { logAdd "ERROR: couldn't log $user in to $wiki!" }
 
}
 
}
 
}
 
}
 +
return $success;
 +
}
 +
 +
# Edit a MediaWiki page
 +
# todo: don't return success if edited succeeded but made no changes
 +
sub wikiPageEdit {
 +
my ($wiki, $title, $content, $comment, $minor) = @_;
 +
logAdd "Attempting to edit \"$title\" on $wiki";
 +
my $success = 0;
 +
my $err = 'ERROR';
 +
my $retries = 1;
 +
while ($retries--) {
 +
# Request the page for editing and extract the edit-token
 +
my $response = $::http->get("$wiki?title=$title&action=edit");
 +
if ($response->is_success and (
 +
$response->content =~ /^<input type='hidden' value="(.*?)" name="wpSection".+?value="(.*?)" name="wpStarttime".+?value="(.*?)" name="wpEdittime".+?<\/textarea>.+?^<input type='hidden' value="(.+?)" name="wpEditToken".+?name="wpAutoSummary" type="hidden" value="(.+?)"/sm
 +
or
 +
$response->content =~ /^<input type='hidden' value="(.*?)" name="wpSection".+?value="(.*?)" name="wpEdittime".+?value="(.*?)" name="wpEditToken"/sm
 +
)) {
 +
# Got token etc, now submit an edit-form
 +
my %form = (
 +
wpSection  => $1,
 +
wpEdittime  => $5 ? $3 : $2,
 +
wpEditToken => $5 ? $4 : $3,
 +
wpTextbox1  => $content,
 +
wpSummary  => $comment,
 +
wpSave      => 'Save page'
 +
);
 +
$form{wpMinoredit} = 1 if $minor;
 +
$form{wpStarttime} = $2 if $5;
 +
$form{wpAutoSummary} = $5 if $5;
 +
$response = $::http->post("$wiki?title=$title&action=submit", \%form);
 +
if ($response->content =~ /<!-- start content -->Someone else has changed this page/) {
 +
$err = 'EDIT CONFLICT';
 +
$retries = 0;
 +
} else { $success = !$response->is_error }
 +
} else { $err = $response->is_success ? 'MATCH FAILED' : 'RQST FAILED' }
 +
if ($success) { $retries = 0; logAdd "\"$title\" updated." }
 +
else { logAdd "$err: Couldn't edit \"$title\" on $wiki!\n" }
 +
}
 +
return $success;
 
}
 
}
 +
</source>
 +
[[Category:PERL]]

Latest revision as of 03:25, 23 April 2020

Legacy.svg Legacy: This article describes a concept that has been superseded in the course of ongoing development on the Organic Design wiki. Please do not develop this any further or base work on this concept, now this page is for historic record only.
#!/usr/bin/perl
# notifier.pl - Multiprotocol notification daemon for OrganicDesign
# Started 2008-06-20, based on wikid.pl
# - See http://www.organicdesign.co.nz/odserver.pl
# - Licenced under LGPL (http://www.gnu.org/copyleft/lesser.html)
# - Author: http://www.organicdesign.co.nz/nad

use POSIX qw(setsid);
use FindBin qw($Bin);
use Getopt::Long;
use Cwd;
use HTTP::Request;
use LWP::UserAgent;
use IO::Socket;
use IO::Select;
use strict;

# Set up global configuration from args and defaults
%::config = (join ' ', @ARGV);
GetOptions(\%::config, 'user=s', 'pass=s', 'rules=s');
$::user   = exists $::config{user}  ? $::config{user}  : die "--user arg missing!";
$::pass   = exists $::config{pass}  ? $::config{pass}  : '';
$::rules  = exists $::config{rules} ? $::config{rules} : 'http://www.organicdesign.co.nz/wiki/index.php?title=Config:Notifier?action=raw';
$::dir    = $Bin;
$::log    = "$::dir/$::user.log";
$::daemon = 'notifier.pl';
$0        = "$daemon::$::user";

# Converts the process into a daemon
# - handles --install, --remove, --stop
daemonise();

# Retrieve rules
$::rules = getResource($::rules) or die "rules missing or unreadable!";

# Global user-agent for making HTTP connections
$::http = LWP::UserAgent->new(
	cookie_jar => {},
	agent      => 'Mozilla/5.0',
	from       => "$::user\@organicdesign.co.nz",
	timeout    => 30,
	max_size   => 100000
);

# Login to channels and wikis found in rules
foreach (split '/^/', $::rules) {
	if (/^\*?\s*schedule\s*:\s*(.+?)\s*^\*?\s*source\s*:\s*(.+?)\s*^\*?\s*match\s*:\s*(.+?)\s*^\*?\s*target\s*:\s*(.+?)\s*^\*?\s*message\s*:\s*(.+?)\s*^\*?\s*comment\s*:\s*(.+?)\s*/msi) {
		($schedule, $source, $match, $target, $message, $comment) = ($1, $2, $3, $4, $5, $6);
		($scheme,,, $host, $path) = parseURL($source);
		if    ($scheme eq 'irc')       { ircConnect($host, $path, $::user, $::pass) }
		elsif ($scheme eq 'mediawiki') { wikiLogin( $host.$path,  $::user, $::pass) }
	}
}

# Main schedule & socket loop
while (1) {

	# Refresh the rules every minute or so

	# Check if any of the cron items match the current localtime
	my $date = scalar localtime;
	for my $i (@cron) {
		if ($i =~ /^\*\s*\/(.+?)\/\s*:\s*(\S+)\s*(.+)?/) {
			my($rule, $func, $args) = ($1, $2, $3);
			spawn($func, split /\s*,\s*/, $args) if $date =~ /$rule/;
		}
	}

	# Handle any IO on the IRC channels we're connected to
	ircChannels();
}

# Append message to log file
sub logAdd {
	my $entry = shift;
	open LOGH, '>>', $::log or die "Can't open $::log for writing!";
	print LOGH localtime()." : $entry\n"; 
	close LOGH;
	return $entry;
}

# Read and return content from passed file
sub readFile {
	my $file = shift;
	if (open FH, '<', $file) {
		binmode FH;
		sysread FH, (my $out), -s $file;
		close FH;
		return $out;
	}
}

# Write passed content to passed file
sub writeFile {
	my $file = shift;
	if (open FH, '>', $file) {
		binmode FH;
		print FH shift;
		close FH;
		return $file;
	}
}

# Return an array of scheme, user, password, host and path for a URL
sub parseURL {
	return ($1, $3, $4, $5, $6) if shift =~ /^(.+?):\/*((.+?):(.+?)@)?(.+?)\/(.+)?$/;
}

# Obtain a resource from a URL
sub getResource {
	($scheme, $user, $pass, $host, $path) = parseURL(shift);
	if ($scheme eq 'file') {
		return readFile($path ? "/$host/$path" : "/$host");
	}
	elsif ($scheme =~ /^https?$/) {
		my $response = $::http->get($url);
		return $response->content if $response->is_success;
	}
	elsif ($scheme =~ /^pop3?$/i) {
	}
	else { return '' }
}

# Update a resource given a URL
sub putResource {
	my ($url, $content, $comment) = @_;
	($scheme, $user, $pass, $host, $path) = parseURL($url);
	if ($scheme eq 'file') {
		writeFile($path ? "/$host/$path" : "/$host", $content);
	}
	elsif ($scheme eq 'mediawiki') {
		wikiPageEdit($1, $2, $content, $comment, 0) if $url =~ /(.+?)\?title=(.+)/;
		}
	}
	elsif ($scheme eq 'mailto') {
	}
	elsif ($scheme eq 'irc') {
		print $::channels{$path} $content;
	}
	else { return '' }
}

# Connect to an IRC channel
sub ircConnect {
	($host, $channel, $user, $pass) = @_;
	
	# Connect to IRC server
	$handle = IO::Socket::INET->new( PeerAddr => $host, PeerPort => 6667, Proto => 'tcp' )
		or logAdd "could not connect to $channel";

	# Add the stream to data structures creating if necessary
	%::channels = () unless defined %::channels;
	$::socks = new IO::Select unless defined $::socks;
	$::socks->add($handle);
	$::channels{fileno $handle} = ( channel => $channel, buffer => '' );
	$::channels{$channel} = $handle;

	# Identify nick
	while (<$handle>) {
		if (/(NOTICE AUTH).*(checking ident)/i) {
			print $handle "NICK $user\nUSER $user 0 0 :$::daemon\n";
			last;
		}
	}

	# Wait for initialisation message and respond to PING if necessary
	while (<$handle>) {

		# if the server asks for a ping
		print $handle "PONG :".(split(/ :/, $_))[1] if /^PING/;

		# end of MOTD section
		if (/ (376|422) /) {
			print $handle "NICKSERV :identify $user $pass\n";
			last;
		}
	}

	# Wait for a while and join the channel
	sleep 1;
	print $handle "JOIN $channel\n";
}

# Handle any IO on the IRC channels we're connected to
sub ircChannels {

	# Loop thru readable handles
	for my $handle ($::socks->can_read(0.5)) {

		# Read some data from this handle
		if (sysread $handle, my $data, 100) {

			my $stream = fileno $handle;
			$channel = $::channels{$stream}{channel};

			# Append the data to this streams buffer
			$::channels{$stream}{buffer} .= $data;

			# Remove and process any complete messages in the stream
			if ($::channels{$stream}{buffer} =~ s/^(.*\r?\n)//s) {
				for (split /\r?\n/, $1) {

					($command, $text) = split(/ :/, $_);

					# Respond to pings if any
					if ($command eq 'PING') {
						$text =~ s/[\r\n]//g;
						print $handle "PONG $text\n";
					}

					# Not a ping
					else {

						# Extract info and tidy
						($nick, $type, $chan) = split(/ /, $_);
						($nick, $host) = split(/!/, $nick);
						$nick =~ s/://;
						$text =~ s/[:\r\n]+//;

						# Process if the line is a message in the channel
						if ($chan eq $channel) {
							$ts = localtime();
							$ts = $1 if $ts =~ /(\d\d:\d\d:\d\d)/;
							logAdd "($ts) $channel/$nick: $text\n";

							# Respond if message is known
							if ($text =~ /(^|\W)$::user(\W|$)/i) {
								$msg = "Yo $nick, you talking to me?";
								print $handle "PRIVMSG $channel :$msg\n";
								logAdd "($ts) $channel/$user: $msg\n";
							}
						}
					}
				}
			}
		}

		# Handle has closed (this shouldn't happen, it means the channel must have kicked us)
		else {
			$::socks->remove($handle);
			$handle->close();
			delete $::channels{$stream};
			delete $::channels{$channel};
			logAdd "$channel disconnected.");
		}
	}
}

# Converts the process into a daemon and also handle --install and --remove
sub daemonise {

	# Install the service into init.d and rc2-5.d
	if (exists $::config{install}) {
		# todo: check if notifier of this name not already running
		my $fn = "$::daemon-".lc $::user;
		my $target = "/etc/init.d/$fn.sh";
		writeFile $target, "#!/bin/sh\n/usr/bin/perl $::dir/$::daemon --user=$::user --pass=$::pass --rules=$::rules\n";
		symlink $target, "/etc/rc$_.d/S99$fn" for 2..5;
		chmod 0755, "/etc/init.d/$fn.sh";
		logAdd "$fn.sh added to /etc/init.d";
	}

	# Remove the named service and exit
	# - remove shell script from init.d and links from rc[2-5].d
	if (exists $::config{remove}) {
		my $fn = "$::daemon-".lc $::user;
		unlink "/etc/rc$_.d/S99$fn" for 2..5;
		unlink "/etc/init.d/$fn.sh";
		logAdd "$fn.sh removed from /etc/init.d";
		exit;
	}

	# Stop daemon and exit
	elsif (exists $::config{stop}) {
		logAdd "Stopping $::daemon::$::user... (not done yet)";
		# - find currently running notifier of this name and kill it
		exit;
	}

	# Start daemon normally
	# - todo: check if notifier of this name not already running
	open STDIN, '/dev/null';
	open STDOUT, ">>$::log";
	open STDERR, ">>$::log";
	defined (my $pid = fork) or die "Can't fork: $!";
	exit if $pid;
	setsid or die "Can't start a new session: $!";
	umask 0;
	logAdd "$::daemon::$::user started successfully\n";
}

# Login to a MediaWiki
# todo: check if logged in first
sub wikiLogin {
	my ($wiki, $user, $pass) = @_;
	my $url = "$wiki?title=Special:Userlogin";
	my $success = 0;
	my $retries = 1;
	while ($retries--) {
		my $html = '';
		if ($::http->get($url)->is_success) {
			my %form = (wpName => $user, wpPassword => $pass, wpLoginattempt => 'Log in', wpRemember => '1');
			my $response = $::http->post("$url&action=submitlogin&type=login", \%form);
			$html = $response->content;
			$success = $response->is_success && $html =~ /You are now logged in/;
		}
		if ($success) {
			logAdd "$user successfully logged in to $wiki.";
			$retries = 0;
		}
		else {
			if ($html =~ /<div class="errorbox">\s*(<h2>.+?<\/h2>\s*)?(.+?)\s*<\/div>/is) { logAdd "ERROR: $2" }
			else { logAdd "ERROR: couldn't log $user in to $wiki!" }
		}
	}
	return $success;
}

# Edit a MediaWiki page
# todo: don't return success if edited succeeded but made no changes
sub wikiPageEdit {
	my ($wiki, $title, $content, $comment, $minor) = @_;
	logAdd "Attempting to edit \"$title\" on $wiki";
	my $success = 0;
	my $err = 'ERROR';
	my $retries = 1;
	while ($retries--) {
		# Request the page for editing and extract the edit-token
		my $response = $::http->get("$wiki?title=$title&action=edit");
		if ($response->is_success and (
			$response->content =~ /^<input type='hidden' value="(.*?)" name="wpSection".+?value="(.*?)" name="wpStarttime".+?value="(.*?)" name="wpEdittime".+?<\/textarea>.+?^<input type='hidden' value="(.+?)" name="wpEditToken".+?name="wpAutoSummary" type="hidden" value="(.+?)"/sm
			or
			$response->content =~ /^<input type='hidden' value="(.*?)" name="wpSection".+?value="(.*?)" name="wpEdittime".+?value="(.*?)" name="wpEditToken"/sm
		)) {
			# Got token etc, now submit an edit-form
			my %form = (
				wpSection   => $1,
				wpEdittime  => $5 ? $3 : $2,
				wpEditToken => $5 ? $4 : $3,
				wpTextbox1  => $content,
				wpSummary   => $comment,
				wpSave      => 'Save page'
			);
			$form{wpMinoredit} = 1 if $minor;
			$form{wpStarttime} = $2 if $5;
			$form{wpAutoSummary} = $5 if $5;
			$response = $::http->post("$wiki?title=$title&action=submit", \%form);
			if ($response->content =~ /<!-- start content -->Someone else has changed this page/) {
				$err = 'EDIT CONFLICT';
				$retries = 0;
			} else { $success = !$response->is_error }
		} else { $err = $response->is_success ? 'MATCH FAILED' : 'RQST FAILED' }
		if ($success) { $retries = 0; logAdd "\"$title\" updated." }
		else { logAdd "$err: Couldn't edit \"$title\" on $wiki!\n" }
	}
	return $success;
}