Notifier.pl

From Organic Design wiki
Revision as of 11:05, 22 June 2008 by Nad (talk | contribs)
  1. !/usr/bin/perl
  2. notifier.pl - Multiprotocol notification daemon for OrganicDesign
  3. Started 2008-06-20, based on wikid.plOur Perl scripts.
  4. - See http://www.organicdesign.co.nz/odserver.pl
  5. - Licenced under LGPL (http://www.gnu.org/copyleft/lesser.html)
  6. - 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;

  1. 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";

  1. Converts the process into a daemon
  2. - handles --install, --remove, --stop

daemonise();

  1. Retrieve rules

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

  1. 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 );

  1. 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) } } }

  1. 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(); }

  1. 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; }

  1. 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; } }

  1. Write passed content to passed file

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

  1. Return an array of scheme, user, password, host and path for a URL

sub parseURL { return ($1, $3, $4, $5, $6) if shift =~ /^(.+?):\/*((.+?):(.+?)@)?(.+?)\/(.+)?$/; }

  1. 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 } }

  1. 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 } }

  1. 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"; }

  1. 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."); } } }

  1. 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"; }

  1. Login to a MediaWiki
  2. 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 =~ /

\s*(

.+?<\/h2>\s*)?(.+?)\s*<\/div>/is) { logAdd "ERROR: $2" }

else { logAdd "ERROR: couldn't log $user in to $wiki!" } } } return $success; }

  1. Edit a MediaWiki page
  2. 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 =~ /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; }