Files
znc/modules/modperl/startup.pl
2011-06-11 18:21:06 +07:00

585 lines
12 KiB
Perl

#
# Copyright (C) 2004-2011 See the AUTHORS file for details.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 as published
# by the Free Software Foundation.
#
use 5.010;
use strict;
use warnings;
use ZNC;
use IO::File;
use feature 'switch', 'say';
package ZNC::Core;
my $uuidtype;
my $uuidgen;
our %pmods;
my %modrefcount;
sub Init {
if (eval { require Data::UUID }) {
$uuidtype = 'Data::UUID';
$uuidgen = new Data::UUID;
} elsif (eval { require UUID }) {
$uuidtype = 'UUID';
} else {
$uuidtype = 'int';
$uuidgen = 0;
}
}
sub CreateUUID {
my $res;
given ($uuidtype) {
when ('Data::UUID') {
$res = $uuidgen->create_str;
}
when ('UUID') {
my ($uuid, $str);
UUID::generate($uuid);
UUID::unparse($uuid, $res);
}
when ('int') {
$uuidgen++;
$res = "$uuidgen";
}
}
say "Created new UUID for modperl with '$uuidtype': $res";
return $res;
}
sub unloadByIDUser {
my ($id, $user) = @_;
my $modpath = $pmods{$id}{_cmod}->GetModPath;
my $modname = $pmods{$id}{_cmod}->GetModName;
$pmods{$id}->OnShutdown;
$user->GetModules->removeModule($pmods{$id}{_cmod});
delete $pmods{$id}{_cmod};# Just for the case
delete $pmods{$id}{_nv};
delete $pmods{$id}{_ptimers};
delete $pmods{$id}{_sockets};
delete $pmods{$id};
unless (--$modrefcount{$modname}) {
say "Unloading $modpath from perl";
ZNC::_CleanupStash($modname);
delete $INC{$modpath};
}
}
sub UnloadModule {
my ($cmod) = @_;
unloadByIDUser($cmod->GetPerlID, $cmod->GetUser);
}
sub UnloadAll {
while (my ($id, $pmod) = each %pmods) {
unloadByIDUser($id, $pmod->{_cmod}->GetUser);
}
}
sub IsModule {
my $path = shift;
my $modname = shift;
my $f = IO::File->new($path);
grep {/package\s+$modname\s*;/} <$f>;
}
sub LoadModule {
my ($modname, $args, $user) = @_;
$modname =~ /^\w+$/ or return ($ZNC::Perl_LoadError, "Module names can only contain letters, numbers and underscores, [$modname] is invalid.");
return ($ZNC::Perl_LoadError, "Module [$modname] already loaded.") if defined $user->GetModules->FindModule($modname);
my $modpath = ZNC::String->new;
my $datapath = ZNC::String->new;
ZNC::CModules::FindModPath("$modname.pm", $modpath, $datapath) or return ($ZNC::Perl_NotFound);
$modpath = $modpath->GetPerlStr;
return ($ZNC::Perl_LoadError, "Incorrect perl module [$modpath]") unless IsModule $modpath, $modname;
eval {
require $modpath;
};
if ($@) {
# modrefcount was 0 before this, otherwise it couldn't die.
# so can safely remove module from %INC
delete $INC{$modpath};
die $@;
}
$modrefcount{$modname}++;
my $id = CreateUUID;
$datapath = $datapath->GetPerlStr;
$datapath =~ s/\.pm$//;
my $cmod = ZNC::CPerlModule->new($user, $modname, $datapath, $id);
my %nv;
tie %nv, 'ZNC::ModuleNV', $cmod;
my $pmod = bless {
_cmod=>$cmod,
_nv=>\%nv
}, $modname;
$cmod->SetDescription($pmod->description);
$cmod->SetArgs($args);
$cmod->SetModPath($modpath);
$pmods{$id} = $pmod;
$user->GetModules->push_back($cmod);
my $x = '';
my $loaded = 0;
eval {
$loaded = $pmod->OnLoad($args, $x);
};
if ($@) {
$x .= ' ' if '' ne $x;
$x .= $@;
}
if (!$loaded) {
unloadByIDUser($id, $user);
if ($x) {
return ($ZNC::Perl_LoadError, "Module [$modname] aborted: $x");
}
return ($ZNC::Perl_LoadError, "Module [$modname] aborted.");
}
if ($x) {
return ($ZNC::Perl_Loaded, "[$x] [$modpath]");
}
return ($ZNC::Perl_Loaded, "[$modpath]")
}
sub GetModInfo {
my ($modname) = @_;
$modname =~ /^\w+$/ or return ($ZNC::Perl_LoadError, "Module names can only contain letters, numbers and underscores, [$modname] is invalid.");
my $modpath = ZNC::String->new;
my $datapath = ZNC::String->new;
ZNC::CModules::FindModPath("$modname.pm", $modpath, $datapath) or return ($ZNC::Perl_NotFound, "Unable to find module [$modname]");
$modpath = $modpath->GetPerlStr;
return ($ZNC::Perl_LoadError, "Incorrect perl module.") unless IsModule $modpath, $modname;
require $modpath;
my $pmod = bless {}, $modname;
return ($ZNC::Perl_Loaded, $modpath, $pmod->description, $pmod->wiki_page)
}
sub ModInfoByPath {
my ($modpath, $modname) = @_;
die "Incorrect perl module." unless IsModule $modpath, $modname;
require $modpath;
my $pmod = bless {}, $modname;
return ($pmod->description, $pmod->wiki_page)
}
sub CallModFunc {
my $id = shift;
my $func = shift;
my $default = shift;
my @arg = @_;
my $res = $pmods{$id}->$func(@arg);
# print "Returned from $func(@_): $res, (@arg)\n";
unless (defined $res) {
$res = $default if defined $default;
}
($res, @arg)
}
sub CallTimer {
my $modid = shift;
my $timerid = shift;
$pmods{$modid}->_CallTimer($timerid)
}
sub CallSocket {
my $modid = shift;
$pmods{$modid}->_CallSocket(@_)
}
sub RemoveTimer {
my $modid = shift;
my $timerid = shift;
$pmods{$modid}->_RemoveTimer($timerid)
}
sub RemoveSocket {
my $modid = shift;
my $sockid = shift;
$pmods{$modid}->_RemoveSocket($sockid)
}
package ZNC::ModuleNV;
sub TIEHASH {
my $name = shift;
my $cmod = shift;
bless {cmod=>$cmod, last=>-1}, $name
}
sub FETCH {
my $self = shift;
my $key = shift;
return $self->{cmod}->GetNV($key) if $self->{cmod}->ExistsNV($key);
return undef
}
sub STORE {
my $self = shift;
my $key = shift;
my $value = shift;
$self->{cmod}->SetNV($key, $value);
}
sub DELETE {
my $self = shift;
my $key = shift;
$self->{cmod}->DelNV($key);
}
sub CLEAR {
my $self = shift;
$self->{cmod}->ClearNV;
}
sub EXISTS {
my $self = shift;
my $key = shift;
$self->{cmod}->ExistsNV($key)
}
sub FIRSTKEY {
my $self = shift;
my @keys = $self->{cmod}->GetNVKeys;
$self->{last} = 0;
return $keys[0];
return undef;
}
sub NEXTKEY {
my $self = shift;
my $last = shift;
my @keys = $self->{cmod}->GetNVKeys;
if ($#keys < $self->{last}) {
$self->{last} = -1;
return undef
}
# Probably caller called delete on last key?
if ($last eq $keys[$self->{last}]) {
$self->{last}++
}
if ($#keys < $self->{last}) {
$self->{last} = -1;
return undef
}
return $keys[$self->{last}]
}
sub SCALAR {
my $self = shift;
my @keys = $self->{cmod}->GetNVKeys;
return $#keys + 1
}
package ZNC::Module;
sub description {
"< Placeholder for a description >"
}
sub wiki_page {
''
}
# Default implementations for module hooks. They can be overriden in derived modules.
sub OnLoad {1}
sub OnBoot {}
sub OnShutdown {}
sub WebRequiresLogin {}
sub WebRequiresAdmin {}
sub GetWebMenuTitle {}
sub OnWebPreRequest {}
sub OnWebRequest {}
sub GetSubPages {}
sub _GetSubPages { my $self = shift; $self->GetSubPages }
sub OnPreRehash {}
sub OnPostRehash {}
sub OnIRCDisconnected {}
sub OnIRCConnected {}
sub OnIRCConnecting {}
sub OnIRCConnectionError {}
sub OnIRCRegistration {}
sub OnBroadcast {}
sub OnChanPermission {}
sub OnOp {}
sub OnDeop {}
sub OnVoice {}
sub OnDevoice {}
sub OnMode {}
sub OnRawMode {}
sub OnRaw {}
sub OnStatusCommand {}
sub OnModCommand {}
sub OnModNotice {}
sub OnModCTCP {}
sub OnQuit {}
sub OnNick {}
sub OnKick {}
sub OnJoin {}
sub OnPart {}
sub OnChanBufferStarting {}
sub OnChanBufferEnding {}
sub OnChanBufferPlayLine {}
sub OnPrivBufferPlayLine {}
sub OnClientLogin {}
sub OnClientDisconnect {}
sub OnUserRaw {}
sub OnUserCTCPReply {}
sub OnUserCTCP {}
sub OnUserAction {}
sub OnUserMsg {}
sub OnUserNotice {}
sub OnUserJoin {}
sub OnUserPart {}
sub OnUserTopic {}
sub OnUserTopicRequest {}
sub OnCTCPReply {}
sub OnPrivCTCP {}
sub OnChanCTCP {}
sub OnPrivAction {}
sub OnChanAction {}
sub OnPrivMsg {}
sub OnChanMsg {}
sub OnPrivNotice {}
sub OnChanNotice {}
sub OnTopic {}
sub OnServerCapAvailable {}
sub OnServerCapResult {}
sub OnTimerAutoJoin {}
sub OnEmbeddedWebRequest {}
# Functions of CModule will be usable from perl modules.
our $AUTOLOAD;
sub AUTOLOAD {
my $name = $AUTOLOAD;
$name =~ s/^.*:://; # Strip fully-qualified portion.
my $sub = sub {
my $self = shift;
$self->{_cmod}->$name(@_)
};
no strict 'refs';
*{$AUTOLOAD} = $sub;
use strict 'refs';
goto &{$sub};
}
sub DESTROY {}
sub BeginNV {
die "Don't use BeginNV from perl modules, use GetNVKeys or NV instead!";
}
sub EndNV {
die "Don't use EndNV from perl modules, use GetNVKeys or NV instead!";
}
sub FindNV {
die "Don't use FindNV from perl modules, use GetNVKeys/ExistsNV or NV instead!";
}
sub NV {
my $self = shift;
$self->{_nv}
}
sub CreateTimer {
my $self = shift;
my $id = ZNC::Core::CreateUUID;
my %a = @_;
my $ctimer = ZNC::CreatePerlTimer(
$self->{_cmod},
$a{interval}//10,
$a{cycles}//1,
"perl-timer-$id",
$a{description}//'Just Another Perl Timer',
$id);
my $ptimer = {
_ctimer=>$ctimer,
_modid=>$self->GetPerlID
};
$self->{_ptimers}{$id} = $ptimer;
if (ref($a{task}) eq 'CODE') {
bless $ptimer, 'ZNC::Timer';
$ptimer->{job} = $a{task};
$ptimer->{context} = $a{context};
} else {
bless $ptimer, $a{task};
}
$ptimer;
}
sub _CallTimer {
my $self = shift;
my $id = shift;
my $t = $self->{_ptimers}{$id};
$t->RunJob;
}
sub _RemoveTimer {
my $self = shift;
my $id = shift;
say "Removing perl timer $id";
$self->{_ptimers}{$id}->OnShutdown;
delete $self->{_ptimers}{$id}
}
sub CreateSocket {
my $self = shift;
my $class = shift;
my $id = ZNC::Core::CreateUUID;
my $csock = ZNC::CreatePerlSocket($self->{_cmod}, $id);
my $psock = bless {
_csock=>$csock,
_modid=>$self->GetPerlID
}, $class;
$self->{_sockets}{$id} = $psock;
$psock->Init(@_);
$psock;
}
sub _CallSocket {
my $self = shift;
my $id = shift;
my $func = shift;
$self->{_sockets}{$id}->$func(@_)
}
sub _RemoveSocket {
my $self = shift;
my $id = shift;
say "Removing perl socket $id";
$self->{_sockets}{$id}->OnShutdown;
delete $self->{_sockets}{$id}
}
package ZNC::Timer;
sub GetModule {
my $self = shift;
$ZNC::Core::pmods{$self->{_modid}};
}
sub RunJob {
my $self = shift;
if (ref($self->{job}) eq 'CODE') {
&{$self->{job}}($self->GetModule, context=>$self->{context}, timer=>$self->{_ctimer});
}
}
sub OnShutdown {}
our $AUTOLOAD;
sub AUTOLOAD {
my $name = $AUTOLOAD;
$name =~ s/^.*:://; # Strip fully-qualified portion.
my $sub = sub {
my $self = shift;
$self->{_ctimer}->$name(@_)
};
no strict 'refs';
*{$AUTOLOAD} = $sub;
use strict 'refs';
goto &{$sub};
}
sub DESTROY {}
package ZNC::Socket;
sub GetModule {
my $self = shift;
$ZNC::Core::pmods{$self->{_modid}};
}
sub Init {}
sub OnConnected {}
sub OnDisconnected {}
sub OnTimeout {}
sub OnConnectionRefused {}
sub OnReadData {}
sub OnReadLine {}
sub OnAccepted {}
sub OnShutdown {}
sub _Accepted {
my $self = shift;
my $psock = $self->OnAccepted(@_);
return $psock->{_csock} if defined $psock;
return undef;
}
our $AUTOLOAD;
sub AUTOLOAD {
my $name = $AUTOLOAD;
$name =~ s/^.*:://; # Strip fully-qualified portion.
my $sub = sub {
my $self = shift;
$self->{_csock}->$name(@_)
};
no strict 'refs';
*{$AUTOLOAD} = $sub;
use strict 'refs';
goto &{$sub};
}
sub DESTROY {}
sub Connect {
my $self = shift;
my $host = shift;
my $port = shift;
my %arg = @_;
$self->GetModule->GetManager->Connect(
$host,
$port,
"perl-socket-".$self->GetPerlID,
$arg{timeout}//60,
$arg{ssl}//0,
$arg{bindhost}//'',
$self->{_csock}
);
}
sub Listen {
my $self = shift;
my %arg = @_;
my $addrtype = $ZNC::ADDR_ALL;
if (defined $arg{addrtype}) {
given ($arg{addrtype}) {
when (/^ipv4$/i) { $addrtype = $ZNC::ADDR_IPV4ONLY }
when (/^ipv6$/i) { $addrtype = $ZNC::ADDR_IPV6ONLY }
when (/^all$/i) { }
default { die "Specified addrtype [$arg{addrtype}] isn't supported" }
}
}
if (defined $arg{port}) {
return $arg{port} if $self->GetModule->GetManager->ListenHost(
$arg{port},
"perl-socket-".$self->GetPerlID,
$arg{bindhost}//'',
$arg{ssl}//0,
$arg{maxconns}//ZNC::_GetSOMAXCONN,
$self->{_csock},
$arg{timeout}//0,
$addrtype
);
return 0;
}
$self->GetModule->GetManager->ListenRand(
"perl-socket-".$self->GetPerlID,
$arg{bindhost}//'',
$arg{ssl}//0,
$arg{maxconns}//ZNC::_GetSOMAXCONN,
$self->{_csock},
$arg{timeout}//0,
$addrtype
);
}
1