summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog14
-rw-r--r--data/global.pl24
-rw-r--r--data/style.css78
-rw-r--r--lib/Multi/Anime.pm271
-rw-r--r--lib/Multi/Core.pm156
-rw-r--r--lib/Multi/IRC.pm667
-rw-r--r--lib/Multi/Image.pm228
-rw-r--r--lib/Multi/Maintenance.pm198
-rw-r--r--lib/Multi/RG.pm220
-rw-r--r--lib/Multi/Sitemap.pm149
-rw-r--r--lib/VNDB/DB/Users.pm54
-rw-r--r--lib/VNDB/DB/VN.pm25
-rw-r--r--lib/VNDB/Handler/Discussions.pm2
-rw-r--r--lib/VNDB/Handler/Misc.pm24
-rw-r--r--lib/VNDB/Handler/Producers.pm2
-rw-r--r--lib/VNDB/Handler/Releases.pm3
-rw-r--r--lib/VNDB/Handler/Tags.pm1
-rw-r--r--lib/VNDB/Handler/Users.pm11
-rw-r--r--lib/VNDB/Handler/VNEdit.pm11
-rw-r--r--lib/VNDB/Handler/VNPage.pm8
-rw-r--r--lib/VNDB/Util/Auth.pm93
-rw-r--r--lib/VNDB/Util/LayoutHTML.pm1
-rw-r--r--lib/VNDB/Util/Misc.pm50
-rw-r--r--static/f/forms.js2
-rw-r--r--static/f/icons.pngbin7422 -> 7526 bytes
-rwxr-xr-xutil/dbgraph.pl8
-rw-r--r--util/dump.sql234
-rwxr-xr-xutil/multi.pl57
-rw-r--r--util/updates/update_2.6.sql261
-rwxr-xr-xutil/vndb.pl8
30 files changed, 1536 insertions, 1324 deletions
diff --git a/ChangeLog b/ChangeLog
index 37bce03f..0e293f8b 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,17 @@
+2.6 - ?
+ - New screen resolutions: 1024x600 and 1600x1200
+ - Rewritten authentication system
+ - New language: Vietnamese
+ - Complete rewrite of Multi
+ - Asynchronous communication with PostgreSQL
+ - Got rid of the shared memory
+ - No more $self->multiCmd in the VNDB code
+ - Extended IRC bot functionality
+ - Tag cache regenerated daily rather than hourly
+ - Added OpenSearch plugin + autodetection
+ - Converted font size units to px in the css
+ - Added double-post prevention
+
2.5 - 2009-07-09
- Hide NSFW images in diff viewer (unless NSFW warnings are disabled)
- Display related boards in recent posts tooltip op homepage
diff --git a/data/global.pl b/data/global.pl
index 7f3ab7a2..a4cf9fbf 100644
--- a/data/global.pl
+++ b/data/global.pl
@@ -20,7 +20,7 @@ our %S = (%S,
site_title => 'Yet another VNDB clone',
skin_default => 'angel',
cookie_domain => '.vndb.org',
- cookie_key => 'any-private-string-here',
+ global_salt => 'any-private-string-here',
source_url => 'http://git.blicky.net/vndb.git/?h=master',
admin_email => 'contact@vndb.org',
sharedmem_key => 'VNDB',
@@ -51,6 +51,7 @@ our %S = (%S,
ru => q|Russian|,
sv => q|Swedish|,
tr => q|Turkish|,
+ vi => q|Vietnamese|,
zh => q|Chinese|,
},
producer_types => {
@@ -125,15 +126,15 @@ our %S = (%S,
}, 7 ],
},
anime_types => [
- # VNDB AniDB
- [ 'unknown', 'unknown', ],
- [ 'TV', 'TV Series' ],
- [ 'OVA', 'OVA' ],
- [ 'Movie', 'Movie' ],
- [ 'unknown', 'Other' ],
- [ 'unknown', 'Web' ],
- [ 'TV Special', 'TV Special' ],
- [ 'unknown', 'Music Video' ],
+ # AniDB anime type starts counting at 1, 0 = unknown
+ # we start counting at 0, with NULL being unknown
+ 'TV Series',
+ 'OVA',
+ 'Movie',
+ 'Other',
+ 'Web',
+ 'TV Special',
+ 'Music Video',
],
vn_relations => [
# Name, Reverse--
@@ -210,7 +211,9 @@ our %S = (%S,
[ '640x480 (480p)', '4:3' ],
[ '800x600', '4:3' ],
[ '1024x768', '4:3' ],
+ [ '1600x1200', '4:3' ],
[ '640x400', 'widescreen' ],
+ [ '1024x600', 'widescreen' ],
[ '1024x640', 'widescreen' ],
[ '1280x720 (720p)', 'widescreen' ],
[ '1920x1080 (1080p)', 'widescreen' ],
@@ -268,7 +271,6 @@ our %S = (%S,
# Multi-specific options (Multi also uses some options in %S and %O)
our %M = (
log_dir => $ROOT.'/data/log',
- log_level => 3, # 3: dbg, 2: wrn, 1: err
modules => {
RG => {},
Image => {},
diff --git a/data/style.css b/data/style.css
index dec6462b..e272ea7f 100644
--- a/data/style.css
+++ b/data/style.css
@@ -6,7 +6,7 @@
}
body, td {
- font: 8pt "Tahoma";
+ font: 11px "Tahoma";
}
body {
$_bodybg$;
@@ -47,7 +47,7 @@ img {
}
#header h1, #header h1 a {
font-family: "Futura", "Century New Gothic", "Arial", Serif;
- font-size: 19pt;
+ font-size: 23px;
font-style: italic;
border: none!important;
$_maintitle$
@@ -88,7 +88,7 @@ p.locked {
}
b.grayedout { font-weight: normal; color: $grayedout$ }
#maincontent h2 b {
- font: 8pt "Tahoma";
+ font: 10px "Tahoma";
font-weight: normal;
}
p.description {
@@ -136,7 +136,7 @@ input.text, input.submit, select, textarea {
background-color: $secbg$;
color: $maintext$;
border: 1px solid $secborder$;
- font: 9pt "Tahoma";
+ font: 11px "Tahoma";
margin: 1px;
}
optgroup option {
@@ -159,7 +159,7 @@ fieldset.submit input {
width: 200px;
}
fieldset.submit h2 {
- font-size: 9pt!important;
+ font-size: 11px!important;
}
fieldset.submit textarea {
margin: 0 20px 5px 20px;
@@ -203,11 +203,11 @@ td.field label {
#maincontent h1, #maincontent h2 {
font-family: "Futura", "Century New Gothic", "Arial", Serif;
font-weight: normal;
- font-size: 11pt;
+ font-size: 13px;
}
#maincontent h1 {
color: $boxtitle$;
- font-size: 14pt;
+ font-size: 16px;
margin: -5px 0 15px 0;
}
#maincontent h2.alttitle {
@@ -232,7 +232,7 @@ td.field label {
}
#maincontent h2 {
font-weight: bold;
- font-size: 11pt;
+ font-size: 13px;
margin: 10px 0 0 5px;
}
p.center {
@@ -268,7 +268,7 @@ b.future {
padding: 1px 3px;
}
#menulist h2, #menulist h2 a {
- font-size: 8pt;
+ font-size: 11px;
color: $maintext$;
}
#menulist dt {
@@ -374,11 +374,11 @@ b.future {
}
#maincontent .mainbox.threelayout h1 {
margin: -3px 0 1px 0;
- font-size: 12pt;
+ font-size: 14px;
font-weight: bold;
}
#maincontent .mainbox.threelayout h2 {
- font-size: 10pt;
+ font-size: 12px;
margin-top: 3px;
}
#maincontent .mainbox.threelayout a.right {
@@ -514,7 +514,7 @@ div.thread i.deleted {
}
div.thread i.lastmod {
float: right;
- font-size: 7pt;
+ font-size: 10px;
color: $grayedout$;
margin: 0 -10px -5px 0;
}
@@ -547,7 +547,7 @@ div.discussions td.tc4 { width: 170px; }
font-weight: bold;
font-style: italic;
color: $grayedout$;
- font-size: 13pt;
+ font-size: 17px;
margin: 20px 0 -20px 0;
}
@@ -570,7 +570,7 @@ div.vnimg i {
display: block;
width: 100%;
text-align: center;
- font-size: 7pt;
+ font-size: 10px;
}
div.vnimg p {
text-align: center;
@@ -615,7 +615,7 @@ div.vndetails td.relations dd {
margin-left: 15px;
}
div.vndetails td.anime b {
- font-size: 6pt;
+ font-size: 8px;
font-weight: normal;
padding-right: 4px;
}
@@ -682,7 +682,6 @@ a.addnew {
margin: 2px;
text-align: center;
}
-#screenshots td.scr div i { font-size: 7pt; }
#screenshots td.scr img { border: 3px solid transparent; }
#screenshots td.scr div.nsfw img { border: 3px solid $statnok$; }
#screenshots td.scr a:hover img { border: 3px solid $border$; }
@@ -723,47 +722,6 @@ a.addnew {
/***** VN edit *****/
-#jt_box_categories ul {
- list-style-type: none;
- padding: 0;
- margin: 0;
-}
-#jt_box_categories li {
- display: block;
- width: 170px;
- float: left;
- clear: none;
- font-weight: bold;
-}
-#jt_box_categories li li {
- display: list-item;
- width: auto;
- float: none;
- clear: left;
- font-weight: normal;
- padding: 0 0 0 20px;
- margin: 0;
- cursor: pointer;
- list-style-type: none;
- padding: 0;
-}
-#maincontent #jt_box_categories li li a {
- color: $grayedout$;
-}
-a.help {
- vertical-align: super;
- font-size: 6pt;
- text-decoration: none;
- font-weight: bold;
- padding-left: 1px;
-}
-#maincontent #jt_box_categories li li a {
- text-decoration: none;
- display: block;
- width: 160px;
- border: none
-}
-
#jt_box_relations table { margin-bottom: 10px; }
#jt_box_relations h2 { margin: 0 0 3px 0px; }
#jt_box_relations td { padding: 1px 2px; vertical-align: middle; }
@@ -1085,7 +1043,7 @@ div.warning li, div.notice li {
margin-left: 20px;
}
#maincontent div.warning h2, #maincontent div.notice h2 {
- font-size: 9pt;
+ font-size: 11px;
font-weight: bold;
margin: 0;
}
@@ -1155,7 +1113,7 @@ div#iv_view {
padding: 5px;
text-align: center;
}
-#iv_view a { border: 0; font-weight: bold; font-size: 10pt; }
+#iv_view a { border: 0; font-weight: bold; font-size: 12px }
#iv_view img { cursor: pointer }
#ivclose { float: right; padding-left: 10px }
#ivnext { padding-left: 5px; }
@@ -1226,6 +1184,8 @@ a .icons { cursor: pointer }
.icons.msx { background-position: -32px -56px; }
.icons.nes { background-position: -32px -70px; }
+.icons.vi { background-position: -34px -88px; }
+
.icons.cs { background-position: -48px 0px; }
.icons.da { background-position: -48px -11px; }
.icons.de { background-position: -48px -22px; }
diff --git a/lib/Multi/Anime.pm b/lib/Multi/Anime.pm
index 1215a4a1..03134925 100644
--- a/lib/Multi/Anime.pm
+++ b/lib/Multi/Anime.pm
@@ -8,7 +8,6 @@ package Multi::Anime;
use strict;
use warnings;
use POE 'Wheel::UDP', 'Filter::Stream';
-use Tie::ShareLite ':lock';
use Socket 'inet_ntoa';
use Time::HiRes 'time';
@@ -26,48 +25,45 @@ sub BANNED () { 555 }
sub ANIDB_OUT_OF_SERVICE () { 601 }
sub SERVER_BUSY () { 602 }
-my @expected_codes = ( TIMEOUT, LOGIN_ACCEPTED, LOGIN_ACCEPTED_NEW_VER, ANIME, NO_SUCH_ANIME, NOT_LOGGED_IN, LOGIN_FIRST, INVALID_SESSION );
+my @handled_codes = (
+ TIMEOUT, LOGIN_ACCEPTED, LOGIN_ACCEPTED_NEW_VER, ANIME, NO_SUCH_ANIME, NOT_LOGGED_IN,
+ LOGIN_FIRST,CLIENT_BANNED, INVALID_SESSION, BANNED, ANIDB_OUT_OF_SERVICE, SERVER_BUSY
+);
-sub spawn {
- # The 'anime' command doesn't actually do anything, it just
- # adds IDs to process to the internal queue, which is seperate
- # from the global processing queue.
- # This module -only- fetches anime information in daemon mode!
- # Calling the anime command with an ID as argument will force
- # the information to be refreshed. This is not recommended,
- # just use 'anime' for normal usage.
+sub spawn {
my $p = shift;
+ my %o = @_;
+
+ die "No AniDB user/pass configured!" if !$o{user} || !$o{pass};
+
+ my $addr = delete($o{PeerAddr}) || 'api.anidb.info';
+ $addr = gethostbyname($addr) or die "Couldn't resolve domain";
+ $addr = inet_ntoa($addr);
+
POE::Session->create(
package_states => [
- $p => [qw| _start shutdown cmd_anime nextcmd receivepacket updateanime |],
+ $p => [qw| _start shutdown check_anime fetch_anime nextcmd receivepacket |],
],
heap => {
- # POE::Wheels::UDP options
+ # POE::Wheels::UDP options
LocalAddr => '0.0.0.0',
LocalPort => 9000,
- PeerAddr => do {
- if(!$Multi::DAEMONIZE) {
- my $a = gethostbyname('api.anidb.info');
- die "ERROR: Couldn't resolve domain" if !defined $a;
- inet_ntoa($a);
- } else {
- 0;
- }
- },
+ PeerAddr => $addr,
PeerPort => 9000,
- # AniDB UDP API options
+ # AniDB UDP API options
client => 'multi',
clientver => 1,
- # Misc settings
+ # Misc settings
msgdelay => 10,
timeout => 30,
timeoutdelay => 0.4, # $delay = $msgdelay ^ (1 + $tm*$timeoutdelay)
maxtimeoutdelay => 2*3600, # two hours
- cachetime => 30*24*3600, # one month
+ check_delay => 3600, # one hour
+ cachetime => '1 month',
- @_,
+ %o,
w => undef,
s => '', # session key, '' = not logged in
tm => 0, # number of repeated timeouts
@@ -81,79 +77,55 @@ sub spawn {
sub _start {
$_[KERNEL]->alias_set('anime');
- $_[KERNEL]->call(core => register => qr/^anime(?: ([0-9]+))?$/, 'cmd_anime');
-
- # check for anime twice a day
- $_[KERNEL]->post(core => addcron => '0 0,12 * * *', 'anime');
- $_[KERNEL]->sig('shutdown' => 'shutdown');
-
- if(!$Multi::DAEMONIZE) {
- # init the UDP 'connection'
- $_[HEAP]{w} = POE::Wheel::UDP->new(
- (map { $_ => $_[HEAP]{$_} } qw| LocalAddr LocalPort PeerAddr PeerPort |),
- InputEvent => 'receivepacket',
- Filter => POE::Filter::Stream->new(),
- );
+ $_[KERNEL]->sig(shutdown => 'shutdown');
- # start executing commands
- $_[KERNEL]->delay(nextcmd => 0); #$_[HEAP]{msgdelay});
- }
+ # listen for 'anime' notifies
+ $_[KERNEL]->post(pg => listen => anime => 'check_anime');
+
+ # init the UDP 'connection'
+ $_[HEAP]{w} = POE::Wheel::UDP->new(
+ (map { $_ => $_[HEAP]{$_} } qw| LocalAddr LocalPort PeerAddr PeerPort |),
+ InputEvent => 'receivepacket',
+ Filter => POE::Filter::Stream->new(),
+ );
+
+ # look for something to do
+ $_[KERNEL]->yield('check_anime');
}
sub shutdown {
undef $_[HEAP]{w};
+ $_[KERNEL]->post(pg => unlisten => 'anime');
+ $_[KERNEL]->delay('check_anime');
$_[KERNEL]->delay('nextcmd');
$_[KERNEL]->delay('receivepacket');
+ $_[KERNEL]->alias_remove('anime');
}
-sub cmd_anime { # cmd, arg
- my @push;
- if(!$_[ARG1]) {
- # only animes we have never fetched, or haven't been updated for a month
- my $q = $Multi::SQL->prepare(q|
- SELECT id
- FROM anime
- WHERE lastfetch < ?
- AND lastfetch <> -1|);
- $q->execute(int(time-$_[HEAP]{cachetime}));
- push @push, map $_->[0], @{$q->fetchall_arrayref([])};
- $_[KERNEL]->call(core => log => 2, 'All anime info is up-to-date!') if !@push;
- } else {
- push @push, $_[ARG1];
- }
+sub check_anime {
+ return if $_[HEAP]{aid};
+ $_[KERNEL]->delay('check_anime');
+ $_[KERNEL]->post(pg => query => 'SELECT id FROM anime WHERE lastfetch IS NULL OR lastfetch < NOW() - ?::interval LIMIT 1',
+ [ $_[HEAP]{cachetime} ], 'fetch_anime');
+}
- if(@push) {
- my $s = tie my %s, 'Tie::ShareLite', -key => $VNDB::S{sharedmem_key}, -create => 'yes', -destroy => 'no', -mode => 0666;
- $s->lock(LOCK_EX);
- my @q = $s{anime} ? @{$s{anime}} : ();
- push @q, grep {
- my $ia = $_;
- !(scalar grep $ia == $_, @q)
- } @push;
- $s{anime} = \@q;
- $s->unlock();
- }
- $_[KERNEL]->post(core => finish => $_[ARG0]);
+sub fetch_anime { # num, res
+ # nothing to do, check again later
+ return $_[KERNEL]->delay('check_anime', $_[HEAP]{check_delay}) if $_[ARG0] == 0;
+
+ # otherwise, fetch info (if we aren't doing so already)
+ return if $_[HEAP]{aid};
+ $_[HEAP]{aid} = $_[ARG1][0]{id};
+ $_[KERNEL]->yield('nextcmd');
}
sub nextcmd {
- return if $_[HEAP]{lm};
-
- my $s = tie my %s, 'Tie::ShareLite', -key => $VNDB::S{sharedmem_key}, -create => 'yes', -destroy => 'no', -mode => 0666;
- my @q = $s{anime} ? @{$s{anime}} : ();
- undef $s;
-
- if(!@q) { # nothing to do...
- $_[KERNEL]->delay(nextcmd => $_[HEAP]{msgdelay});
- return;
- }
my %cmd;
-
- # not logged in, get a session
+ # not logged in, get a session
if(!$_[HEAP]{s}) {
%cmd = (
command => 'AUTH',
@@ -164,21 +136,19 @@ sub nextcmd {
clientver => $_[HEAP]{clientver},
enc => 'UTF-8',
);
- $_[KERNEL]->call(core => log => 3, 'Authenticating with AniDB...');
+ $_[KERNEL]->call(core => log => 'Authenticating with AniDB...');
}
-
- # logged in, get anime
+ # logged in, get anime
else {
- $_[HEAP]{aid} = $q[0];
%cmd = (
command => 'ANIME',
- aid => $q[0],
+ aid => $_[HEAP]{aid},
acode => 3973121, # aid, ANN id, NFO id, year, type, romaji, kanji
);
- $_[KERNEL]->call(core => log => 3, 'Fetching info for a%d', $q[0]);
+ $_[KERNEL]->call(core => log => 'Fetching info for a%d', $_[HEAP]{aid});
}
- # send command
+ # send command
my $cmd = delete $cmd{command};
$cmd{tag} = ++$_[HEAP]{tag};
$cmd{s} = $_[HEAP]{s} if $_[HEAP]{s};
@@ -189,111 +159,88 @@ sub nextcmd {
} keys %cmd);
$_[HEAP]{w}->put({ payload => [ $cmd ]});
- #$_[KERNEL]->call(core => log => 3, '> %s', $cmd);
$_[KERNEL]->delay(receivepacket => $_[HEAP]{timeout}, { payload => [ $_[HEAP]{tag}.' 100 TIMEOUT' ] });
$_[HEAP]{lm} = time;
}
sub receivepacket { # input, wheelid
- $_[KERNEL]->delay('receivepacket'); # disable the timeout
+ # parse message
my @r = split /\n/, $_[ARG0]{payload}[0];
- my $delay = $_[HEAP]{msgdelay};
-
my($tag, $code, $msg) = ($1, $2, $3) if $r[0] =~ /^([0-9]+) ([0-9]+) (.+)$/;
- if(!grep $_ == $code, @expected_codes) {
- $_[KERNEL]->call(core => log => 1, "Received an unexpected reply after %.2fs!\n < %s",
- time-$_[HEAP]{lm}, join("\n < ", @r));
- } else {
- $_[KERNEL]->call(core => log => 3, 'Received from AniDB after %.2fs: %d %s',
- time-$_[HEAP]{lm}, $code, $msg);
- }
+ # log
+ $_[KERNEL]->call(core => log => 'Received from AniDB after %.2fs: %d %s',
+ time-$_[HEAP]{lm}, $code, $msg);
- # just handle anime data, even if the tag is not correct
- if($code == ANIME) {
- $_[KERNEL]->yield(updateanime => $_[HEAP]{aid}, $r[1]);
- }
+ # tag incorrect, ignore message
+ return $_[KERNEL]->call(core => log => 'Ignoring incorrect tag')
+ if $tag != $_[HEAP]{tag};
- # tag incorrect, ignore message
- if($tag != $_[HEAP]{tag}) {
- $_[KERNEL]->call(core => log => 3, 'Ignoring incorrect tag') if $code != ANIME;
- return;
- }
+ # unhandled code, ignore as well
+ return $_[KERNEL]->call(core => log => 'Ignoring unhandled code')
+ if !grep $_ == $code, @handled_codes;
+
+ # at this point, we have a message we can handle, so disable the timeout
+ $_[KERNEL]->delay('receivepacket');
+ $_[HEAP]{lm} = 0;
- # try again later
+ # received a timeout of some sorts, try again later
if($code == TIMEOUT || $code == CLIENT_BANNED || $code == BANNED || $code == ANIDB_OUT_OF_SERVICE || $code == SERVER_BUSY) {
$_[HEAP]{tm}++;
- $delay = $_[HEAP]{msgdelay}**(1 + $_[HEAP]{tm}*$_[HEAP]{timeoutdelay});
+ my $delay = $_[HEAP]{msgdelay}**(1 + $_[HEAP]{tm}*$_[HEAP]{timeoutdelay});
$delay = $_[HEAP]{maxtimeoutdelay} if $delay > $_[HEAP]{maxtimeoutdelay};
- $_[KERNEL]->call(core => log => 1, 'Delaying %.0fs.', $delay);
+ $_[KERNEL]->call(core => log => 'Delaying %.0fs.', $delay);
+ return $_[KERNEL]->delay(nextcmd => $_[HEAP]{msgdelay});
}
- # oops, wrong id
- if($code == NO_SUCH_ANIME) {
- $_[KERNEL]->yield(updateanime => $_[HEAP]{aid}, 'notfound');
- }
+ # message wasn't a timeout, reset timeout counter
+ $_[HEAP]{tm} = 0;
- # ok, we have a session now
- if($code == LOGIN_ACCEPTED || $code == LOGIN_ACCEPTED_NEW_VER) {
- $_[HEAP]{s} = $1 if $msg =~ /^\s*([a-zA-Z0-9]{4,8}) /;
- }
-
- # oops, we should've logged in, get a new session
+ # our session isn't valid, discard it and call nextcmd to get a new one
if($code == NOT_LOGGED_IN || $code == LOGIN_FIRST || $code == INVALID_SESSION) {
$_[HEAP]{s} = '';
+ return $_[KERNEL]->delay(nextcmd => $_[HEAP]{msgdelay});
}
- $_[HEAP]{lm} = $_[HEAP]{aid} = 0;
- $_[HEAP]{tm} = 0 if $delay == $_[HEAP]{msgdelay};
- $_[KERNEL]->delay(nextcmd => $delay);
-}
-
-
-sub updateanime { # aid, data|'notfound'
- # aid, ANN id, NFO id, year, type, romaji, kanji, lastfetch
- my @col = $_[ARG1] eq 'notfound'
- ? ($_[ARG0], 0, 0, 0, 0, '', '', -1)
- : (split(/\|/, $_[ARG1], 7), int time);
+ # we received a session ID, call nextcmd again to fetch anime info
+ if($code == LOGIN_ACCEPTED || $code == LOGIN_ACCEPTED_NEW_VER) {
+ $_[HEAP]{s} = $1 if $msg =~ /^\s*([a-zA-Z0-9]{4,8}) /;
+ return $_[KERNEL]->delay(nextcmd => $_[HEAP]{msgdelay});
+ }
- if($col[7] > 0) {
+ # we now know something about the anime we requested, update DB
+ if($code == NO_SUCH_ANIME) {
+ $_[KERNEL]->call(core => log => 'ERROR: No anime found with id = %d', $_[HEAP]{aid});
+ $_[KERNEL]->post(pg => do => 'UPDATE anime SET lastfetch = NOW() WHERE id = ?', [ $_[HEAP]{aid} ]);
+ } else {
+ # aid, ANN id, NFO id, year, type, romaji, kanji
+ my @col = split(/\|/, $r[1], 7);
for (@col) {
$_ =~ s/<br \/>/\n/g;
$_ =~ s/`/'/g;
}
+ $col[1] = undef if !$col[1];
+ $col[2] = undef if !$col[2] || $col[2] =~ /^0,/;
$col[3] = $1 if $col[3] =~ /^([0-9]+)/; # remove multi-year stuff
- for(0..$#{$VNDB::S{anime_types}}) {
- $col[4] = $_ if lc($VNDB::S{anime_types}[$_][1]) eq lc($col[4]);
- }
- $col[4] = 0 if $col[4] !~ /^[0-9]+$/;
- $col[2] = '' if $col[2] =~ /^0,/;
+ $col[3] = undef if !$col[3];
+ $col[4] = (grep lc($VNDB::S{anime_types}[$_]) eq lc($col[4]), 0..$#{$VNDB::S{anime_types}})[0];
+ $col[5] = undef if !$col[5];
+ $col[6] = undef if !$col[6];
+ $_[KERNEL]->post(pg => do => 'UPDATE anime
+ SET id = ?, ann_id = ?, nfo_id = ?, year = ?, type = ?,
+ title_romaji = ?,title_kanji = ?, lastfetch = NOW()
+ WHERE id = ?',
+ [ @col, $_[HEAP]{aid} ]
+ );
+ $_[KERNEL]->call(core => log => 'Updated anime info for a%d', $_[HEAP]{aid});
+ $_[KERNEL]->call(core => log => 'ERROR: a%d doesn\'t have a title or year!', $_[HEAP]{aid})
+ if !$col[3] || !$col[5];
}
- # try to UPDATE first
- my $r = $Multi::SQL->do(q|
- UPDATE anime
- SET id = ?, ann_id = ?, nfo_id = ?, year = ?, type = ?,
- title_romaji = ?, title_kanji = ?, lastfetch = ?
- WHERE id = ?|,
- undef, @col, $col[0]);
-
- # fall back to INSERT when nothing was updated
- $Multi::SQL->do(q|
- INSERT INTO anime
- (id, ann_id, nfo_id, year, type, title_romaji, title_kanji, lastfetch)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)|,
- undef, @col) if $r < 1;
-
- # remove from queue
- my $s = tie my %s, 'Tie::ShareLite', -key => $VNDB::S{sharedmem_key}, -create => 'yes', -destroy => 'no', -mode => 0666;
- $s->lock(LOCK_EX);
- my @q = grep $_ != $_[ARG0], ($s{anime} ? @{$s{anime}} : ());
- $s{anime} = \@q;
- $s->unlock();
-
- $col[7] > 0
- ? $_[KERNEL]->post(core => log => 2, 'Updated anime info for a%d', $col[0])
- : $_[KERNEL]->post(core => log => 1, 'Anime a%d not found!', $col[0]);
+ # this anime is handled, check for more
+ $_[HEAP]{aid} = 0;
+ $_[KERNEL]->delay(check_anime => $_[HEAP]{msgdelay});
}
diff --git a/lib/Multi/Core.pm b/lib/Multi/Core.pm
index 7b2ae24b..c40be430 100644
--- a/lib/Multi/Core.pm
+++ b/lib/Multi/Core.pm
@@ -7,134 +7,88 @@ package Multi::Core;
use strict;
use warnings;
-use POE 'Component::Cron';
-use Tie::ShareLite ':lock';
-use Time::HiRes 'time', 'gettimeofday', 'tv_interval'; # overload time()
-use DateTime::Event::Cron; # bug in PoCo::Cron (rt #35422, fixed in 0.019)
+use POE;
+use POE::Component::Pg;
+use DBI;
-sub spawn {
+sub run {
my $p = shift;
+
+ # spawn our SQL handling session
+ my @db = @{$VNDB::O{db_login}};
+ my(@dsn) = DBI->parse_dsn($db[0]);
+ $dsn[2] = ($dsn[2]?$dsn[2].',':'').'pg_enable_utf8=>1';
+ $db[0] = "$dsn[0]:$dsn[1]($dsn[2]):$dsn[4]";
+ POE::Component::Pg->spawn(alias => 'pg', dsn => $db[0], user => $db[1], password => $db[2]);
+
+ # spawn the core session (which handles logging & external signals)
POE::Session->create(
package_states => [
- $p => [qw| _start register addcron heartbeat queue prepare execute finish log cmd_exit |],
+ $p => [qw| _start log pg_error sig_shutdown shutdown |],
],
- heap => { cron => [], cmds => [], running => 0, starttime => 0 },
);
-}
-
-
-sub _start {
- $_[KERNEL]->alias_set('core');
- $_[KERNEL]->call(core => register => qr/^(exit|reload)$/, 'cmd_exit');
- $_[KERNEL]->yield(queue => $_) for (grep !/^-/, @ARGV);
- $_[KERNEL]->yield(heartbeat => time) if $Multi::DAEMONIZE != 1;
- $_[KERNEL]->yield('prepare');
-}
-
-
-sub register { # regex, state
- push @{$_[HEAP]{cmds}}, [ $_[ARG0], $_[SENDER], $_[ARG1] ];
- (my $p = $_[SENDER][2]{$_[CALLER_STATE]}[0]) =~ s/^Multi:://; # NOT PORTABLE
- $_[KERNEL]->call(core => log => 3, "Command '%s' handled by %s::%s", $_[ARG0], $p, $_[ARG1]);
-}
-
-
-sub addcron { # cronline, cmd
- return if $Multi::DAEMONIZE; # no cronjobs when we aren't a daemon!
- push @{$_[HEAP]{cron}}, POE::Component::Cron->from_cron($_[ARG0], $_[SESSION], queue => $_[ARG1]);
- $_[KERNEL]->call(core => log => 3, "Added cron: %s %s", $_[ARG0], $_[ARG1]);
-}
-
-
-sub heartbeat { # last beat
- $_[KERNEL]->yield('prepare');
- $_[KERNEL]->call(core => log => 1, 'Heartbeat took %.2fs, possible block', time-$_[ARG0])
- if time > $_[ARG0]+3;
- $_[KERNEL]->delay(heartbeat => 1, time) if $Multi::DAEMONIZE == 0;
-}
-
-sub queue { # cmd
- my $s = tie my %s, 'Tie::ShareLite', -key => $VNDB::S{sharedmem_key}, -create => 'yes', -destroy => 'no', -mode => 0666;
- $s->lock(LOCK_EX);
- my @q = ( ($s{queue} ? @{$s{queue}} : ()), $_[ARG0] );
- $s{queue} = \@q;
- $s->unlock();
+ # log warnings
+ $SIG{__WARN__} = sub {(local$_=shift)=~s/\r?\n//;$poe_kernel->call(core=>log=>'__WARN__: '.$_)};
- $_[KERNEL]->call(core => log => 3, "Queuing '%s'.", $_[ARG0]);
- $_[KERNEL]->yield('prepare');
+ $poe_kernel->run();
}
-sub prepare { # determines whether to execute a new cmd
- return if $Multi::STOP || $_[HEAP]{running};
-
- my $s = tie my %s, 'Tie::ShareLite', -key => $VNDB::S{sharedmem_key}, -create => 'yes', -destroy => 'no', -mode => 0666;
- $s->lock(LOCK_SH);
- if($s{queue} && @{$s{queue}}) {
- $_[KERNEL]->yield(execute => $s{queue}[0]);
- $_[HEAP]{running} = 1;
+sub _start {
+ $_[KERNEL]->alias_set('core');
+ $_[KERNEL]->call(core => log => 'Starting Multi '.$VNDB::S{version});
+ $_[KERNEL]->post(pg => register => error => 'pg_error');
+ $_[KERNEL]->post(pg => 'connect');
+ $_[KERNEL]->sig(INT => 'sig_shutdown');
+ $_[KERNEL]->sig(TERM => 'sig_shutdown');
+ $_[KERNEL]->sig('shutdown', 'shutdown');
+
+ # dynamically load and spawn modules
+ for (keys %{$VNDB::M{modules}}) {
+ my($mod, $args) = ($_, $VNDB::M{modules}{$_});
+ next if !$args || ref($args) ne 'HASH';
+ require "Multi/$mod.pm";
+ # I'm surprised the strict pagma isn't complaining about this
+ "Multi::$mod"->spawn(%$args);
}
- $s->unlock();
}
-sub execute { # cmd
- $_[HEAP]{starttime} = [ gettimeofday ];
- my $cmd = (grep { $_[ARG0] =~ /$_->[0]/ } @{$_[HEAP]{cmds}})[0];
- if(!$cmd) {
- $_[KERNEL]->call(core => log => 1, 'Unknown cmd: %s', $_[ARG0]);
- $_[KERNEL]->yield(finish => $_[ARG0]);
- return;
- }
- $_[KERNEL]->call(core => log => 2, 'Executing cmd: %s', $_[ARG0]);
- $_[ARG0] =~ /$cmd->[0]/; # determine arguments (see perlvar for the magic)
- my @arg = $#- ? map { substr $_[ARG0], $-[$_], $+[$_]-$-[$_] } 1..$#- : ();
- $_[KERNEL]->post($cmd->[1] => $cmd->[2], $_[ARG0], @arg);
-}
-
+sub log { # level, msg
+ (my $p = eval { $_[SENDER][2]{$_[CALLER_STATE]}[0] } || '') =~ s/^Multi:://;
+ my $msg = sprintf '%s::%s: %s', $p, $_[CALLER_STATE],
+ $_[ARG1] ? sprintf($_[ARG0], @_[ARG1..$#_]) : $_[ARG0];
-sub finish { # cmd
- $_[HEAP]{running} = 0;
- $_[KERNEL]->call(core => log => 2, "Unqueuing '%s' after %.2fs.",
- $_[ARG0], tv_interval($_[HEAP]{starttime}));
+ open(my $F, '>>', $VNDB::M{log_dir}.'/multi.log');
+ printf "[%s] %s\n", scalar localtime, $msg;
+ close $F;
+}
- my $s = tie my %s, 'Tie::ShareLite', -key => $VNDB::S{sharedmem_key}, -create => 'yes', -destroy => 'no', -mode => 0666;
- $s->lock(LOCK_EX);
- my @q = grep { $_ ne $_[ARG0] } $s{queue} ? @{$s{queue}} : ();
- $s{queue} = \@q;
- $s->unlock();
- $_[KERNEL]->yield('prepare');
+sub pg_error { # ARG: command, errmsg, [ query, params, orig_session, event-args ]
+ my $s = $_[ARG2] ? sprintf ' (Session: %s, Query: "%s", Params: %s, Args: %s)',
+ join(', ', $_[KERNEL]->alias_list($_[ARG4])), $_[ARG2],
+ join(', ', $_[ARG3] ? map qq|"$_"|, @{$_[ARG3]} : '[none]'), $_[ARG5]||'' : '';
+ $_[KERNEL]->call(core => log => 'SQL Error for command %s: %s %s', $_[ARG0], $_[ARG1], $s);
}
-sub log { # level, msg
- return if $_[ARG0] > $VNDB::M{log_level};
-
- (my $p = eval { $_[SENDER][2]{$_[CALLER_STATE]}[0] } || '') =~ s/^Multi:://;
- my $msg = sprintf '(%s) %s::%s: %s',
- (qw|WRN ACT DBG|)[$_[ARG0]-1], $p, $_[CALLER_STATE],
- $_[ARG2] ? sprintf($_[ARG1], @_[ARG2..$#_]) : $_[ARG1];
-
- open(my $F, '>>', $VNDB::M{log_dir}.'/multi.log');
- printf $F "[%s] %s\n", scalar localtime, $msg;
- close $F;
+sub sig_shutdown {
+ # Multi modules should listen to the shutdown signal (but should never call sig_handled() on it!)
+ $_[KERNEL]->signal($_[SESSION], 'shutdown', 'SIG'.$_[ARG0]);
+ # consider this event as handled, so our process won't be killed directly
+ $_[KERNEL]->sig_handled();
}
-sub cmd_exit {
- $Multi::STOP = $_[ARG0] eq 'reload' ? 2 : 1;
- $_[KERNEL]->call(core => finish => $_[ARG0]);
- $_[KERNEL]->call(core => log => 2, 'Exiting...');
-
- $_[KERNEL]->delay('heartbeat'); # stop the heartbeats
- $_->delete() for (@{$_[HEAP]{cron}}); # stop scheduling cron jobs
- $_[KERNEL]->signal($_[KERNEL], 'shutdown'); # Broadcast to other sessions
+sub shutdown {
+ $_[KERNEL]->call(core => log => 'Shutting down (%s)', $_[ARG1]);
+ $_[KERNEL]->post(pg => 'shutdown');
+ $_[KERNEL]->alias_remove('core');
}
1;
-
diff --git a/lib/Multi/IRC.pm b/lib/Multi/IRC.pm
index 02be04fb..2373178b 100644
--- a/lib/Multi/IRC.pm
+++ b/lib/Multi/IRC.pm
@@ -15,310 +15,288 @@ use POE qw|
|;
use POE::Component::IRC::Common ':ALL';
use URI::Escape 'uri_escape_utf8';
-use Net::HTTP;
use constant {
- ARG => ARG0,
+ USER => ARG0,
DEST => ARG1,
- NICK => ARG2
+ ARG => ARG2,
+ MASK => ARG3,
+
+ # long subquery used in several places
+ GETBOARDS => q{array_to_string(array(
+ SELECT tb.type||COALESCE(':'||COALESCE(u.username, vr.title, pr.name), '')
+ FROM threads_boards tb
+ LEFT JOIN vn v ON tb.type = 'v' AND v.id = tb.iid
+ LEFT JOIN vn_rev vr ON vr.id = v.latest
+ LEFT JOIN producers p ON tb.type = 'p' AND p.id = tb.iid
+ LEFT JOIN producers_rev pr ON pr.id = p.latest
+ LEFT JOIN users u ON tb.type = 'u' AND u.id = tb.iid
+ WHERE tb.tid = t.id
+ ORDER BY tb.type, tb.iid
+ ), ', ') AS boards},
};
+my $irc;
-sub spawn {
- return if $Multi::DAEMONIZE != 0; # we don't provide any commands, after all
+sub spawn {
my $p = shift;
- my $irc = POE::Component::IRC::State->spawn(
+ $irc = POE::Component::IRC::State->spawn(
alias => 'circ',
NoDNS => 1,
);
POE::Session->create(
package_states => [
$p => [qw|
- _start irc_001 irc_public irc_ctcp_action irc_msg irccmd vndbid ircnotify shutdown
- cmd_info cmd_vndb cmd_list cmd_vn cmd_uptime cmd_notifications cmd_me cmd_say cmd_cmd cmd_eval
+ _start shutdown throttle_gc irc_001 irc_public irc_ctcp_action irc_msg
+ command idlequote reply notify
+ cmd_info cmd_list cmd_uptime cmd_vn cmd_vn_results cmd_p cmd_p_results cmd_quote cmd_quote_result
+ cmd_say cmd_me cmd_notifications cmd_eval cmd_die cmd_post vndbid formatid
|],
],
- heap => { irc => $irc,
- o => {
- user => 'Multi_test'.$$,
- server => 'irc.synirc.net',
- ircname => 'VNDB.org Multi',
- channel => [ '#vndb' ],
- @_
+ heap => {
+ nick => 'Multi_test'.$$,
+ server => 'irc.synirc.net',
+ ircname => 'VNDB.org Multi',
+ channels => [ '#vndb' ],
+ masters => [ 'yorhel!*@*' ],
+ @_,
+ throttle => {},
+ idlequotes => {},
+ notify => {},
+ lastrev => time, # using Multi's time for comparing to the DB time can fail
+ lastpost => time, # in some rare cases, but I doubt it'll be a problem.
+ lasttag => time,
+ commands => {
+ info => 0, # argument = authentication level/flags,
+ list => 0, # 0: everyone,
+ uptime => 0, # 1: only OPs in the first channel listed in @channels
+ vn => 0, # 2: only users matching the mask in @masters
+ p => 0, # |8: has to be addressed to the bot (e.g. 'Multi: eval' instead of '!eval')
+ quote => 0,
+ say => 1|8,
+ me => 1|8,
+ notifications => 1,
+ eval => 2|8,
+ die => 2|8,
+ post => 2|8,
},
- log => {},
- privpers => {},
- notify => [],
}
);
}
+# non-POE helper function
+# Arguments: $_[HEAP], key, timeout, (optional) num
+# no key = remove all keys with no activity in the last hour
+# returns false if throttling isn't necessary for that key
+sub throttle {
+ my($heap, $key, $tm, $num) = @_;
+
+ # garbage collect
+ return ($heap->{throttle} = {
+ map $_->[$#$_] > time-3600 ? ($_, $heap->{throttle}{$_}) : (), keys %{$heap->{throttle}}
+ }) if !$key;
+
+ $num ||= 1;
+ my $dat = $heap->{throttle};
+ if(!$dat->{$key}) {
+ $dat->{$key} = [ time ];
+ return 0;
+ }
+ $dat->{$key} = [ grep $_ > time-$tm, @{$dat->{$key}} ];
+ return 1 if @{$dat->{$key}} >= $num;
+ push @{$dat->{$key}}, time;
+ return 0;
+}
+
+
sub _start {
$_[KERNEL]->alias_set('irc');
- $_[KERNEL]->call(core => register => qr/^ircnotify ([vrptg][0-9]+(?:\.[0-9]+)?)$/, 'ircnotify');
- $_[HEAP]{irc}->plugin_add(
+ $irc->plugin_add(
Logger => POE::Component::IRC::Plugin::Logger->new(
Path => $VNDB::M{log_dir},
Private => 0,
Public => 1,
));
- $_[HEAP]{irc}->plugin_add(
+ $irc->plugin_add(
Connector => POE::Component::IRC::Plugin::Connector->new()
);
- $_[HEAP]{irc}->plugin_add(
+ $irc->plugin_add(
CTCP => POE::Component::IRC::Plugin::CTCP->new(
- version => $_[HEAP]{o}{ircname}.' v'.$VNDB::S{version},
- userinfo => $_[HEAP]{o}{ircname},
+ version => $_[HEAP]{ircname}.' v'.$VNDB::S{version},
+ userinfo => $_[HEAP]{ircname},
));
- if($_[HEAP]{o}{pass}) {
+ if($_[HEAP]{pass}) {
require POE::Component::IRC::Plugin::NickServID;
- $_[HEAP]{irc}->plugin_add(
+ $irc->plugin_add(
NickServID => POE::Component::IRC::Plugin::NickServID->new(
- Password => $_[HEAP]{o}{pass}
- ))
+ Password => $_[HEAP]{pass}
+ ))
}
- if($_[HEAP]{o}{console}) {
+ if($_[HEAP]{console}) {
require POE::Component::IRC::Plugin::Console;
- $_[HEAP]{irc}->plugin_add(
+ $irc->plugin_add(
Console => POE::Component::IRC::Plugin::Console->new(
bindport => 3030,
- password => $_[HEAP]{o}{console}
- ))
+ password => $_[HEAP]{console}
+ ))
}
- $_[KERNEL]->post(circ => register => 'all');
- $_[KERNEL]->post(circ => connect => {
- Nick => $_[HEAP]{o}{user},
+ $irc->yield(register => 'all');
+ $irc->yield(connect => {
+ Nick => $_[HEAP]{nick},
Username => 'u1',
- Ircname => $_[HEAP]{o}{ircname},
- Server => $_[HEAP]{o}{server},
+ Ircname => $_[HEAP]{ircname},
+ Server => $_[HEAP]{server},
});
- # notifications in the main channel enabled by default
- push @{$_[HEAP]{notify}}, $_[HEAP]{o}{channel}[0];
+ $_[KERNEL]->post(pg => listen =>
+ newrevision => 'notify',
+ newpost => 'notify',
+ newtag => 'notify',
+ );
+ $_[HEAP]{notify}{$_[HEAP]{channels}[0]} = 1;
- $_[KERNEL]->sig('shutdown' => 'shutdown');
+ $_[KERNEL]->sig(shutdown => 'shutdown');
+ $_[KERNEL]->delay(throttle_gc => 1800);
+ $_[KERNEL]->delay(idlequote => 300);
}
-sub irc_001 {
- $_[KERNEL]->post(circ => join => $_) for (@{$_[HEAP]{o}{channel}});
- $_[KERNEL]->call(core => log => 2, 'Connected to IRC!');
+sub shutdown {
+ $irc->yield(shutdown => $_[ARG1]);
+ $_[KERNEL]->post(pg => unlisten => qw|newrevision newpost newtag|);
+ $_[KERNEL]->delay('throttle_gc');
+ $_[KERNEL]->delay('idlequote');
+ $_[KERNEL]->alias_remove('irc');
}
-sub irc_public {
- if($_[ARG2] =~ /^!/) {
- (my $cmd = $_[ARG2]) =~ s/^!//;
- my $nick = (split /!/, $_[ARG0])[0];
- $_[KERNEL]->call(irc => irccmd => $_[ARG1][0], $cmd, $nick);
- } else {
- $_[KERNEL]->call(irc => vndbid => $_[ARG1][0], $_[ARG2]);
- }
+sub throttle_gc {
+ throttle $_[HEAP];
+ $_[KERNEL]->delay(throttle_gc => 1800);
}
-sub irc_ctcp_action {
- $_[KERNEL]->call(irc => vndbid => $_[ARG1][0], $_[ARG2]);
+sub irc_001 {
+ $irc->yield(join => $_) for (@{$_[HEAP]{channels}});
+ $_[KERNEL]->call(core => log => 'Connected to IRC');
}
-sub irc_msg {
- my $nick = ( split /!/, $_[ARG0] )[0];
- $_[ARG2] =~ s/^!//;
- if(!$_[KERNEL]->call(irc => irccmd => $nick => $_[ARG2])) {
- $_[HEAP]{privpers}{$_} < time-3600 and delete $_[HEAP]{privpers}{$_}
- for (keys %{$_[HEAP]{privpers}});
- $_[KERNEL]->post(circ => privmsg => $nick => 'I am not human, join #vndb or PM Yorhel if you need something.')
- if !$_[HEAP]{privpers}{$nick};
- $_[HEAP]{privpers}{$nick} ||= time;
- }
+sub irc_public { # mask, dest, msg
+ $_[HEAP]{idlequotes}{$_[ARG1][0]} = 0;
+ return if $_[KERNEL]->call($_[SESSION] => command => @_[ARG0..$#_]);
+ $_[KERNEL]->call($_[SESSION] => vndbid => $_[ARG1], $_[ARG2]);
}
-sub irccmd { # dest, cmd, [nick]
- my($dest, $cmd, $nick) = @_[ARG0..$#_];
- $nick ||= $_[ARG0];
+sub irc_ctcp_action { # mask, dest, msg
+ $_[KERNEL]->call($_[SESSION] => vndbid => $_[ARG1], $_[ARG2]);
+}
+
- return 0 if $cmd !~ /^([a-z0-9A-Z_]+)(?: (.+))?$/;
- my($f, $a) = (lc $1, $2||'');
+sub irc_msg { # mask, dest, msg
+ return if $_[KERNEL]->call($_[SESSION] => command => $_[ARG0], [scalar parse_user($_[ARG0])], $_[ARG2]);
- # check for a cmd_* function and call it (some scary magic, see perlmod)
- my $sub;
- {
- no strict;
- $sub = ${__PACKAGE__.'::'}{'cmd_'.$f};
- }
- return 0 if !defined $sub;
- local *SUB = $sub;
- return 0 if !defined *SUB{CODE};
- $_[KERNEL]->yield('cmd_'.$f, $a, $dest, $nick);
- return 1;
+ my $usr = parse_user($_[ARG0]);
+ $irc->yield(notice => $usr, 'I am not human, join #vndb or PM Yorhel if you need something.')
+ unless throttle $_[HEAP], "pm-$usr", 30;
}
-sub vndbid { # dest, msg, force
- my $m = $_[ARG1];
+sub command { # mask, dest, msg
+ my($mask, $dest, $msg) = @_[ARG0..$#_];
- $_[HEAP]{log}{$_} < time-60 and delete $_[HEAP]{log}{$_}
- for (keys %{$_[HEAP]{log}});
+ my $me = $irc->nick_name();
+ my $addressed = $dest->[0] !~ /^#/ || $msg =~ s/^\s*\Q$me\E[:,;.!?~]?\s*//;
+ return 0 if !$addressed && !($msg =~ s/^\s*!//);
- # Four possible options:
- # 1. [tvprug]+ -> item/user/thread/tag (nf)
- # 2. [vprt]+.+ -> revision/reply (ef)
- # 3. d+ -> documentation page (nf)
- # 4. d+.+ -> documentation page # section (sf)
+ return 0 if $msg !~ /^([a-z]+)(?:\s+(.+))?$/;
+ my($cmd, $arg) = ($1, $2);
+ return 0 if !exists $_[HEAP]{commands}{$cmd} || ($_[HEAP]{commands}{$cmd} & 8) && !$addressed;
- # nf (normal format): x+ : x, id, title
- # sf (sub format): x+.+ : x, id, subid, title, action2, title2
- # ef (extended format): x+.+ : x, id, subid, action, title, action2, title2
- my $nf = BOLD.RED.'['.NORMAL.BOLD.'%s%d' .RED.']' .NORMAL.' %s ' .RED.'@'.NORMAL.LIGHT_GREY.' '.$VNDB::S{url}.'/%1$s%2$d'.NORMAL;
- my $sf = BOLD.RED.'['.NORMAL.BOLD.'%s%d.%d'.RED.']' .NORMAL.' %s '.RED.'%s'.NORMAL.' %s '.RED.'@'.NORMAL.LIGHT_GREY.' '.$VNDB::S{url}.'/%1$s%2$d.%3$d'.NORMAL;
- my $ef = BOLD.RED.'['.NORMAL.BOLD.'%s%d.%d'.RED.']'.NORMAL.RED.' %s'.NORMAL.' %s '.RED.'%s'.NORMAL.' %s '.RED.'@'.NORMAL.LIGHT_GREY.' '.$VNDB::S{url}.'/%1$s%2$d.%3$d'.NORMAL;
-
- # get a list of possible IDs (a la sub summary in defs.pl)
- my @id; # [ type, id, ref ]
- for (split /[, ]/, $m) {
- next if length > 15 or m{[a-z]{3,6}://}i; # weed out URLs and too long things
- push @id, /^(?:.*[^\w]|)([dvprt])([1-9][0-9]*)\.([1-9][0-9]*)(?:[^\w].*|)$/ ? [ $1, $2, $3 ] # matches 2 and 4
- : /^(?:.*[^\w]|)([dvprtug])([1-9][0-9]*)(?:[^\w].*|)$/ ? [ $1, $2, 0 ] : (); # matches 1 and 3
- }
+ my $usr = parse_user($mask);
+ return $_[KERNEL]->yield(reply => $dest,
+ $dest eq $_[HEAP]{channels}[0] ? 'Only OPs can do that!' : "Only $_[HEAP]{channel}[0] OPs can do that!", $usr) || 1
+ if $_[HEAP]{commands}{$cmd} == 1 && !$irc->is_channel_operator($_[HEAP]{channels}[0], $usr);
+ return $_[KERNEL]->yield(reply => $dest, 'You are not my master!', $usr) || 1
+ if $_[HEAP]{commands}{$cmd} == 2 && !grep matches_mask($_, $mask), @{$_[HEAP]{masters}};
- # loop through the matched IDs and search the database
- for (@id) {
- my($t, $id, $rev) = (@$_);
-
- next if $_[HEAP]{log}{$t.$id.'.'.$rev} && !$_[ARG2];
- $_[HEAP]{log}{$t.$id.'.'.$rev} = time;
-
- # option 1: item/user/thread/tag
- if($t =~ /[vprtug]/ && !$rev) {
- my $s = $Multi::SQL->prepare(
- $t eq 'v' ? 'SELECT vr.title FROM vn_rev vr JOIN vn v ON v.latest = vr.id WHERE v.id = ?' :
- $t eq 'u' ? 'SELECT u.username AS title FROM users u WHERE u.id = ?' :
- $t eq 'p' ? 'SELECT pr.name AS title FROM producers_rev pr JOIN producers p ON p.latest = pr.id WHERE p.id = ?' :
- $t eq 't' ? 'SELECT title FROM threads WHERE id = ?' :
- $t eq 'g' ? 'SELECT name AS title FROM tags WHERE id = ?' :
- 'SELECT rr.title FROM releases_rev rr JOIN releases r ON r.latest = rr.id WHERE r.id = ?'
- );
- $s->execute($id);
- my $r = $s->fetchrow_hashref;
- $s->finish;
- next if !$r || ref($r) ne 'HASH';
- $_[KERNEL]->post(circ => privmsg => $_[ARG0], sprintf $nf,
- $t, $id, $r->{title});
-
- # option 2: revision/reply
- } elsif($t =~ /[vprt]/) {
- my $s = $Multi::SQL->prepare(
- $t eq 'v' ? 'SELECT vr.title, u.username FROM changes c JOIN vn_rev vr ON c.id = vr.id JOIN users u ON u.id = c.requester WHERE vr.vid = ? AND c.rev = ?' :
- $t eq 'r' ? 'SELECT rr.title, u.username FROM changes c JOIN releases_rev rr ON c.id = rr.id JOIN users u ON u.id = c.requester WHERE rr.rid = ? AND c.rev = ?' :
- $t eq 'p' ? 'SELECT pr.name, u.username FROM changes c JOIN producers_rev pr ON c.id = pr.id JOIN users u ON u.id = c.requester WHERE pr.pid = ? AND c.rev = ?' :
- 'SELECT t.title, u.username FROM threads t JOIN threads_posts tp ON tp.tid = t.id JOIN users u ON u.id = tp.uid WHERE t.id = ? AND tp.num = ?'
- );
- $s->execute($id, $rev);
- my $r = $s->fetchrow_arrayref;
- next if !$r || ref($r) ne 'ARRAY';
- $_[KERNEL]->post(circ => privmsg => $_[ARG0], sprintf $ef, $t, $id, $rev,
- $rev == 1 ? 'New '.($t eq 'v' ? 'visual novel' : $t eq 'p' ? 'producer' : $t eq 'r' ? 'release': 'thread')
- : ($t eq 't' ? 'Reply to' : 'Edit of'), $r->[0], 'By', $r->[1]
- );
-
- # option 3: documentation page
- } elsif($t eq 'd') {
- my $f = sprintf '/www/vndb/data/docs/%d', $id;
- open my $F, '<', $f or next;
- (my $title = <$F>) =~ s/^:TITLE://;
- chomp($title);
+ return $_[KERNEL]->yield('cmd_'.$cmd, $usr, $dest, $arg, $mask) || 1;
+}
- if(!$rev) {
- $_[KERNEL]->post(circ => privmsg => $_[ARG0], sprintf $nf,
- 'd', $id, $title);
- next;
- }
- # option 4: documentation page # section
- my($sec, $sub);
- while(<$F>) {
- if(/^:SUB:/ && ++$sec == $rev) {
- chomp;
- ($sub = $_) =~ s/^:SUB://;
- last;
- }
- }
- next if !$sub;
- $_[KERNEL]->post(circ => privmsg => $_[ARG0], sprintf $sf,
- 'd', $id, $rev, $title, '->', $sub);
- }
+sub idlequote {
+ for (keys %{$_[HEAP]{idlequotes}}) {
+ next if --$_[HEAP]{idlequotes}{$_} > 0;
+ $_[KERNEL]->yield(cmd_quote => '', [$_]) if $_[HEAP]{idlequotes}{$_} == 0;
+ $_[HEAP]{idlequotes}{$_} = int(60+rand(300));
}
+ $_[KERNEL]->delay(idlequote => 60);
}
-sub ircnotify { # command, VNDBID
- $_[KERNEL]->yield(vndbid => $_ => $_[ARG1] => 1) for (@{$_[HEAP]{notify}});
- $_[KERNEL]->post(core => finish => $_[ARG0]);
+# convenience function
+sub reply { # target, msg [, mask/user]
+ my $usr = $_[ARG0][0] =~ /^#/ && parse_user($_[ARG2]);
+ $irc->yield($_[ARG0][0] =~ /^#/ ? 'privmsg' : 'notice', $_[ARG0], ($usr ? "$usr, " : '').$_[ARG1]);
}
-sub shutdown {
- $_[KERNEL]->post(circ => shutdown => 'Byebye!');
+sub notify { # name, pid, payload
+ my $k = $_[ARG0] eq 'newrevision' ? 'lastrev' : $_[ARG0] eq 'newpost' ? 'lastpost' : 'lasttag';
+ my $t = $_[HEAP]{$k};
+ $_[HEAP]{$k} = time;
+
+ return if !keys %{$_[HEAP]{notify}};
+
+ my $q = $_[ARG0] eq 'newrevision' ? q|SELECT
+ CASE WHEN c.type = 0 THEN 'v' WHEN c.type = 1 THEN 'r' ELSE 'p' END AS type, c.rev, c.comments,
+ COALESCE(vr.vid, rr.rid, pr.pid) AS id, COALESCE(vr.title, rr.title, pr.name) AS title, u.username
+ FROM changes c
+ LEFT JOIN vn_rev vr ON c.type = 0 AND c.id = vr.id
+ LEFT JOIN releases_rev rr ON c.type = 1 AND c.id = rr.id
+ LEFT JOIN producers_rev pr ON c.type = 2 AND c.id = pr.id
+ JOIN users u ON u.id = c.requester
+ WHERE c.added > ?
+ ORDER BY c.added|
+ : $_[ARG0] eq 'newpost' ? q|SELECT
+ 't' AS type, tp.tid AS id, tp.num AS rev, t.title, u.username, |.GETBOARDS.q|
+ FROM threads_posts tp
+ JOIN threads t ON t.id = tp.tid
+ JOIN users u ON u.id = tp.uid
+ WHERE tp.date > ?
+ ORDER BY tp.date|
+ : q|SELECT
+ 'g' AS type, t.id, t.name AS title, u.username
+ FROM tags t
+ JOIN users u ON u.id = t.addedby
+ WHERE t.added > ?
+ ORDER BY t.added|;
+
+ $_[KERNEL]->post(pg => query => $q, [ $t ], 'formatid', [ keys %{$_[HEAP]{notify}} ]);
}
-# cmd_* commands: $arg, $dest, $nick
-sub cmd_info {
- $_[KERNEL]->post(circ => privmsg => $_[DEST],
- 'Hello, I am HMX-12 Multi v'.$VNDB::S{version}.' made by the great Yorhel!');
-}
+#
+# I R C C O M M A N D S
+#
-sub cmd_vndb {
- $_[KERNEL]->post(circ => privmsg => $_[DEST],
- 'VNDB ~ The Visual Novel Database ~ http://vndb.org/');
+sub cmd_info {
+ $_[KERNEL]->yield(reply => $_[DEST],
+ 'Hi! I am HMX-12 Multi '.$VNDB::S{version}.', the IRC bot of '.$VNDB::S{url}.'/, written by the great Yorhel!');
}
sub cmd_list {
- return if $_[DEST] ne $_[HEAP]{o}{channel}[0];
- $_[KERNEL]->post(circ => privmsg => $_[DEST],
- $_[NICK].', this is not a warez channel!');
-}
-
-
-sub cmd_vn { # $arg = search string
- $_[ARG] =~ s/%//g;
- return $_[KERNEL]->post(circ => privmsg => $_[DEST], 'You forgot the search query, idiot~~!.') if !$_[ARG];
-
- my $q = $Multi::SQL->prepare(q|
- SELECT v.id
- FROM vn v
- JOIN vn_rev vr ON vr.id = v.latest
- WHERE vr.title ILIKE $1
- OR vr.alias ILIKE $1
- OR v.id IN(
- SELECT rv.vid
- FROM releases r
- JOIN releases_rev rr ON rr.id = r.latest
- JOIN releases_vn rv ON rv.rid = rr.id
- WHERE rr.title ILIKE $1
- OR rr.original ILIKE $1
- )
- ORDER BY vr.id
- LIMIT 6|);
- $q->execute('%'.$_[ARG].'%');
-
- my $res = $q->fetchall_arrayref([]);
- return $_[KERNEL]->post(circ => privmsg => $_[DEST],
- sprintf 'No results found for %s', $_[ARG]) if !@$res;
- return $_[KERNEL]->post(circ => privmsg => $_[DEST],
- sprintf 'Too many results found, see %s/v/search?q=%s',
- $VNDB::S{url}, uri_escape_utf8($_[ARG])) if @$res > 5;
- $_[KERNEL]->yield(vndbid => $_[DEST], join(' ', map 'v'.$_->[0], @$res), 1);
+ $_[KERNEL]->yield(reply => $_[DEST],
+ $_[DEST][0] =~ /^#/ ? 'This is not a warez channel!' : 'I am not a warez bot!', $_[USER]);
}
@@ -337,82 +315,245 @@ sub cmd_uptime {
open my $R, '<', '/proc/uptime';
my $server = <$R> =~ /^\s*([0-9]+)/ ? $1 : 0;
close $R;
-
my $multi = time - $^T;
- my $http=0;
- # this should actually be done asynchronously... but I don't expect it to timeout
- if(my $req = Net::HTTP->new(Host => 'localhost', Timeout => 1)) {
- $req->write_request(GET => '/server-status?auto');
- my $d;
- $req->read_entity_body($d, 1024) if $req->read_response_headers;
- $http = $1 if $d =~ /Uptime:\s*([0-9]+)/i;
- }
-
- $_[KERNEL]->post(circ => privmsg => $_[DEST], $_) for (split /\n/, sprintf
- "Uptimes:\n Server: %s\n Multi: %s\n HTTP: %s", map $age->($_), $server, $multi, $http);
+ $_[KERNEL]->yield(reply => $_[DEST], sprintf 'Server uptime: %s -- mine: %s', $age->($server), $age->($multi));
}
-sub cmd_notifications { # $arg = '' or 'on' or 'off'
- return unless &mymaster;
- if($_[ARG] =~ /^on$/i) {
- push @{$_[HEAP]{notify}}, $_[DEST] if !grep $_ eq $_[DEST], @{$_[HEAP]{notify}};
- $_[KERNEL]->post(circ => privmsg => $_[DEST], 'Notifications enabled.');
- } elsif($_[ARG] =~ /^off$/i) {
- $_[HEAP]{notify} = [ grep $_ ne $_[DEST], @{$_[HEAP]{notify}} ];
- $_[KERNEL]->post(circ => privmsg => $_[DEST], 'Notifications disabled.');
- } else {
- $_[KERNEL]->post(circ => privmsg => $_[DEST], sprintf 'Notifications %s, type !notifications %s to %s.',
- (grep $_ eq $_[DEST], @{$_[HEAP]{notify}}) ? ('enabled', 'off', 'disable') : ('disabled', 'on', 'enable'));
- }
+sub cmd_vn {
+ (my $q = $_[ARG]||'') =~ s/%//g;
+ return $_[KERNEL]->yield(reply => $_[DEST], 'You forgot the search query, dummy~~!', $_[USER]) if !$q;
+ return $_[KERNEL]->yield(reply => $_[DEST], 'Stop abusing me, it\'s not like I enjoy spamming this channel!', $_[USER])
+ if throttle $_[HEAP], "query-$_[USER]-$_[DEST][0]", 60, 5;
+
+ $_[KERNEL]->post(pg => query => q|
+ SELECT 'v'::text AS type, v.id, vr.title
+ FROM vn v
+ JOIN vn_rev vr ON vr.id = v.latest
+ WHERE v.hidden = FALSE AND (vr.title ILIKE $1
+ OR vr.alias ILIKE $1
+ OR v.id IN(
+ SELECT rv.vid
+ FROM releases r
+ JOIN releases_rev rr ON rr.id = r.latest
+ JOIN releases_vn rv ON rv.rid = rr.id
+ WHERE rr.title ILIKE $1
+ OR rr.original ILIKE $1
+ ))
+ ORDER BY vr.title
+ LIMIT 6|, [ "%$q%" ], 'cmd_vn_results', \@_);
+}
+
+
+sub cmd_vn_results { # num, res, \@_
+ return $_[KERNEL]->yield(reply => $_[ARG2][DEST], 'No visual novels found', $_[ARG2][USER]) if $_[ARG0] < 1;
+ return $_[KERNEL]->yield(reply => $_[ARG2][DEST], sprintf(
+ 'Too many results found, see %s/v/all?q=%s', $VNDB::S{url}, uri_escape_utf8($_[ARG2][ARG])
+ ), $_[ARG2][USER]) if $_[ARG0] > 5;
+ $_[KERNEL]->yield(formatid => $_[ARG0], $_[ARG1], $_[ARG2][DEST]);
+}
+
+
+sub cmd_p {
+ (my $q = $_[ARG]||'') =~ s/%//g;
+ return $_[KERNEL]->yield(reply => $_[DEST], 'You forgot the search query, dummy~~!', $_[USER]) if !$q;
+ return $_[KERNEL]->yield(reply => $_[DEST], 'Stop abusing me, it\'s not like I enjoy spamming this channel!', $_[USER])
+ if throttle $_[HEAP], "query-$_[USER]-$_[DEST][0]", 60, 5;
+
+ $_[KERNEL]->post(pg => query => q|
+ SELECT 'p'::text AS type, p.id, pr.name AS title
+ FROM producers p
+ JOIN producers_rev pr ON pr.id = p.latest
+ WHERE p.hidden = FALSE AND (pr.name ILIKE $1 OR pr.original ILIKE $1 OR pr.alias ILIKE $1)
+ ORDER BY pr.name
+ LIMIT 6|, [ "%$q%" ], "cmd_p_results", \@_);
+}
+
+
+sub cmd_p_results { # num, res, \@_
+ return $_[KERNEL]->yield(reply => $_[ARG2][DEST], 'No producers found', $_[ARG2][USER]) if $_[ARG0] < 1;
+ return $_[KERNEL]->yield(reply => $_[ARG2][DEST], sprintf(
+ 'Too many results found, see %s/p/all?q=%s', $VNDB::S{url}, uri_escape_utf8($_[ARG2][ARG])
+ ), $_[ARG2][USER]) if $_[ARG0] > 5;
+ $_[KERNEL]->yield(formatid => $_[ARG0], $_[ARG1], $_[ARG2][DEST]);
+}
+
+
+sub cmd_quote {
+ $_[KERNEL]->post(pg => query => q|SELECT quote FROM quotes ORDER BY random() LIMIT 1|, undef, 'cmd_quote_result', $_[DEST]);
+}
+
+
+sub cmd_quote_result { # 1, res, dest
+ $_[KERNEL]->yield(reply => $_[ARG2] => $_[ARG1][0]{quote}) if $_[ARG0] > 0;
}
-sub cmd_say { # $arg = '[#chan ]text', no #chan = $dest
- return unless &mymaster;
+sub cmd_say {
my $chan = $_[ARG] =~ s/^(#[a-zA-Z0-9-_.]+) // ? $1 : $_[DEST];
- $_[KERNEL]->post(circ => privmsg => $chan, $_[ARG]);
+ $irc->yield(privmsg => $chan, $_[ARG]);
}
-sub cmd_me { # same as cmd_say, but CTCP ACTION
- return unless &mymaster;
+sub cmd_me {
my $chan = $_[ARG] =~ s/^(#[a-zA-Z0-9-_.]+) // ? $1 : $_[DEST];
- $_[KERNEL]->post(circ => ctcp => $chan, 'ACTION '.$_[ARG]);
+ $irc->yield(ctcp => $chan, 'ACTION '.$_[ARG]);
}
-sub cmd_cmd { # TODO: feedback?
- return unless &mymaster;
- $_[KERNEL]->post(core => queue => $_[ARG]);
- $_[KERNEL]->post(circ => privmsg => $_[DEST] => sprintf "Executing %s", $_[ARG]);
+sub cmd_notifications { # $arg = '' or 'on' or 'off'
+ if($_[ARG] && $_[ARG] =~ /^on$/i) {
+ $_[HEAP]{notify}{$_[DEST][0]} = 1;
+ $_[KERNEL]->yield(reply => $_[DEST], 'Notifications enabled.');
+ } elsif($_[ARG] && $_[ARG] =~ /^off$/i) {
+ delete $_[HEAP]{notify}{$_[DEST][0]};
+ $_[KERNEL]->yield(reply => $_[DEST], 'Notifications disabled.');
+ } else {
+ $_[KERNEL]->yield(reply => $_[DEST], sprintf 'Notifications %s, type !notifications %s to %s.',
+ $_[HEAP]{notify}{$_[DEST][0]} ? ('enabled', 'off', 'disable') : ('disabled', 'on', 'enable'));
+ }
}
-sub cmd_eval { # the evil cmd
- return unless &mymaster;
- $_[KERNEL]->post(circ => privmsg => $_[DEST], 'eval: '.$_)
+sub cmd_eval {
+ $_[KERNEL]->yield(reply => $_[DEST], 'eval: '.$_)
for (split /\r?\n/, eval($_[ARG])||$@);
}
+sub cmd_die {
+ $irc->yield(ctcp => $_[DEST] => 'ACTION dies');
+ $_[KERNEL]->signal(core => shutdown => "Killed on IRC by $_[USER]");
+}
-# non-POE function, checks whether we should trust $nick
-sub mymaster { # same @_ as the cmd_ functions
- if(!$_[HEAP]{irc}->is_channel_operator($_[HEAP]{o}{channel}[0], $_[ARG2])
- && !$_[HEAP]{irc}->is_channel_owner($_[HEAP]{o}{channel}[0], $_[ARG2])
- && !$_[HEAP]{irc}->is_channel_admin($_[HEAP]{o}{channel}[0], $_[ARG2])
- || ($_[HEAP]{o}{master_users} && !grep lc($_) eq lc($_[ARG2]), @{$_[HEAP]{o}{master_users}})
- ) {
- $_[KERNEL]->post(circ => privmsg => $_[ARG1],
- ($_[ARG1]=~/^#/?$_[ARG2].', ':'').'You are not my master!');
- return 0;
+sub cmd_post {
+ $_[KERNEL]->yield(reply => $_[DEST], $_[KERNEL]->post(split /\s+/, $_[ARG])
+ ? 'Sent your message to the post office, it will be processed shortly!'
+ : "Oh no! The post office wouldn't accept your message! Wrong destination address?", $_[USER]);
+}
+
+
+
+
+#
+# D B I T E M L I N K S
+#
+
+
+sub vndbid { # dest, msg
+ my($dest, $msg) = @_[ARG0, ARG1];
+
+ my @id; # [ type, id, ref ]
+ for (split /[, ]/, $msg) {
+ next if length > 15 or m{[a-z]{3,6}://}i; # weed out URLs and too long things
+ push @id, /^(?:.*[^\w]|)([dvprt])([1-9][0-9]*)\.([1-9][0-9]*)(?:[^\w].*|)$/ ? [ $1, $2, $3 ] # x+.+
+ : /^(?:.*[^\w]|)([dvprtug])([1-9][0-9]*)(?:[^\w].*|)$/ ? [ $1, $2, 0 ] : (); # x+
+ }
+
+ for (@id) {
+ my($t, $id, $rev) = @$_;
+ next if throttle $_[HEAP], "$dest->[0].$t$id.$rev", 60;
+
+ # plain vn/user/producer/thread/tag/release
+ $_[KERNEL]->post(pg => query => 'SELECT ?::text AS type, ?::integer AS id, '.(
+ $t eq 'v' ? 'vr.title FROM vn_rev vr JOIN vn v ON v.latest = vr.id WHERE v.id = ?' :
+ $t eq 'u' ? 'u.username AS title FROM users u WHERE u.id = ?' :
+ $t eq 'p' ? 'pr.name AS title FROM producers_rev pr JOIN producers p ON p.latest = pr.id WHERE p.id = ?' :
+ $t eq 't' ? 'title, '.GETBOARDS.' FROM threads t WHERE id = ?' :
+ $t eq 'g' ? 'name AS title FROM tags WHERE id = ?' :
+ 'rr.title FROM releases_rev rr JOIN releases r ON r.latest = rr.id WHERE r.id = ?'),
+ [ $t, $id, $id ], 'formatid', $dest
+ ) if !$rev && $t =~ /[vprtug]/;
+
+ # edit/insert of vn/release/producer or discussion board post
+ $_[KERNEL]->post(pg => query => 'SELECT ?::text AS type, ?::integer AS id, ?::integer AS rev, '.(
+ $t eq 'v' ? 'vr.title, u.username, c.comments FROM changes c JOIN vn_rev vr ON c.id = vr.id JOIN users u ON u.id = c.requester WHERE vr.vid = ? AND c.rev = ?' :
+ $t eq 'r' ? 'rr.title, u.username, c.comments FROM changes c JOIN releases_rev rr ON c.id = rr.id JOIN users u ON u.id = c.requester WHERE rr.rid = ? AND c.rev = ?' :
+ $t eq 'p' ? 'pr.name AS title, u.username, c.comments FROM changes c JOIN producers_rev pr ON c.id = pr.id JOIN users u ON u.id = c.requester WHERE pr.pid = ? AND c.rev = ?' :
+ 't.title, u.username, '.GETBOARDS.' FROM threads t JOIN threads_posts tp ON tp.tid = t.id JOIN users u ON u.id = tp.uid WHERE t.id = ? AND tp.num = ?'),
+ [ $t, $id, $rev, $id, $rev], 'formatid', $dest
+ ) if $rev && $t =~ /[vprt]/;
+
+ # documentation page (need to parse the doc pages manually here)
+ if($t eq 'd') {
+ my $f = sprintf $VNDB::ROOT.'/data/docs/%d', $id;
+ my($title, $sec, $sub) = (undef, 0);
+ open my $F, '<', $f or next;
+ while(<$F>) {
+ chomp;
+ $title = $1 if /^:TITLE:(.+)$/;
+ $sub = $1 if $rev && /^:SUB:(.+)$/ && ++$sec == $rev;
+ }
+ close $F;
+ next if $rev && !$sub;
+ $_[KERNEL]->yield(formatid => 1, [{type => 'd', id => $id, title => $title, rev => $rev, section => $sub}], $dest);
+ }
}
- return 1;
}
-1;
+# formats and posts database items listed in @res, where each item is a hashref with:
+# type database item in [dvprtug]
+# id database id
+# title main name or title of the DB entry
+# rev (optional) revision, post number or section number
+# username (optional) relevant username
+# section (optional, for d+.+) section title
+# boards (optional) board titles the thread has been posted in
+# comments (optional) edit summary
+sub formatid {
+ my($num, $res, $dest) = @_[ARG0..$#_];
+
+ # only the types for which creation/edit announcements matter
+ my %types = (
+ v => 'visual novel',
+ p => 'producer',
+ r => 'release',
+ g => 'tag',
+ t => 'thread',
+ );
+
+ for (@$res) {
+ my $id = $_->{type}.$_->{id} . ($_->{rev} ? '.'.$_->{rev} : '');
+
+ # (always) [x+.+]
+ my @msg = (
+ BOLD.RED.'['.NORMAL.BOLD.$id.RED.']'.NORMAL
+ );
+
+ # (only if username key is present) Edit of / New item / reply to / whatever
+ push @msg, RED.(
+ ($_->{rev}||1) == 1 ? 'New '.$types{$_->{type}} :
+ $_->{type} eq 't' ? 'Reply to' : 'Edit of'
+ ).NORMAL if $_->{username};
+
+ # (always) main title
+ push @msg, $_->{title};
+
+ # (only if boards key is present) Posted in [boards]
+ push @msg, RED.'Posted in'.NORMAL.' '.$_->{boards} if $_->{boards};
+
+ # (only if username key is present) By [username]
+ push @msg, RED.'By'.NORMAL.' '.$_->{username} if $_->{username};
+
+ # (only if comments key is present) Summary:
+ push @msg, RED.'Summary:'.NORMAL.' '.(
+ length $_->{comments} > 40 ? substr($_->{comments}, 0, 37).'...' : $_->{comments}
+ ) if defined $_->{comments};
+
+ # (for d+.+) -> section title
+ push @msg, RED.'->'.NORMAL.' '.$_->{section} if $_->{section};
+
+ # (always) @ URL
+ push @msg, RED.'@ '.NORMAL.LIGHT_GREY.$VNDB::S{url}.'/'.$id.NORMAL;
+
+ # now post it
+ $_[KERNEL]->yield(reply => $dest, join ' ', @msg);
+ }
+}
+
+
+1;
diff --git a/lib/Multi/Image.pm b/lib/Multi/Image.pm
index fe3d411e..f947bf7d 100644
--- a/lib/Multi/Image.pm
+++ b/lib/Multi/Image.pm
@@ -9,7 +9,6 @@ use strict;
use warnings;
use POE;
use Image::Magick;
-use Image::MetaData::JPEG;
use Time::HiRes 'time';
@@ -18,8 +17,7 @@ sub spawn {
POE::Session->create(
package_states => [
$p => [qw| _start
- cmd_coverimage cv_process cv_update cv_finish
- cmd_screenshot scr_process scr_update scr_clean scr_finish
+ _start shutdown cv_check cv_process scr_check scr_process
|],
],
heap => {
@@ -28,6 +26,8 @@ sub spawn {
cvpath => $VNDB::ROOT.'/static/cv',
sfpath => $VNDB::ROOT.'/static/sf',
stpath => $VNDB::ROOT.'/static/st',
+ check_delay => 3600,
+ @_,
},
);
}
@@ -36,212 +36,112 @@ sub spawn {
sub _start {
$_[KERNEL]->alias_set('image');
$_[KERNEL]->sig(shutdown => 'shutdown');
- $_[KERNEL]->call(core => register => qr/^coverimage(?: ([0-9]+)|)$/, 'cmd_coverimage');
- $_[KERNEL]->call(core => register => qr/^screenshot(?: ([0-9]+|all|clean))?$/, 'cmd_screenshot');
+ $_[KERNEL]->post(pg => listen => coverimage => 'cv_check', screenshot => 'scr_check');
+ $_[KERNEL]->yield('cv_check');
+ $_[KERNEL]->yield('scr_check');
}
-sub cmd_coverimage {
- $_[HEAP]{curcmd} = $_[ARG0];
-
- if($_[ARG1]) {
- $_[HEAP]{todo} = [ $_[ARG1] ];
- } else {
- my $q = $Multi::SQL->prepare('SELECT image FROM vn_rev WHERE image < 0');
- $q->execute();
- $_[HEAP]{todo} = [ map { -1*$_->[0]} @{$q->fetchall_arrayref([])} ];
- if(!@{$_[HEAP]{todo}}) {
- $_[KERNEL]->call(core => log => 2, 'No images to process');
- $_[KERNEL]->yield('cv_finish');
- return;
- }
- }
- $_[KERNEL]->yield(cv_process => $_[HEAP]{todo}[0]);
+sub shutdown {
+ $_[KERNEL]->post(pg => unlisten => 'coverimage', 'screenshot');
+ $_[KERNEL]->delay('cv_check');
+ $_[KERNEL]->delay('scr_check');
+ $_[KERNEL]->alias_remove('image');
}
-sub cv_process { # id
- my $start = time;
+sub cv_check {
+ $_[KERNEL]->delay('cv_check');
+ $_[KERNEL]->post(pg => query => 'SELECT image FROM vn_rev WHERE image < 0 LIMIT 1', undef, 'cv_process');
+}
+
- my $img = sprintf '%s/%02d/%d.jpg', $_[HEAP]{cvpath}, $_[ARG0]%100, $_[ARG0];
+sub cv_process { # num, res
+ return $_[KERNEL]->delay(cv_check => $_[HEAP]{check_delay}) if $_[ARG0] == 0;
+ my $id = -1*$_[ARG1][0]{image};
+ my $start = time;
+ my $img = sprintf '%s/%02d/%d.jpg', $_[HEAP]{cvpath}, $id%100, $id;
my $os = -s $img;
+
my $im = Image::Magick->new;
$im->Read($img);
$im->Set(magick => 'JPEG');
- my($w, $h) = ($im->Get('width'), $im->Get('height'));
- my($ow, $oh) = ($w, $h);
- if($w > $_[HEAP]{cvsize}[0] || $h > $_[HEAP]{cvsize}[1]) {
- if($w/$h > $_[HEAP]{cvsize}[0]/$_[HEAP]{cvsize}[1]) { # width is the limiting factor
- $h *= $_[HEAP]{cvsize}[0]/$w;
- $w = $_[HEAP]{cvsize}[0];
- } else {
- $w *= $_[HEAP]{cvsize}[1]/$h;
- $h = $_[HEAP]{cvsize}[1];
- }
- $im->Thumbnail(width => $w, height => $h);
- }
+ my($old, $new) = do_resize($im, $_[HEAP]{cvsize});
$im->Set(quality => 80);
$im->Write($img);
- undef $im;
- my $md = Image::MetaData::JPEG->new($img);
- $md->drop_segments('METADATA');
- $md->save($img);
+ $_[KERNEL]->post(pg => do => 'UPDATE vn_rev SET image = image*-1 WHERE image = ?', [ -1*$id ]);
+ $_[KERNEL]->call(core => log => 'Processed cover image %d in %.2fs: %.2fkB (%dx%d) -> %.2fkB (%dx%d)',
+ $id, time-$start, $os/1024, $$old[0], $$old[1], (-s $img)/1024, $$new[0], $$new[1]);
- $_[KERNEL]->call(core => log => 2, 'Processed cover image %d in %.2fs: %.2fkB (%dx%d) -> %.2fkB (%dx%d)',
- $_[ARG0], time-$start, $os/1024, $ow, $oh, (-s $img)/1024, $w, $h);
- $_[KERNEL]->yield(cv_update => $_[ARG0]);
+ $_[KERNEL]->yield('cv_check');
}
-sub cv_update { # id
- if($Multi::SQL->do('UPDATE vn_rev SET image = ? WHERE image = ?', undef, $_[ARG0], -1*$_[ARG0]) > 0) {
- $_[KERNEL]->yield(cv_finish => $_[ARG0]);
- } else {
- $_[KERNEL]->call(core => log => 1, 'Image %d not present in the database!', $_[ARG0]);
- $_[KERNEL]->yield(cv_finish => $_[ARG0]);
- }
-}
-
-
-sub cv_finish { # [id]
- if($_[ARG0]) {
- $_[HEAP]{todo} = [ grep $_[ARG0]!=$_, @{$_[HEAP]{todo}} ];
- return $_[KERNEL]->yield(cv_process => $_[HEAP]{todo}[0])
- if @{$_[HEAP]{todo}};
- }
-
- $_[KERNEL]->post(core => finish => $_[HEAP]{curcmd});
- delete @{$_[HEAP]}{qw| curcmd todo |};
+sub scr_check {
+ $_[KERNEL]->delay('scr_check');
+ $_[KERNEL]->post(pg => query => 'SELECT id FROM screenshots WHERE processed = false LIMIT 1', undef, 'scr_process');
}
+sub scr_process { # num, res
+ return $_[KERNEL]->delay(scr_check => $_[HEAP]{check_delay}) if $_[ARG0] == 0;
-
-sub cmd_screenshot {
- my($cmd, $id) = @_[ARG0, ARG1];
- $_[HEAP]{curcmd} = $_[ARG0];
- $_[HEAP]{id} = $_[ARG1];
-
- if(!$id) {
- my $q = $Multi::SQL->prepare('SELECT id FROM screenshots WHERE status = 0');
- $q->execute();
- $_[HEAP]{todo} = [ map { $_->[0]} @{$q->fetchall_arrayref([])} ];
- if(!@{$_[HEAP]{todo}}) {
- $_[KERNEL]->call(core => log => 2, 'No screenshots to process');
- $_[KERNEL]->yield('scr_finish');
- return;
- }
-
- } elsif($id eq 'clean') {
- return $_[KERNEL]->yield('scr_clean');
-
- } elsif($id eq 'all') {
- my $q = $Multi::SQL->prepare('SELECT DISTINCT scr FROM vn_screenshots');
- $q->execute();
- $_[HEAP]{todo} = [ map $_->[0], @{$q->fetchall_arrayref([])} ];
-
- } else {
- $_[HEAP]{todo} = [ $_[ARG1] ];
- }
-
- $_[KERNEL]->yield(scr_process => $_[HEAP]{todo}[0]);
-}
-
-
-sub scr_process { # id
+ my $id = $_[ARG1][0]{id};
my $start = time;
-
- my $sf = sprintf '%s/%02d/%d.jpg', $_[HEAP]{sfpath}, $_[ARG0]%100, $_[ARG0];
- my $st = sprintf '%s/%02d/%d.jpg', $_[HEAP]{stpath}, $_[ARG0]%100, $_[ARG0];
-
- # convert/compress full-size image
+ my $sf = sprintf '%s/%02d/%d.jpg', $_[HEAP]{sfpath}, $id%100, $id;
+ my $st = sprintf '%s/%02d/%d.jpg', $_[HEAP]{stpath}, $id%100, $id;
my $os = -s $sf;
+
+ # convert/compress full-size image
my $im = Image::Magick->new;
$im->Read($sf);
$im->Set(magick => 'JPEG');
$im->Set(quality => 90);
$im->Write($sf);
- # create thumbnail
- my($w, $h) = ($im->Get('width'), $im->Get('height'));
- my($ow, $oh) = ($w, $h);
- if($w/$h > $_[HEAP]{scrsize}[0]/$_[HEAP]{scrsize}[1]) { # width is the limiting factor
- $h *= $_[HEAP]{scrsize}[0]/$w;
- $w = $_[HEAP]{scrsize}[0];
- } else {
- $w *= $_[HEAP]{scrsize}[1]/$h;
- $h = $_[HEAP]{scrsize}[1];
- }
- $im->Thumbnail(width => $w, height => $h);
+ # create thumbnail
+ my($old, $new) = do_resize($im, $_[HEAP]{scrsize});
$im->Set(quality => 90);
$im->Write($st);
- undef $im;
-
- # remove metadata in both files
- my $md = Image::MetaData::JPEG->new($sf);
- $md->drop_segments('METADATA');
- $md->save($sf);
- $md = Image::MetaData::JPEG->new($st);
- $md->drop_segments('METADATA');
- $md->save($st);
- undef $md;
-
- $_[KERNEL]->call(core => log => 2, 'Processed screenshot #%d in %.2fs: %.1fkB -> %.1fkB (%dx%d), thumb: %.1fkB (%dx%d)',
- $_[ARG0], time-$start, $os/1024, (-s $sf)/1024, $ow, $oh, (-s $st)/1024, $w, $h);
- $_[KERNEL]->yield(scr_update => $_[ARG0], $ow, $oh);
-}
+ $_[KERNEL]->post(pg => do =>
+ 'UPDATE screenshots SET processed = true, width = ?, height = ? WHERE id = ?',
+ [ $$old[0], $$old[1], $id ]
+ );
+ $_[KERNEL]->call(core => log =>
+ 'Processed screenshot #%d in %.2fs: %.1fkB -> %.1fkB (%dx%d), thumb: %.1fkB (%dx%d)',
+ $id, time-$start, $os/1024, (-s $sf)/1024, $$old[0], $$old[1], (-s $st)/1024, $$new[0], $$new[1]
+ );
-sub scr_update { # id, width, height
- if($Multi::SQL->do('UPDATE screenshots SET status = 1, width = ?, height = ? WHERE id = ?', undef, $_[ARG1], $_[ARG2], $_[ARG0]) > 0) {
- $_[KERNEL]->yield(scr_finish => $_[ARG0]);
- } else {
- $_[KERNEL]->call(core => log => 1, 'Screenshot %d not present in the database!', $_[ARG0]);
- $_[KERNEL]->yield(scr_finish => $_[ARG0]);
- }
+ $_[KERNEL]->yield('scr_check');
}
-sub scr_clean {
- my $sql = ' FROM screenshots s WHERE NOT EXISTS(SELECT 1 FROM vn_screenshots vs WHERE vs.scr = s.id)';
- my $q = $Multi::SQL->prepare('SELECT s.id'.$sql);
- $q->execute();
-
- my($bytes, $items, $id) = (0, 0, 0);
- while(($id) = $q->fetchrow_array) {
- my $f = sprintf '%s/%02d/%d.jpg', $_[HEAP]{sfpath}, $id%100, $id;
- my $t = sprintf '%s/%02d/%d.jpg', $_[HEAP]{stpath}, $id%100, $id;
- $bytes += -s $f;
- $bytes += -s $t;
- $items++;
- unlink $f;
- unlink $t;
- $_[KERNEL]->call(core => log => 3, 'Removing screenshot #%d', $id);
- }
-
- $Multi::SQL->do('DELETE'.$sql);
- $_[KERNEL]->call(core => log => 2, 'Removed %d unused screenshots, total of %.2fMB freed.',
- $items, $bytes/1024/1024) if $items;
- $_[KERNEL]->call(core => log => 2, 'No unused screenshots found') if !$items;
- $_[KERNEL]->yield('scr_finish');
-}
+# non-POE helper function
+sub do_resize { # im, [ maxwidth, maxheight ]
+ my($im, $dim) = @_;
-sub scr_finish { # [id]
- if($_[ARG0]) {
- $_[HEAP]{todo} = [ grep $_!=$_[ARG0], @{$_[HEAP]{todo}} ];
- return $_[KERNEL]->yield(scr_process => $_[HEAP]{todo}[0])
- if @{$_[HEAP]{todo}};
+ my($w, $h) = ($im->Get('width'), $im->Get('height'));
+ $dim = [ $w, $h ] if !$dim;
+ my($ow, $oh) = ($w, $h);
+ if($w > $$dim[0] || $h > $$dim[1]) {
+ if($w/$h > $$dim[0]/$$dim[1]) { # width is the limiting factor
+ $h *= $$dim[0]/$w;
+ $w = $$dim[0];
+ } else {
+ $w *= $$dim[1]/$h;
+ $h = $$dim[1];
+ }
}
+ $im->Thumbnail(width => $w, height => $h);
- $_[KERNEL]->post(core => finish => $_[HEAP]{curcmd});
- delete @{$_[HEAP]}{qw| curcmd todo |};
+ return ([$ow, $oh], [$w, $h]);
}
-
1;
diff --git a/lib/Multi/Maintenance.pm b/lib/Multi/Maintenance.pm
index d50f3ce9..9e268d68 100644
--- a/lib/Multi/Maintenance.pm
+++ b/lib/Multi/Maintenance.pm
@@ -3,58 +3,128 @@
# Multi::Maintenance - General maintenance functions
#
+# TODO: more logging?
+
package Multi::Maintenance;
use strict;
use warnings;
use POE;
use PerlIO::gzip;
-use Time::HiRes 'gettimeofday', 'tv_interval';
sub spawn {
- # WARNING: these maintenance tasks can block the process for a few seconds
-
my $p = shift;
POE::Session->create(
package_states => [
- $p => [qw| _start cmd_maintenance vncache usercache statscache revcache integrity unkanime logrotate vnpopularity tagcache |],
+ $p => [qw|
+ _start shutdown set_daily daily set_monthly monthly
+ vncache tagcache vnpopularity
+ usercache statscache revcache logrotate
+ |],
],
+ heap => {
+ daily => [qw|vncache tagcache vnpopularity|],
+ monthly => [qw|usercache statscache revcache logrotate|],
+ @_,
+ },
);
}
sub _start {
$_[KERNEL]->alias_set('maintenance');
- $_[KERNEL]->call(core => register => qr/^maintenance((?: (?:vncache|revcache|usercache|statscache|integrity|unkanime|logrotate|vnpopularity|tagcache))+)$/, 'cmd_maintenance');
-
- # recalculate tag<->vn cache each hour (better do this once every 24 hours when the DB grows)
- $_[KERNEL]->post(core => addcron => '0 * * * *', 'maintenance tagcache');
- # Perform some maintenance functions every day on 0:00
- $_[KERNEL]->post(core => addcron => '0 0 * * *', 'maintenance vncache integrity unkanime vnpopularity');
- # update caches and rotate logs every 1st day of the month at 0:05
- $_[KERNEL]->post(core => addcron => '5 0 1 * *' => 'maintenance usercache statscache revcache logrotate');
+ $_[KERNEL]->sig(shutdown => 'shutdown');
+ $_[KERNEL]->yield('set_daily');
+ $_[KERNEL]->yield('set_monthly');
+}
+
+
+sub shutdown {
+ $_[KERNEL]->delay('daily');
+ $_[KERNEL]->delay('monthly');
+ $_[KERNEL]->alias_remove('maintenance');
+}
+
+
+sub set_daily {
+ # run daily each day at 0:00 GMT
+ # (GMT because we're calculating on the UNIX timestamp, I can easily add an
+ # offset if necessary, but it doesn't really matter what time this cron
+ # runs, as long as it's run on a daily basis)
+ $_[KERNEL]->alarm(daily => int(time/86400+1)*86400);
+}
+
+
+sub daily {
+ $_[KERNEL]->call(core => log => 'Running daily cron: %s', join ', ', @{$_[HEAP]{daily}});
+
+ # dispatch events that need to be run on a daily basis
+ $_[KERNEL]->call($_[SESSION], $_) for (@{$_[HEAP]{daily}});
+
+ # re-activate timer
+ $_[KERNEL]->call($_[SESSION], 'set_daily');
}
-sub cmd_maintenance {
- $_[KERNEL]->yield($_)
- for (split /\s+/, $_[ARG1]);
+sub set_monthly {
+ # Calculate the UNIX timestamp of 0:00 GMT of the first day of the next month.
+ # We do this by simply incrementing the timestamp with one day and checking gmtime()
+ # for a month change. This might not be very reliable, but should be enough for
+ # our purposes.
+ my $nextday = int(time/86400+1)*86400;
+ my $thismonth = (gmtime)[5]*100+(gmtime)[4]; # year*100 + month, for easy comparing
+ $nextday += 86400 while (gmtime $nextday)[5]*100+(gmtime $nextday)[4] <= $thismonth;
+ $_[KERNEL]->alarm(monthly => $nextday);
+}
+
+
+sub monthly {
+ $_[KERNEL]->call(core => log => 'Running monthly cron: %s', join ', ', @{$_[HEAP]{monthly}});
+
+ # dispatch events that need to be run on a monthly basis
+ $_[KERNEL]->call($_[SESSION], $_) for (@{$_[HEAP]{monthly}});
- $_[KERNEL]->post(core => finish => $_[ARG0]);
+ # re-activate timer
+ $_[KERNEL]->call($_[SESSION], 'set_monthly');
}
+
+#
+# D A I L Y J O B S
+#
+
+
sub vncache {
- $_[KERNEL]->call(core => log => 3 => 'Updating c_* columns in the vn table...');
- # takes ~5 seconds, better do this in the background...
- $Multi::SQL->do('SELECT update_vncache(0)');
+ # this takes about 30s to complete. We really need to search for an alternative
+ # method of keeping the c_* columns in the vn table up-to-date.
+ $_[KERNEL]->post(pg => do => 'SELECT update_vncache(0)');
+}
+
+
+sub tagcache {
+ # this still takes "only" about 3 seconds max. Let's hope that doesn't increase too much.
+ $_[KERNEL]->post(pg => do => 'SELECT tag_vn_calc()');
}
+sub vnpopularity {
+ # still takes at most 2 seconds. Againt, let's hope that doesn't increase...
+ $_[KERNEL]->post(pg => do => 'SELECT update_vnpopularity()');
+}
+
+
+
+#
+# M O N T H L Y J O B S
+#
+
+
sub usercache {
- $_[KERNEL]->call(core => log => 3 => 'Updating c_* columns in the users table...');
- $Multi::SQL->do(q|UPDATE users SET
+ # Shouldn't really be necessary, except c_changes could be slightly off when hiding/unhiding DB items
+ # Currently takes about 25 seconds to complete.
+ $_[KERNEL]->post(pg => do => q|UPDATE users SET
c_votes = COALESCE(
(SELECT COUNT(vid)
FROM votes
@@ -78,8 +148,9 @@ sub usercache {
sub statscache {
- $_[KERNEL]->call(core => log => 3 => 'Updating the stats_cache table...');
- $Multi::SQL->do($_) for(
+ # Shouldn't really be necessary, the triggers in PgSQL should keep these up-to-date nicely.
+ # But it takes less than 100ms to complete, anyway
+ $_[KERNEL]->post(pg => do => $_) for(
q|UPDATE stats_cache SET count = (SELECT COUNT(*) FROM users)-1 WHERE section = 'users'|,
q|UPDATE stats_cache SET count = (SELECT COUNT(*) FROM vn WHERE hidden = FALSE) WHERE section = 'vn'|,
q|UPDATE stats_cache SET count = (SELECT COUNT(*) FROM releases WHERE hidden = FALSE) WHERE section = 'releases'|,
@@ -92,66 +163,9 @@ sub statscache {
sub revcache {
- $_[KERNEL]->call(core => log => 3 => 'Updating rev column in the changes table...');
- # this can take a while, maybe split these up in 3 queries?
- # ...or better yet, use asynchronous communication with PgSQL
- $Multi::SQL->do(q|SELECT update_rev('vn', ''), update_rev('releases', ''), update_rev('producers', '')|);
-}
-
-
-sub integrity {
- # checks for database inconsistencies not handled by the foreign key constraints:
- # - releases without a VN relation
- # - changes without an entry in the (vn|releases|producers)_rev table
- # - threads without a tag or post
-
- my $q = $Multi::SQL->prepare(q|
- SELECT 'r', id FROM releases_rev rr
- WHERE NOT EXISTS(SELECT 1 FROM releases_vn rv WHERE rr.id = rv.rid)
- UNION
- SELECT c.type::varchar, id FROM changes c
- WHERE (c.type = 0 AND NOT EXISTS(SELECT 1 FROM vn_rev vr WHERE vr.id = c.id))
- OR (c.type = 1 AND NOT EXISTS(SELECT 1 FROM releases_rev rr WHERE rr.id = c.id))
- OR (c.type = 2 AND NOT EXISTS(SELECT 1 FROM producers_rev pr WHERE pr.id = c.id))
- UNION
- SELECT 't', id FROM threads t
- WHERE NOT EXISTS(SELECT 1 FROM threads_posts tp WHERE tp.tid = t.id)
- OR NOT EXISTS(SELECT 1 FROM threads_boards tb WHERE tb.tid = t.id)|);
- $q->execute();
- my $r = $q->fetchall_arrayref([]);
- if(@$r) {
- $_[KERNEL]->call(core => log => 1, '!DATABASE INCONSISTENCIES FOUND!: %s',
- join(', ', map { $_->[0].':'.$_->[1] } @$r));
- } else {
- $_[KERNEL]->call(core => log => 3, 'No database inconsistencies found');
- }
-}
-
-
-sub unkanime {
- # warn for VNs with a non-existing anidb id
- # (maybe do an automated edit or something in the future)
-
- my $q = $Multi::SQL->prepare(q|
- SELECT v.id, va.aid
- FROM vn_anime va
- JOIN vn v ON va.vid = v.latest
- JOIN anime a ON va.aid = a.id
- WHERE a.lastfetch < 0|);
- $q->execute();
- my $r = $q->fetchall_arrayref([]);
- my %aid = map {
- my $a=$_;
- $a->[1] => join(',', map { $a->[1] == $_->[1] ? $_->[0] : () } @$r)
- } @$r;
-
- if(keys %aid) {
- $_[KERNEL]->call(core => log => 1, '!NON-EXISTING RELATED ANIME FOUND!: %s',
- join('; ', map { 'a'.$_.':v'.$aid{$_} } keys %aid)
- );
- } else {
- $_[KERNEL]->call(core => log => 3, 'No problems found with the related anime');
- }
+ # This -really- shouldn't be necessary...
+ # Currently takes about 25 seconds to complete
+ $_[KERNEL]->post(pg => do => q|SELECT update_rev('vn', ''), update_rev('releases', ''), update_rev('producers', '')|);
}
@@ -164,7 +178,7 @@ sub logrotate {
my $f = /([^\/]+)$/ ? $1 : $_;
my $n = sprintf '%s/%s.%04d-%02d-%02d.gz', $dir, $f, (localtime)[5]+1900, (localtime)[4]+1, (localtime)[3];
if(-f $n) {
- $_[KERNEL]->call(core => log => 1, 'Logs already rotated earlier today!');
+ $_[KERNEL]->call(core => log => 'Logs already rotated earlier today!');
return;
}
open my $I, '<', sprintf '%s/%s', $VNDB::M{log_dir}, $f;
@@ -175,23 +189,9 @@ sub logrotate {
open $I, '>', sprintf '%s/%s', $VNDB::M{log_dir}, $f;
close $I;
}
-}
-
-
-sub vnpopularity {
- my $S = [gettimeofday];
- $Multi::SQL->do(q|SELECT update_vnpopularity()|);
- $_[KERNEL]->call(core => log => 3 => '(Re)calculated vn.c_popularity in %.2fs', tv_interval($S));
-}
-
-
-sub tagcache {
- my $S = [gettimeofday];
- $Multi::SQL->do(q|SELECT tag_vn_calc()|);
- $_[KERNEL]->call(core => log => 3 => '(Re)calculated tags_vn_stored in %.2fs', tv_interval($S));
+ $_[KERNEL]->call(core => log => 'Logs rotated.');
}
1;
-
diff --git a/lib/Multi/RG.pm b/lib/Multi/RG.pm
index d16d63b5..e427caf2 100644
--- a/lib/Multi/RG.pm
+++ b/lib/Multi/RG.pm
@@ -9,6 +9,7 @@ use strict;
use warnings;
use POE 'Wheel::Run', 'Filter::Stream';
use Encode 'encode_utf8';
+use Time::HiRes 'time';
sub spawn {
@@ -16,8 +17,7 @@ sub spawn {
POE::Session->create(
package_states => [
$p => [qw|
- _start cmd_relgraph
- creategraph getrel builddot buildgraph savegraph completegraph
+ _start shutdown check_rg creategraph getrel builddot buildgraph savegraph
proc_stdin proc_stdout proc_stderr proc_closed proc_child
|],
],
@@ -27,6 +27,7 @@ sub spawn {
imgdir => '/www/vndb/static/rg',
moy => [qw| Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec |],
dot => '/usr/bin/dot',
+ check_delay => 3600,
@_,
}
);
@@ -36,94 +37,71 @@ sub spawn {
sub _start {
$_[KERNEL]->alias_set('rg');
$_[KERNEL]->sig(CHLD => 'proc_child');
- $_[KERNEL]->call(core => register => qr/^relgraph ((?:[0-9]+)(?:\s+[0-9]+)*|all)$/, 'cmd_relgraph');
-
- # regenerate all relation graphs once a month
- $_[KERNEL]->post(core => addcron => '0 3 1 * *', 'relgraph all');
+ $_[KERNEL]->sig(shutdown => 'shutdown');
+ $_[KERNEL]->post(pg => listen => relgraph => 'check_rg');
+ $_[KERNEL]->yield('check_rg');
}
-sub cmd_relgraph {
- $_[HEAP]{curcmd} = $_[ARG0];
+sub shutdown {
+ $_[KERNEL]->delay('check_rg');
+ $_[KERNEL]->post(pg => unlisten => 'relgraph');
+ $_[KERNEL]->alias_remove('rg');
+}
- # determine vns to generate graphs for
- if($_[ARG1] ne 'all') {
- $_[HEAP]{todo} = [ split /\s/, $_[ARG1] ];
- } else {
- my $q = $Multi::SQL->prepare('SELECT id FROM vn WHERE hidden = FALSE');
- $q->execute;
- $_[HEAP]{todo} = [ map { $_->[0] } @{$q->fetchall_arrayref([])} ];
- }
- # generate first graph
- $_[KERNEL]->yield(creategraph => $_[HEAP]{todo}[0]);
+sub check_rg {
+ return if $_[HEAP]{vid};
+ $_[KERNEL]->call(pg => query =>
+ 'SELECT v.id FROM vn v JOIN vn_relations vr ON vr.vid1 = v.latest WHERE rgraph IS NULL AND hidden = FALSE LIMIT 1',
+ undef, 'creategraph');
}
-sub creategraph { # id
- # Function order:
- # creategraph (inits vars and initates getrel)
- # getrel (recursive - fetches relation and vn data)
- # if !rels
- # completegraph (checks for other vids in the queue, exits otherwise)
- # else
- # builddot (creates input for graphviz)
- # buildgraph (fetches graph ID and calls grapviz)
- # savegraph (writes cmap, chmods files, updates database entries)
- # completegraph
-
- $_[KERNEL]->call(core => log => 3, 'Processing graph for v%d', $_[ARG0]);
-
- $_[HEAP]{rels} = {}; # relations (key=vid1-vid2, value=relation)
- $_[HEAP]{nodes} = {}; # nodes (key=vid, value=[ vid, title, date, lang, processed ])
- $_[HEAP]{vid} = $_[ARG0];
- $_[KERNEL]->yield(getrel => $_[ARG0]);
+sub creategraph { # num, res
+ return $_[KERNEL]->delay('check_rg', $_[HEAP]{check_delay}) if $_[ARG0] == 0;
+
+ $_[HEAP]{start} = time;
+ $_[HEAP]{vid} = $_[ARG1][0]{id};
+ $_[HEAP]{rels} = {}; # relations (key=vid1-vid2, value=relation)
+ $_[HEAP]{nodes} = {}; # nodes (key=vid, value= 0:found, 1:processed)
+
+ $_[KERNEL]->post(pg => query =>
+ 'SELECT vid2 AS id, relation FROM vn v JOIN vn_relations vr ON vr.vid1 = v.latest WHERE v.id = ?',
+ [ $_[HEAP]{vid} ], 'getrel', $_[HEAP]{vid});
}
-sub getrel { # vid
- $_[KERNEL]->call(core => log => 3, 'Fetching relations for v%d', $_[ARG0]);
+sub getrel { # num, res, vid
+ my $id = $_[ARG2];
+ $_[HEAP]{nodes}{$id} = 1;
- my $s = $Multi::SQL->prepare(q|
- SELECT vr1.vid AS vid1, r.vid2, r.relation, vr1.title AS title1, vr2.title AS title2,
- v1.c_released AS date1, v2.c_released AS date2, v1.c_languages AS lang1, v2.c_languages AS lang2
- FROM vn_relations r
- JOIN vn_rev vr1 ON r.vid1 = vr1.id
- JOIN vn v1 ON v1.latest = vr1.id
- JOIN vn v2 ON r.vid2 = v2.id
- JOIN vn_rev vr2 ON v2.latest = vr2.id
- WHERE (r.vid2 = ? OR vr1.vid = ?)|
- );
- $s->execute($_[ARG0], $_[ARG0]);
- while(my $r = $s->fetchrow_hashref) {
- $_[HEAP]{rels}{$r->{vid1}.'-'.$r->{vid2}} = reverserel($r->{relation}) if $r->{vid1} < $r->{vid2};
- $_[HEAP]{rels}{$r->{vid2}.'-'.$r->{vid1}} = $r->{relation} if $r->{vid1} > $r->{vid2};
-
- for (1,2) {
- my($vid, $title, $date, $lang) = @$r{ "vid$_", "title$_", "date$_", "lang$_" };
- if(!$_[HEAP]{nodes}{$vid}) {
- $_[HEAP]{nodes}{$vid} = [ $vid, $title, $date, $lang, 0 ];
- $_[KERNEL]->yield(getrel => $vid) if $vid != $_[ARG0];
- }
+ for($_[ARG0] > 0 ? @{$_[ARG1]} : ()) {
+ $_[HEAP]{rels}{$id.'-'.$_->{id}} = reverserel($_->{relation}) if $id < $_->{id};
+ $_[HEAP]{rels}{$_->{id}.'-'.$id} = $_->{relation} if $id > $_->{id};
+
+ if(!exists $_[HEAP]{nodes}{$_->{id}}) {
+ $_[HEAP]{nodes}{$_->{id}} = 0;
+ $_[KERNEL]->post(pg => query =>
+ 'SELECT vid2 AS id, relation FROM vn v JOIN vn_relations vr ON vr.vid1 = v.latest WHERE v.id = ?',
+ [ $_->{id} ], 'getrel', $_->{id});
}
- $_[HEAP]{nodes}{$_[ARG0]}[4]++;
}
- if(!grep !$_->[4], values %{$_[HEAP]{nodes}}) {
- if(!keys %{$_[HEAP]{nodes}}) {
- $_[KERNEL]->call(core => log => 3, 'No relation graph for v%d', $_[HEAP]{vid});
- $Multi::SQL->do('UPDATE vn SET rgraph = NULL WHERE id = ?', undef, $_[HEAP]{vid});
- $_[HEAP]{nodes}{$_[HEAP]{vid}} = [];
- $_[KERNEL]->yield('completegraph');
- return;
- }
- $_[KERNEL]->call(core => log => 3, 'Fetched all relation data');
- $_[KERNEL]->yield('builddot')
+ # do we have all relations now? get VN info
+ if(!grep !$_, values %{$_[HEAP]{nodes}}) {
+ $_[KERNEL]->post(pg => query =>
+ 'SELECT v.id, vr.title, v.c_released AS date, v.c_languages AS lang
+ FROM vn v JOIN vn_rev vr ON vr.id = v.latest
+ WHERE v.id IN('.join(', ', map '?', keys %{$_[HEAP]{nodes}}).')',
+ [ keys %{$_[HEAP]{nodes}} ], 'builddot');
}
}
-sub builddot {
+sub builddot { # num, res
+ my $vns = $_[ARG1];
+
my $gv =
qq|graph rgraph {\n|.
qq|\tratio = "compress"\n|.
@@ -133,18 +111,18 @@ sub builddot {
qq|\tedge [ labeldistance = 2.5, labelangle = -20, labeljust = 1, minlen = 2, dir = "both",|.
qq| fontname = $_[HEAP]{font}, fontsize = $_[HEAP]{fsize}[1], arrowsize = 0.7, color = "#225588", fontcolor = "#cccccc" ]\n|;
- # insert all nodes, ordered by release date
- for (sort { $a->[2] <=> $b->[2] } values %{$_[HEAP]{nodes}}) {
- my $date = sprintf '%08d', $_->[2];
+ # insert all nodes, ordered by release date
+ for (sort { $a->{date} <=> $b->{date} } @$vns) {
+ my $date = sprintf '%08d', $_->{date};
$date =~ s#^([0-9]{4})([0-9]{2}).+#$1==0?'N/A':$1==9999?'TBA':(($2&&$2<13?($_[HEAP]{moy}[$2-1].' '):'').$1)#e;
- my $title = $_->[1];
+ my $title = $_->{title};
$title = substr($title, 0, 27).'...' if length($title) > 30;
$title =~ s/&/&amp;/g;
$title =~ s/>/&gt;/g;
$title =~ s/</&lt;/g;
- my $tooltip = $_->[1];
+ my $tooltip = $_->{title};
$tooltip =~ s/\\/\\\\/g;
$tooltip =~ s/"/\\"/g;
@@ -154,99 +132,85 @@ sub builddot {
q|<TR><TD COLSPAN="2" ALIGN="CENTER" CELLPADDING="2"><FONT POINT-SIZE="%d"> %s </FONT></TD></TR>|.
q|<TR><TD> %s </TD><TD> %s </TD></TR>|.
qq|</TABLE>> ]\n|,
- $_->[0], $_->[0], encode_utf8($tooltip), $_[HEAP]{fsize}[2], encode_utf8($title), $date, $_->[3]||'N/A';
+ $_->{id}, $_->{id}, encode_utf8($tooltip), $_[HEAP]{fsize}[2], encode_utf8($title), $date, $_->{lang}||'N/A';
}
- # @rels = ([ vid1, vid2, relation, date1, date2 ], ..), for easier processing
+ # @rels = ([ vid1, vid2, relation, date1, date2 ], ..), for easier processing
my @rels = map {
/^([0-9]+)-([0-9]+)$/;
- [ $1, $2, $_[HEAP]{rels}{$_}, $_[HEAP]{nodes}{$1}[2], $_[HEAP]{nodes}{$2}[2] ]
+ my $vn1 = (grep $1 == $_->{id}, @$vns)[0];
+ my $vn2 = (grep $2 == $_->{id}, @$vns)[0];
+ [ $1, $2, $_[HEAP]{rels}{$_}, $vn1->{date}, $vn2->{date} ]
} keys %{$_[HEAP]{rels}};
- # insert all edges, ordered by release date again
+ # insert all edges, ordered by release date again
for (sort { ($a->[3]>$a->[4]?$a->[4]:$a->[3]) <=> ($b->[3]>$b->[4]?$b->[4]:$b->[3]) } @rels) {
- # [older game] -> [newer game]
+ # [older game] -> [newer game]
if($_->[4] > $_->[3]) {
($_->[0], $_->[1]) = ($_->[1], $_->[0]);
$_->[2] = reverserel($_->[2]);
}
-
my $label =
- $VNDB::S{vn_relations}[$_->[2]][1] ? qq|headlabel = "$VNDB::S{vn_relations}[$_->[2]][0]", taillabel = "$VNDB::S{vn_relations}[$_->[2]-1][0]"| :
- $VNDB::S{vn_relations}[$_->[2]+1][1] ? qq|headlabel = "$VNDB::S{vn_relations}[$_->[2]][0]", taillabel = "$VNDB::S{vn_relations}[$_->[2]+1][0]"|
- : qq|label = " $VNDB::S{vn_relations}[$_->[2]][0]"|;
-
+ $VNDB::S{vn_relations}[$_->[2]][1]
+ ? qq|headlabel = "$VNDB::S{vn_relations}[$_->[2]][0]", taillabel = "$VNDB::S{vn_relations}[$_->[2]-1][0]"| :
+ $VNDB::S{vn_relations}[$_->[2]+1][1]
+ ? qq|headlabel = "$VNDB::S{vn_relations}[$_->[2]][0]", taillabel = "$VNDB::S{vn_relations}[$_->[2]+1][0]"|
+ : qq|label = " $VNDB::S{vn_relations}[$_->[2]][0]"|;
$gv .= qq|\tv$$_[1] -- v$$_[0] [ $label ]\n|;
}
$gv .= "}\n";
- #print $gv;
- $_[HEAP]{gv} = \$gv;
- $_[KERNEL]->yield('buildgraph');
+
+ # get ID
+ $_[KERNEL]->post(pg => query => 'INSERT INTO relgraph (cmap) VALUES (\'\') RETURNING id', undef, 'buildgraph', \$gv);
}
-sub buildgraph {
- # get a new ID
- my $gid = $Multi::SQL->prepare("INSERT INTO relgraph (cmap) VALUES ('') RETURNING id");
- $gid->execute;
- $gid = $gid->fetchrow_arrayref->[0];
- $_[HEAP]{gid} = [
- $gid,
- sprintf('%s/%02d/%d.png', $_[HEAP]{imgdir}, $gid % 100, $gid),
- ];
+sub buildgraph { # num, res, \$gv
+ $_[HEAP]{gid} = $_[ARG1][0]{id};
+ $_[HEAP]{graph} = sprintf('%s/%02d/%d.png', $_[HEAP]{imgdir}, $_[ARG1][0]{id} % 100, $_[ARG1][0]{id});
+ $_[HEAP]{cmap} = '';
# roughly equivalent to:
# cat layout.txt | dot -Tpng -o graph.png -Tcmapx
$_[HEAP]{proc} = POE::Wheel::Run->new(
Program => $_[HEAP]{dot},
- ProgramArgs => [ '-Tpng', '-o', $_[HEAP]{gid}[1], '-Tcmapx' ],
+ ProgramArgs => [ '-Tpng', '-o', $_[HEAP]{graph}, '-Tcmapx' ],
StdioFilter => POE::Filter::Stream->new(),
StdinEvent => 'proc_stdin',
StdoutEvent => 'proc_stdout',
StderrEvent => 'proc_stderr',
CloseEvent => 'proc_closed',
);
- $_[HEAP]{proc}->put(${$_[HEAP]{gv}});
- $_[HEAP]{cmap} = '';
+ $_[HEAP]{proc}->put(${$_[ARG2]});
}
sub savegraph {
- # save the image map
- $Multi::SQL->do('UPDATE relgraph SET cmap = ? WHERE id = ?', undef,
- '<!-- V:'.join(',',keys %{$_[HEAP]{nodes}})." -->\n$_[HEAP]{cmap}", $_[HEAP]{gid}[0]);
+ my $vids = join ',', sort map int, keys %{$_[HEAP]{nodes}};
- # proper chmod
- chmod 0666, $_[HEAP]{gid}[1];
+ # chmod graph
+ chmod 0666, $_[HEAP]{graph};
- # update the VN table
- $Multi::SQL->do(sprintf q|
- UPDATE vn
- SET rgraph = %d
- WHERE id IN(%s)|,
- $_[HEAP]{gid}[0], join(',', keys %{$_[HEAP]{nodes}}));
+ # save the image map in the database
+ $_[KERNEL]->post(pg => do => 'UPDATE relgraph SET cmap = ? WHERE id = ?',
+ [ "<!-- V:$vids -->\n$_[HEAP]{cmap}", $_[HEAP]{gid} ]);
- $_[KERNEL]->yield('completegraph');
-}
+ # update the VN table
+ $_[KERNEL]->post(pg => do => "UPDATE vn SET rgraph = ? WHERE id IN($vids)", [ $_[HEAP]{gid} ]);
+ # log
+ $_[KERNEL]->call(core => log => 'Generated relation graph in %.2fs, V: %s', time-$_[HEAP]{start}, $vids);
-sub completegraph {
- $_[KERNEL]->call(core => log => 3, 'Generated the relation graph for v%d', $_[HEAP]{vid});
+ # clean up
+ delete @{$_[HEAP]}{qw| start vid nodes rels gid graph cmap proc |};
- # remove processed vns, and check for other graphs in the queue
- $_[HEAP]{todo} = [ grep { !$_[HEAP]{nodes}{$_} } @{$_[HEAP]{todo}} ];
- if(@{$_[HEAP]{todo}}) {
- $_[KERNEL]->yield(creategraph => $_[HEAP]{todo}[0]);
- } else {
- $_[KERNEL]->post(core => finish => $_[HEAP]{curcmd});
- delete @{$_[HEAP]}{qw| vid nodes rels curcmd gv todo gid cmap |};
- }
+ # check for more things to do
+ $_[KERNEL]->yield('check_rg');
}
-
# POE handlers for communication with GraphViz
sub proc_stdin {
$_[HEAP]{proc}->shutdown_stdin;
@@ -255,11 +219,10 @@ sub proc_stdout {
$_[HEAP]{cmap} .= $_[ARG0];
}
sub proc_stderr {
- $_[KERNEL]->call(core => log => 1, 'GraphViz STDERR: %s', $_[ARG0]);
+ $_[KERNEL]->call(core => log => 'GraphViz STDERR: %s', $_[ARG0]);
}
sub proc_closed {
$_[KERNEL]->yield('savegraph');
- undef $_[HEAP]{proc};
}
sub proc_child {
1; # do nothing, just make sure SIGCHLD is handled to reap the process
@@ -267,8 +230,7 @@ sub proc_child {
-
-# Not a POE handler, just a small macro
+# non-POE helper function
sub reverserel { # relation
return $VNDB::S{vn_relations}[$_[0]][1] ? $_[0]-1 : $VNDB::S{vn_relations}[$_[0]+1][1] ? $_[0]+1 : $_[0];
}
diff --git a/lib/Multi/Sitemap.pm b/lib/Multi/Sitemap.pm
index 5ae305c1..175b170a 100644
--- a/lib/Multi/Sitemap.pm
+++ b/lib/Multi/Sitemap.pm
@@ -10,17 +10,20 @@ use warnings;
use POE;
use XML::Writer;
use PerlIO::gzip;
-use DateTime;
+use POSIX 'strftime';
+use Time::HiRes 'gettimeofday', 'tv_interval';
sub spawn {
my $p = shift;
POE::Session->create(
package_states => [
- $p => [qw| _start cmd_sitemap staticpages vnpages releasepages producerpages finish addurl |],
+ $p => [qw| _start shutdown check_age generate addquery addurl finish |],
],
heap => {
output => $VNDB::ROOT.'/www/sitemap.xml.gz',
+ max_age => 24*3600, # seconds
+ check_delay => 3600, # seconds
@_,
}
);
@@ -29,24 +32,31 @@ sub spawn {
sub _start {
$_[KERNEL]->alias_set('sitemap');
- $_[KERNEL]->call(core => register => qr/^sitemap$/, 'cmd_sitemap');
-
- # Regenerate the sitemap every day on 0:00
- $_[KERNEL]->post(core => addcron => '0 0 * * *', 'sitemap');
+ $_[KERNEL]->yield('check_age');
+ $_[KERNEL]->sig(shutdown => 'shutdown');
}
-sub cmd_sitemap {
- # Function order:
- # cmd_sitemap
- # staticpages
- # vnpages
- # releasepages
- # producerpages
- # finish
+sub shutdown {
+ $_[KERNEL]->delay('check_age');
+ $_[KERNEL]->alias_remove('sitemap');
+}
+
+
+sub check_age {
+ # check the last modified time of the sitemap, and if it's older than max_age, regenerate it
+ $_[KERNEL]->yield('generate') if !-f $_[HEAP]{output} || (stat $_[HEAP]{output})[9] < time-$_[HEAP]{max_age};
+
+ # check sitemap again later
+ $_[KERNEL]->delay(check_age => $_[HEAP]{check_delay});
+}
+
+
+sub generate {
+ $_[KERNEL]->call(core => log => '(Re)generating sitemap');
- $_[HEAP]{cmd} = $_[ARG0];
$_[HEAP]{urls} = 0;
+ $_[HEAP]{start} = [ gettimeofday ];
open($_[HEAP]{io}, '>:gzip', $_[HEAP]{output}) || die $1;
$_[HEAP]{xml} = new XML::Writer(
@@ -56,76 +66,63 @@ sub cmd_sitemap {
DATA_INDENT => 1
);
$_[HEAP]{xml}->xmlDecl();
- $_[HEAP]{xml}->comment(q|NOTE: All URL's that require you to login or that may contain usernames are left out.|);
$_[HEAP]{xml}->startTag('urlset', xmlns => 'http://www.sitemaps.org/schemas/sitemap/0.9');
- $_[KERNEL]->yield('staticpages');
-}
-
-
-sub staticpages {
- $_[KERNEL]->call(core => log => 3, 'Adding static pages');
-
+ # /
$_[KERNEL]->call(sitemap => addurl => '', 'daily');
+ # /d+
/([0-9]+)$/ && $_[KERNEL]->call(sitemap => addurl => 'd'.$1, 'monthly', [stat $_]->[9])
for (glob "$VNDB::ROOT/data/docs/*");
+ # /v/[browse] & /p/[browse]
$_[KERNEL]->call(sitemap => addurl => $_, 'weekly')
for (map { 'v/'.$_, 'p/'.$_ } 'a'..'z', 0, 'all');
- $_[KERNEL]->yield('vnpages');
-}
-
-
-sub vnpages {
- $_[KERNEL]->call(core => log => 3, 'Adding visual novel pages');
-
- my $q = $Multi::SQL->prepare(q|
- SELECT v.id, c.added, v.rgraph
- FROM vn v
- JOIN vn_rev vr ON vr.id = v.latest
- JOIN changes c ON vr.id = c.id
- |);
- $q->execute;
- $_[KERNEL]->call(sitemap => addurl => 'v'.$_->[0], 'weekly', $_->[1], 0.7)
- while(local $_ = $q->fetchrow_arrayref);
-
- $_[KERNEL]->yield('releasepages');
-}
-
-
-sub releasepages {
- $_[KERNEL]->call(core => log => 3, 'Adding release pages');
-
- my $q = $Multi::SQL->prepare(q|
+ # /v+
+ $_[KERNEL]->post(pg => query => '
+ SELECT v.id, c.added
+ FROM vn v
+ JOIN vn_rev vr ON vr.id = v.latest
+ JOIN changes c ON vr.id = c.id
+ WHERE v.hidden = FALSE
+ ORDER BY v.id',
+ undef, 'addquery', [ 'v', 0.7 ]);
+
+ # /r+
+ $_[KERNEL]->post(pg => query => '
SELECT r.id, c.added
- FROM releases r
- JOIN releases_rev rr ON rr.id = r.latest
- JOIN changes c ON c.id = rr.id
- |);
- $q->execute;
- $_[KERNEL]->call(sitemap => addurl => 'r'.$_->[0], 'weekly', $_->[1], 0.3)
- while(local $_ = $q->fetchrow_arrayref);
-
- $_[KERNEL]->yield('producerpages');
+ FROM releases r
+ JOIN releases_rev rr ON rr.id = r.latest
+ JOIN changes c ON c.id = rr.id
+ WHERE r.hidden = FALSE
+ ORDER BY r.id',
+ undef, 'addquery', [ 'r', 0.5 ]);
+
+ # /p+
+ $_[KERNEL]->post(pg => query => '
+ SELECT p.id, c.added
+ FROM producers p
+ JOIN producers_rev pr ON pr.id = p.latest
+ JOIN changes c ON c.id = pr.id
+ WHERE p.hidden = FALSE
+ ORDER BY p.id',
+ undef, 'addquery', [ 'p', 0.3 ]);
+
+ # /g+
+ $_[KERNEL]->post(pg => query => '
+ SELECT t.id, t.added
+ FROM tags t
+ WHERE state = 2
+ ORDER BY t.id',
+ undef, 'addquery', [ 'g', 0.3, 1 ]);
}
-sub producerpages {
- $_[KERNEL]->call(core => log => 3, 'Adding producer pages');
-
- my $q = $Multi::SQL->prepare(q|
- SELECT p.id, c.added
- FROM producers p
- JOIN producers_rev pr ON pr.id = p.latest
- JOIN changes c ON c.id = pr.id
- |);
- $q->execute;
- $_[KERNEL]->call(sitemap => addurl => 'p'.$_->[0], 'weekly', $_->[1])
- while(local $_ = $q->fetchrow_arrayref);
-
- $_[KERNEL]->yield('finish');
+sub addquery { # num, db-res, [ type, priority, finish ]
+ $_[KERNEL]->call(sitemap => addurl => $_[ARG2][0].$_->{id}, 'weekly', $_->{added}, $_[ARG2][1])
+ for(@{$_[ARG1]});
+ $_[KERNEL]->yield('finish') if $_[ARG2][2];
}
@@ -133,9 +130,11 @@ sub finish {
$_[HEAP]{xml}->endTag('urlset');
$_[HEAP]{xml}->end();
close $_[HEAP]{io};
- $_[KERNEL]->call(core => log => 2 => 'Wrote %d URLs in the sitemap', $_[HEAP]{urls});
- $_[KERNEL]->post(core => finish => $_[HEAP]{cmd});
- delete @{$_[HEAP]}{qw| xml io cmd urls |};
+
+ $_[KERNEL]->call(core => log => 'Wrote %d URLs (%.1f kB gzipped) to the sitemap in %.2f seconds',
+ $_[HEAP]{urls}, (-s $_[HEAP]{output})/1024, tv_interval($_[HEAP]{start}));
+
+ delete @{$_[HEAP]}{qw| xml io start urls |};
}
@@ -143,7 +142,7 @@ sub addurl { # loc, changefreq, lastmod, priority
$_[HEAP]{xml}->startTag('url');
$_[HEAP]{xml}->dataElement(loc => $VNDB::S{url}.'/'.$_[ARG0]);
$_[HEAP]{xml}->dataElement(changefreq => $_[ARG1]) if defined $_[ARG1];
- $_[HEAP]{xml}->dataElement(lastmod => DateTime->from_epoch(epoch => $_[ARG2])->ymd) if defined $_[ARG2];
+ $_[HEAP]{xml}->dataElement(lastmod => strftime('%Y-%m-%d', gmtime $_[ARG2])) if defined $_[ARG2];
$_[HEAP]{xml}->dataElement(priority => $_[ARG3]) if defined $_[ARG3];
$_[HEAP]{xml}->endTag('url');
$_[HEAP]{urls}++;
diff --git a/lib/VNDB/DB/Users.pm b/lib/VNDB/DB/Users.pm
index d5cdcc6c..7e280999 100644
--- a/lib/VNDB/DB/Users.pm
+++ b/lib/VNDB/DB/Users.pm
@@ -5,7 +5,7 @@ use strict;
use warnings;
use Exporter 'import';
-our @EXPORT = qw|dbUserGet dbUserEdit dbUserAdd dbUserDel|;
+our @EXPORT = qw|dbUserGet dbUserEdit dbUserAdd dbUserDel dbSessionAdd dbSessionDel dbSessionCheck|;
# %options->{ username passwd mail order uid ip registered search results page what }
@@ -24,8 +24,6 @@ sub dbUserGet {
my %where = (
$o{username} ? (
'username = ?' => $o{username} ) : (),
- $o{passwd} ? (
- 'passwd = decode(?, \'hex\')' => $o{passwd} ) : (),
$o{firstchar} ? (
'SUBSTRING(username from 1 for 1) = ?' => $o{firstchar} ) : (),
!$o{firstchar} && defined $o{firstchar} ? (
@@ -45,7 +43,8 @@ sub dbUserGet {
);
my @select = (
- 'u.*',
+ qw|id username mail rank salt registered c_votes c_changes show_nsfw show_list skin customcss ip c_tags|,
+ q|encode(passwd, 'hex') AS passwd|,
$o{what} =~ /stats/ ? (
'(SELECT COUNT(*) FROM rlists WHERE uid = u.id) AS releasecount',
'(SELECT COUNT(DISTINCT rv.vid) FROM rlists rl JOIN releases r ON rl.rid = r.id JOIN releases_vn rv ON rv.rid = r.latest WHERE uid = u.id) AS vncount',
@@ -75,7 +74,7 @@ sub dbUserEdit {
my %h;
defined $o{$_} && ($h{$_.' = ?'} = $o{$_})
- for (qw| username mail rank show_nsfw show_list skin customcss |);
+ for (qw| username mail rank show_nsfw show_list skin customcss salt |);
$h{'passwd = decode(?, \'hex\')'} = $o{passwd}
if defined $o{passwd};
@@ -88,11 +87,11 @@ sub dbUserEdit {
}
-# username, md5(pass), mail, [ip]
+# username, pass(ecrypted), salt, mail, [ip]
sub dbUserAdd {
my($s, @o) = @_;
- $s->dbExec(q|INSERT INTO users (username, passwd, mail, ip, registered) VALUES(?, decode(?, 'hex'), ?, ?, ?)|,
- @o[0..2], $o[3]||$s->reqIP, time);
+ $s->dbExec(q|INSERT INTO users (username, passwd, salt, mail, ip, registered) VALUES(?, decode(?, 'hex'), ?, ?, ?, ?)|,
+ @o[0..3], $o[4]||$s->reqIP, time);
}
@@ -100,10 +99,10 @@ sub dbUserAdd {
sub dbUserDel {
my($s, $id) = @_;
$s->dbExec($_, $id) for (
- q|DELETE FROM vnlists WHERE uid = ?|,
q|DELETE FROM rlists WHERE uid = ?|,
q|DELETE FROM wlists WHERE uid = ?|,
q|DELETE FROM votes WHERE uid = ?|,
+ q|DELETE FROM sessions WHERE uid = ?|,
q|UPDATE changes SET requester = 0 WHERE requester = ?|,
q|UPDATE threads_posts SET uid = 0 WHERE uid = ?|,
q|DELETE FROM users WHERE id = ?|
@@ -111,4 +110,41 @@ sub dbUserDel {
}
+# Adds a session to the database
+# If no expiration is supplied the database default is used
+# uid, 40 character session token, expiration time (timestamp)
+sub dbSessionAdd {
+ my($s, @o) = @_;
+ if (defined $o[2]) {
+ $s->dbExec(q|INSERT INTO sessions (uid, token, expiration) VALUES(?, decode(?, 'hex'), ?)|,
+ @o);
+ } else {
+ $s->dbExec(q|INSERT INTO sessions (uid, token) VALUES(?, decode(?, 'hex'))|,
+ @o);
+ }
+}
+
+
+# Deletes session(s) from the database
+# If no token is supplied, all sessions for the uid are destroyed
+# uid, token (optional)
+sub dbSessionDel {
+ my($s, @o) = @_;
+ my %where = ('uid = ?' => $o[0]);
+ $where{"token = decode(?, 'hex')"} = $o[1] if $o[1];
+ $s->dbExec('DELETE FROM sessions !W', \%where);
+}
+
+
+# Queries the database for the validity of a session
+# Returns 1 if corresponding session found, 0 if not
+# uid, token
+sub dbSessionCheck {
+ my($s, @o) = @_;
+ return $s->dbRow(
+ q|SELECT count(uid) AS count FROM sessions WHERE uid = ? AND token = decode(?, 'hex') LIMIT 1|, @o
+ )->{count}||0;
+}
+
+
1;
diff --git a/lib/VNDB/DB/VN.pm b/lib/VNDB/DB/VN.pm
index 76cacf70..254e24e1 100644
--- a/lib/VNDB/DB/VN.pm
+++ b/lib/VNDB/DB/VN.pm
@@ -214,24 +214,11 @@ sub insert_rev {
$cid, $_->[1], $_->[0]
) for (@{$o->{relations}});
- if(@{$o->{anime}}) {
- $self->dbExec(q|
- INSERT INTO vn_anime (vid, aid)
- VALUES (?, ?)|,
- $cid, $_
- ) for (@{$o->{anime}});
-
- # insert unknown anime
- my $a = $self->dbAll(q|
- SELECT id FROM anime WHERE id IN(!l)|,
- $o->{anime});
- $self->dbExec(q|
- INSERT INTO anime (id) VALUES (?)|, $_
- ) for (grep {
- my $ia = $_;
- !(scalar grep $ia == $_->{id}, @$a)
- } @{$o->{anime}});
- }
+ $self->dbExec(q|
+ INSERT INTO vn_anime (vid, aid)
+ VALUES (?, ?)|,
+ $cid, $_
+ ) for (@{$o->{anime}});
}
@@ -251,7 +238,7 @@ sub dbVNCache {
# insert a new screenshot and return it's ID
# (no arguments required, as Multi is responsible for filling the entry with information)
sub dbScreenshotAdd {
- return shift->dbRow(q|INSERT INTO screenshots (status) VALUES(0) RETURNING id|)->{id};
+ return shift->dbRow(q|INSERT INTO screenshots (processed) VALUES(false) RETURNING id|)->{id};
}
diff --git a/lib/VNDB/Handler/Discussions.pm b/lib/VNDB/Handler/Discussions.pm
index 56bf9e83..d8aa2f63 100644
--- a/lib/VNDB/Handler/Discussions.pm
+++ b/lib/VNDB/Handler/Discussions.pm
@@ -210,8 +210,6 @@ sub edit {
$self->dbPostEdit($tid, $num, %post) if $num;
$nnum = $self->dbPostAdd($ntid, %post) if !$num;
- $self->multiCmd("ircnotify t$ntid.$nnum") if !$num && !$frm->{hidden};
-
return $self->resRedirect("/t$ntid".($nnum > 25 ? '/'.ceil($nnum/25) : '').'#'.$nnum, 'post');
}
}
diff --git a/lib/VNDB/Handler/Misc.pm b/lib/VNDB/Handler/Misc.pm
index 9ff2d315..ceae11ce 100644
--- a/lib/VNDB/Handler/Misc.pm
+++ b/lib/VNDB/Handler/Misc.pm
@@ -4,7 +4,7 @@ package VNDB::Handler::Misc;
use strict;
use warnings;
-use YAWF ':html';
+use YAWF ':html', ':xml';
use VNDB::Func;
@@ -15,6 +15,7 @@ YAWF::register(
qr{nospam}, \&nospam,
qr{([vrp])([1-9]\d*)/(lock|hide)}, \&itemmod,
qr{we-dont-like-ie6}, \&ie6message,
+ qr{opensearch\.xml}, \&opensearch,
# redirects for old URLs
qr{(.*[^/]+)/+}, sub { $_[0]->resRedirect("/$_[1]", 'perm') },
@@ -323,7 +324,7 @@ sub itemmod {
$self->dbItemMod($type, $iid, $act eq 'hide' ? (hidden => !$obj->{hidden}) : (locked => !$obj->{locked}));
# update cached vn info when hiding an r+ page
- $self->vnCacheUpdate(map $_->{vid}, @{$obj->{vn}})
+ $self->dbVNCache(map $_->{vid}, @{$obj->{vn}})
if $type eq 'r' && $act eq 'hide';
$self->resRedirect("/$type$iid", 'temp');
@@ -372,5 +373,24 @@ sub ie6message {
}
+sub opensearch {
+ my $self = shift;
+ $self->resHeader('Content-Type' => 'application/opensearchdescription+xml');
+ xml;
+ tag 'OpenSearchDescription',
+ xmlns => 'http://a9.com/-/spec/opensearch/1.1/', 'xmlns:moz' => 'http://www.mozilla.org/2006/browser/search/';
+ tag 'ShortName', 'VNDB';
+ tag 'LongName', 'VNDB.org visual novel search';
+ tag 'Description', 'Search visual vovels on VNDB.org';
+ tag 'Image', width => 16, height => 16, type => 'image/x-icon', $self->{url}.'/favicon.ico'
+ if -s "$VNDB::ROOT/www/favicon.ico";
+ tag 'Url', type => 'text/html', method => 'get', template => $self->{url}.'/v/all?q={searchTerms}', undef;
+ tag 'Url', type => 'application/opensearchdescription+xml', rel => 'self', template => $self->{url}.'/opensearch.xml', undef;
+ tag 'Query', role => 'example', searchTerms => 'Tsukihime', undef;
+ tag 'moz:SearchForm', $self->{url}.'/v/all';
+ end;
+}
+
+
1;
diff --git a/lib/VNDB/Handler/Producers.pm b/lib/VNDB/Handler/Producers.pm
index 5977ac1e..aa010e6e 100644
--- a/lib/VNDB/Handler/Producers.pm
+++ b/lib/VNDB/Handler/Producers.pm
@@ -121,8 +121,6 @@ sub edit {
($pid) = $self->dbProducerAdd(%$frm);
}
- $self->multiCmd("ircnotify p$pid.$rev");
-
return $self->resRedirect("/p$pid.$rev", 'post');
}
}
diff --git a/lib/VNDB/Handler/Releases.pm b/lib/VNDB/Handler/Releases.pm
index cc3f56bc..db234aea 100644
--- a/lib/VNDB/Handler/Releases.pm
+++ b/lib/VNDB/Handler/Releases.pm
@@ -354,8 +354,7 @@ sub edit {
($rev) = $self->dbReleaseEdit($rid, %opts) if !$copy && $rid;
($rid) = $self->dbReleaseAdd(%opts) if $copy || !$rid;
- $self->multiCmd("ircnotify r$rid.$rev");
- $self->vnCacheUpdate(@$new_vn, map $_->{vid}, @$vn);
+ $self->dbVNCache(@$new_vn, map $_->{vid}, @$vn);
return $self->resRedirect("/r$rid.$rev", 'post');
}
diff --git a/lib/VNDB/Handler/Tags.pm b/lib/VNDB/Handler/Tags.pm
index 6471d44f..dfcd041d 100644
--- a/lib/VNDB/Handler/Tags.pm
+++ b/lib/VNDB/Handler/Tags.pm
@@ -279,7 +279,6 @@ sub tagedit {
);
if(!$tag) {
$tag = $self->dbTagAdd(%opts);
- $self->multiCmd("ircnotify g$tag");
} else {
$self->dbTagEdit($tag, %opts, upddate => $frm->{state} == 2 && $t->{state} != 2);
}
diff --git a/lib/VNDB/Handler/Users.pm b/lib/VNDB/Handler/Users.pm
index d4755ae1..982d6089 100644
--- a/lib/VNDB/Handler/Users.pm
+++ b/lib/VNDB/Handler/Users.pm
@@ -4,7 +4,6 @@ package VNDB::Handler::Users;
use strict;
use warnings;
use YAWF ':html';
-use Digest::MD5 'md5_hex';
use VNDB::Func;
@@ -183,7 +182,9 @@ sub newpass {
if(!$frm->{_err}) {
my @chars = ( 'A'..'Z', 'a'..'z', 0..9 );
my $pass = join '', map $chars[int rand $#chars+1], 0..8;
- $self->dbUserEdit($u->{id}, passwd => md5_hex($pass));
+ my %o;
+ ($o{passwd}, $o{salt}) = $self->authPreparePass($pass);
+ $self->dbUserEdit($u->{id}, %o);
my $body = <<'__';
Hello %s,
@@ -258,7 +259,8 @@ sub register {
push @{$frm->{_err}}, 'oneaday' if !$frm->{_err} && $self->dbUserGet(ip => $self->reqIP, registered => time-24*3600)->[0]{id};
if(!$frm->{_err}) {
- $self->dbUserAdd($frm->{usrname}, md5_hex($frm->{usrpass}), $frm->{mail});
+ my ($pass, $salt) = $self->authPreparePass($frm->{usrpass});
+ $self->dbUserAdd($frm->{usrname}, $pass, $salt, $frm->{mail});
return $self->authLogin($frm->{usrname}, $frm->{usrpass}, '/');
}
}
@@ -330,10 +332,11 @@ sub edit {
$o{mail} = $frm->{mail};
$o{skin} = $frm->{skin};
$o{customcss} = $frm->{customcss};
- $o{passwd} = md5_hex($frm->{usrpass}) if $frm->{usrpass};
+ ($o{passwd}, $o{salt}) = $self->authPreparePass($frm->{usrpass}) if $frm->{usrpass};
$o{show_list} = $frm->{flags_list} ? 1 : 0;
$o{show_nsfw} = $frm->{flags_nsfw} ? 1 : 0;
$self->dbUserEdit($uid, %o);
+ $self->dbSessionDel($uid) if $frm->{usrpass};
return $self->resRedirect("/u$uid/edit?d=1", 'post') if $uid != $self->authInfo->{id} || !$frm->{usrpass};
return $self->authLogin($frm->{usrname}||$u->{username}, $frm->{usrpass}, "/u$uid/edit?d=1");
}
diff --git a/lib/VNDB/Handler/VNEdit.pm b/lib/VNDB/Handler/VNEdit.pm
index f0f12e88..2b6e4ac5 100644
--- a/lib/VNDB/Handler/VNEdit.pm
+++ b/lib/VNDB/Handler/VNEdit.pm
@@ -87,13 +87,8 @@ sub edit {
my %old = $vid ? (map { $_->{id} => $_->{relation} } @{$v->{relations}}) : ();
my %new = map { $_->[1] => $_->[0] } @$relations;
_updreverse($self, \%old, \%new, $nvid, $cid, $nrev);
- } elsif($vid && @$relations && $frm->{title} ne $b4{title}) {
- $self->multiCmd("relgraph $vid");
}
- $self->multiCmd("ircnotify v$nvid.$nrev");
- $self->multiCmd('anime') if $vid && $frm->{anime} ne $b4{anime} || !$vid && $frm->{anime};
-
return $self->resRedirect("/v$nvid.$nrev", 'post');
}
}
@@ -131,12 +126,11 @@ sub _uploadimage {
return undef;
}
- # store the file and let multi handle it
+ # get image ID and move it to the correct location
my $imgid = $self->dbVNImageId;
my $new = sprintf '%s/static/cv/%02d/%d.jpg', $VNDB::ROOT, $imgid%100, $imgid;
rename $tmp, $new or die $!;
chmod 0666, $new;
- $self->multiCmd("coverimage $imgid");
return -1*$imgid;
}
@@ -300,8 +294,6 @@ sub _updreverse {
( map { $_ => $r->{$_} } qw| title original desc alias categories img_nsfw length l_wp l_encubed l_renai l_vnn image | )
);
}
-
- $self->multiCmd('relgraph '.join(' ', $vid, keys %upd));
}
@@ -371,7 +363,6 @@ sub scrxml {
my $new = sprintf '%s/static/sf/%02d/%d.jpg', $VNDB::ROOT, $id%100, $id;
rename $tmp, $new or die $!;
chmod 0666, $new;
- $self->multiCmd('screenshot');
}
xml;
diff --git a/lib/VNDB/Handler/VNPage.pm b/lib/VNDB/Handler/VNPage.pm
index 8a3087f2..ed8ad7c0 100644
--- a/lib/VNDB/Handler/VNPage.pm
+++ b/lib/VNDB/Handler/VNPage.pm
@@ -327,9 +327,9 @@ sub _anime {
td 'Related anime';
td class => 'anime';
for (sort { ($a->{year}||9999) <=> ($b->{year}||9999) } @{$v->{anime}}) {
- if($_->{lastfetch} < 1) {
+ if(!$_->{lastfetch} || !$_->{year} || !$_->{title_romaji}) {
b;
- txt $_->{lastfetch} < 0 ? '[unknown anidb id: ' : '[no information available at this time: ';
+ txt '[no information available at this time: ';
a href => "http://anidb.net/a$_->{id}", $_->{id};
txt ']';
end;
@@ -347,8 +347,8 @@ sub _anime {
}
txt '] ';
end;
- acronym title => $_->{title_kanji}, shorten $_->{title_romaji}, 50;
- b ' ('.($self->{anime_types}[$_->{type}][0] eq 'unknown' ? '' : $self->{anime_types}[$_->{type}][0].', ').$_->{year}.')';
+ acronym title => $_->{title_kanji}||$_->{title_romaji}, shorten $_->{title_romaji}, 50;
+ b ' ('.(defined $_->{type} ? $self->{anime_types}[$_->{type}].', ' : '').$_->{year}.')';
txt "\n";
}
}
diff --git a/lib/VNDB/Util/Auth.pm b/lib/VNDB/Util/Auth.pm
index cb0751e9..b9724964 100644
--- a/lib/VNDB/Util/Auth.pm
+++ b/lib/VNDB/Util/Auth.pm
@@ -1,19 +1,18 @@
package VNDB::Util::Auth;
-# This module is just a small improvement of the 1.x equivalent
-# and is designed to work with the cookies and database of VNDB 1.x
-# without modifications. A proper and more secure (incompatible)
-# implementation should be written at some point.
use strict;
use warnings;
use Exporter 'import';
use Digest::MD5 'md5_hex';
-use Crypt::Lite;
+use Digest::SHA qw|sha1_hex sha256_hex|;
+use Time::HiRes;
+use Encode 'encode_utf8';
+use POSIX 'strftime';
-our @EXPORT = qw| authInit authLogin authLogout authInfo authCan |;
+our @EXPORT = qw| authInit authLogin authLogout authInfo authCan authPreparePass |;
# initializes authentication information and checks the vndb_auth cookie
@@ -23,11 +22,11 @@ sub authInit {
my $cookie = $self->reqCookie('vndb_auth');
return 0 if !$cookie;
- my $str = Crypt::Lite->new()->decrypt($cookie, md5_hex($self->{cookie_key}));
- return 0 if length($str) < 36;
- my $pass = substr($str, 4, 32);
- my $user = substr($str, 36);
- _authCheck($self, $user, $pass);
+ return _rmcookie($self) if length($cookie) < 41;
+ my $token = substr($cookie, 0, 40);
+ my $uid = substr($cookie, 40);
+ return _rmcookie($self) if $uid !~ /^\d+$/ || !$self->dbSessionCheck($uid, $token);
+ $self->{_auth} = $self->dbUserGet(uid => $uid, what => 'mymessages')->[0];
}
@@ -36,15 +35,21 @@ sub authInit {
sub authLogin {
my $self = shift;
my $user = lc(scalar shift);
- my $pass = md5_hex(shift);
+ my $pass = shift;
my $to = shift;
if(_authCheck($self, $user, $pass)) {
- (my $cookie = Crypt::Lite->new()->encrypt("VNDB$pass$user", md5_hex($self->{cookie_key}))) =~ s/\r?\n//g;
+ my $token = sha1_hex(join('', Time::HiRes::gettimeofday()) . join('', map chr(rand(93)+33), 1..9));
+ my $expiration = time + 31536000; # 1yr
+ my $cookie = $token . $self->{_auth}{id};
+ $self->dbSessionAdd($self->{_auth}{id}, $token);
+
+ my $expstr = strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime($expiration));
$self->resRedirect($to, 'post');
- $self->resHeader('Set-Cookie', "vndb_auth=$cookie; expires=Sat, 01-Jan-2030 00:00:00 GMT; path=/; domain=$self->{cookie_domain}");
+ $self->resHeader('Set-Cookie', "vndb_auth=$cookie; expires=$expstr; path=/; domain=$self->{cookie_domain}");
return 1;
}
+
return 0;
}
@@ -52,8 +57,16 @@ sub authLogin {
# clears authentication cookie and redirects to /
sub authLogout {
my $self = shift;
+
+ my $cookie = $self->reqCookie('vndb_auth');
+ if ($cookie && length($cookie) >= 41) {
+ my $token = substr($cookie, 0, 40);
+ my $uid = substr($cookie, 40);
+ $self->dbSessionDel($uid, $token);
+ }
+
$self->resRedirect('/', 'temp');
- $self->resHeader('Set-Cookie', "vndb_auth= ; expires=Sat, 01-Jan-2000 00:00:00 GMT; path=/; domain=$self->{cookie_domain}");
+ _rmcookie($self);
}
@@ -75,20 +88,56 @@ sub authCan {
# Checks for a valid login and writes information in _auth
-# Arguments: user, md5_hex(pass)
+# Arguments: user, pass
# Returns: 1 if login is valid, 0 otherwise
sub _authCheck {
my($self, $user, $pass) = @_;
- return 0 if
- !$user || length($user) > 15 || length($user) < 2
- || !$pass || length($pass) != 32;
+ return 0 if !$user || length($user) > 15 || length($user) < 2 || !$pass;
- my $d = $self->dbUserGet(username => $user, passwd => $pass, what => 'mymessages')->[0];
+ my $d = $self->dbUserGet(username => $user, what => 'mymessages')->[0];
return 0 if !defined $d->{id} || !$d->{rank};
- $self->{_auth} = $d;
- return 1;
+ if(_authEncryptPass($self, $pass, $d->{salt}) eq $d->{passwd}) {
+ $self->{_auth} = $d;
+ return 1;
+ }
+ if(md5_hex($pass) eq $d->{passwd}) {
+ $self->{_auth} = $d;
+ my %o;
+ ($o{passwd}, $o{salt}) = authPreparePass($self, $pass);
+ $self->dbUserEdit($d->{id}, %o);
+ return 1;
+ }
+
+ return 0;
+}
+
+
+# Encryption algorithm for user passwords
+# Arguments: self, pass, salt
+# Returns: encrypted password (in hex)
+sub _authEncryptPass{
+ my($self, $pass, $salt, $bin) = @_;
+ return sha256_hex($self->{global_salt} . encode_utf8($pass) . encode_utf8($salt));
+}
+
+
+# Prepares a plaintext password for database storage
+# Arguments: pass
+# Returns: list (pass, salt)
+sub authPreparePass{
+ my($self, $pass) = @_;
+ my $salt = join '', map chr(rand(93)+33), 1..9;
+ my $hash = _authEncryptPass($self, $pass, $salt);
+ return ($hash, $salt);
+}
+
+
+# removes the vndb_auth cookie
+sub _rmcookie {
+ $_[0]->resHeader('Set-Cookie',
+ "vndb_auth= ; expires=Sat, 01-Jan-2000 00:00:00 GMT; path=/; domain=$_[0]->{cookie_domain}");
}
diff --git a/lib/VNDB/Util/LayoutHTML.pm b/lib/VNDB/Util/LayoutHTML.pm
index 2cf3fc40..b5813330 100644
--- a/lib/VNDB/Util/LayoutHTML.pm
+++ b/lib/VNDB/Util/LayoutHTML.pm
@@ -21,6 +21,7 @@ sub htmlHeader { # %options->{ title, js, noindex, search }
title $o{title};
Link rel => 'shortcut icon', href => '/favicon.ico', type => 'image/x-icon';
Link rel => 'stylesheet', href => $self->{url_static}.'/s/'.$skin.'/style.css?'.$self->{version}, type => 'text/css', media => 'all';
+ Link rel => 'search', type => 'application/opensearchdescription+xml', title => 'VNDB VN Search', href => $self->{url}.'/opensearch.xml';
if($o{js}) {
script type => 'text/javascript', src => $self->{url_static}.'/f/forms.js?'.$self->{version}; end;
}
diff --git a/lib/VNDB/Util/Misc.pm b/lib/VNDB/Util/Misc.pm
deleted file mode 100644
index 139eb571..00000000
--- a/lib/VNDB/Util/Misc.pm
+++ /dev/null
@@ -1,50 +0,0 @@
-
-package VNDB::Util::Misc;
-
-use strict;
-use warnings;
-use Exporter 'import';
-use Tie::ShareLite ':lock';
-
-our @EXPORT = qw|multiCmd vnCacheUpdate|;
-
-
-# Sends a command to Multi
-# Argument: the commands to add to the queue, or none to send the queue to Multi
-sub multiCmd {
- my $self = shift;
-
- $self->{_multiCmd} = [] if !$self->{_multiCmd};
- return push @{$self->{_multiCmd}}, @_ if @_;
-
- return if !@{$self->{_multiCmd}};
-
- my $s = tie my %s, 'Tie::ShareLite', -key => $self->{sharedmem_key}, -create => 'yes', -destroy => 'no', -mode => 0666;
- $s->lock(LOCK_EX);
- my @q = ( ($s{queue} ? @{$s{queue}} : ()), @{$self->{_multiCmd}} );
- $s{queue} = \@q;
- $s->unlock();
- $self->{_multiCmd} = [];
-}
-
-
-# Recalculates the vn.c_* columns and regenerates the related relation graphs on any change
-# Arguments: list of vids to be updated
-sub vnCacheUpdate {
- my($self, @vns) = @_;
-
- my $before = $self->dbVNGet(id => \@vns, order => 'v.id', what => 'relations');
- $self->dbVNCache(@vns);
- my $after = $self->dbVNGet(id => \@vns, order => 'v.id');
-
- my @upd = map {
- @{$before->[$_]{relations}} && (
- $before->[$_]{c_released} != $after->[$_]{c_released}
- || $before->[$_]{c_languages} ne $after->[$_]{c_languages}
- ) ? $before->[$_]{id} : ();
- } 0..$#$before;
- $self->multiCmd('relgraph '.join(' ', @upd)) if @upd;
-}
-
-
-1;
diff --git a/static/f/forms.js b/static/f/forms.js
index 4fb02f1e..4e59796d 100644
--- a/static/f/forms.js
+++ b/static/f/forms.js
@@ -484,7 +484,7 @@ function scrCheckStatus() {
var tr;
for(var s=0;s<ls.length;s++) {
for(i=0;i<l.length-1;i++)
- if(l[i].scrId == ls[s].getAttribute('id') && ls[s].getAttribute('status') > 0)
+ if(l[i].scrId == ls[s].getAttribute('id') && ls[s].getAttribute('processed') == "1")
tr = l[i];
if(!tr)
continue;
diff --git a/static/f/icons.png b/static/f/icons.png
index 9430e7c6..47412c62 100644
--- a/static/f/icons.png
+++ b/static/f/icons.png
Binary files differ
diff --git a/util/dbgraph.pl b/util/dbgraph.pl
index b9800a0a..167680cd 100755
--- a/util/dbgraph.pl
+++ b/util/dbgraph.pl
@@ -17,12 +17,12 @@ use warnings;
my %subgraphs = (
'Producers' => [qw| FFFFCC producers producers_rev |],
- 'Releases' => [qw| C8FFC8 releases releases_rev releases_media releases_platforms releases_producers releases_vn |],
+ 'Releases' => [qw| C8FFC8 releases releases_rev releases_media releases_platforms releases_producers releases_lang releases_vn |],
'Visual Novels' => [qw| FFE6BE vn vn_rev vn_relations vn_categories vn_anime vn_screenshots |],
- 'Users' => [qw| CCFFFF users votes vnlists rlists wlists |],
+ 'Users' => [qw| CCFFFF users votes rlists wlists |],
'Discussion board' => [qw| FFDCDC threads threads_boards threads_posts |],
'Tags' => [qw| FFC8C8 tags tags_aliases tags_parents tags_vn |],
- 'Misc' => [qw| F5F5F5 changes anime screenshots relgraph stats_cache quotes |],
+ 'Misc' => [qw| F5F5F5 changes anime screenshots relgraph stats_cache quotes sessions |],
);
my %tables; # table_name => [ [ col1, pri ], ... ]
@@ -76,7 +76,7 @@ print
qq|graph G {\n|.
#qq| ratio = "compress"\n|.
#qq| overlap = "false"\n|.
- qq| rankdir = "LR"\n|.
+ #qq| rankdir = "LR"\n|.
qq| node [ shape="plaintext" ]\n|.
#qq| edge [ color="#cccccc" ]\n|.
qq| labelloc="t"\n|.
diff --git a/util/dump.sql b/util/dump.sql
index 22e80099..3a470102 100644
--- a/util/dump.sql
+++ b/util/dump.sql
@@ -20,13 +20,13 @@ SET default_with_oids = false;
-- anime
CREATE TABLE anime (
id integer NOT NULL PRIMARY KEY,
- year smallint NOT NULL DEFAULT 0,
- ann_id integer NOT NULL DEFAULT 0,
- nfo_id varchar(200) NOT NULL DEFAULT '',
- type smallint NOT NULL DEFAULT 0,
- title_romaji varchar(200) NOT NULL DEFAULT '',
- title_kanji varchar(200) NOT NULL DEFAULT '',
- lastfetch bigint NOT NULL DEFAULT 0
+ year smallint,
+ ann_id integer,
+ nfo_id varchar(200),
+ type smallint,
+ title_romaji,
+ title_kanji,
+ lastfetch timestamptz
);
-- changes
@@ -156,11 +156,19 @@ CREATE TABLE rlists (
-- screenshots
CREATE TABLE screenshots (
id SERIAL NOT NULL PRIMARY KEY,
- status smallint NOT NULL DEFAULT 0,
+ processed boolean NOT NULL DEFAULT FALSE,
width smallint NOT NULL DEFAULT 0,
height smallint NOT NULL DEFAULT 0
);
+-- sessions
+CREATE TABLE sessions (
+ uid integer NOT NULL,
+ token bytea NOT NULL,
+ expiration timestamptz NOT NULL DEFAULT (now() + '1 year'::interval),
+ PRIMARY KEY (uid, token)
+);
+
-- stats_cache
CREATE TABLE stats_cache (
section varchar(25) NOT NULL PRIMARY KEY,
@@ -248,7 +256,6 @@ CREATE TABLE users (
rank smallint NOT NULL DEFAULT 3,
passwd bytea NOT NULL DEFAULT '',
registered bigint NOT NULL DEFAULT 0,
- flags integer NOT NULL DEFAULT 7,
show_nsfw boolean NOT NULL DEFAULT FALSE,
show_list boolean NOT NULL DEFAULT TRUE,
c_votes integer NOT NULL DEFAULT 0,
@@ -256,7 +263,8 @@ CREATE TABLE users (
skin varchar(128) NOT NULL DEFAULT '',
customcss text NOT NULL DEFAULT '',
ip inet NOT NULL DEFAULT '0.0.0.0',
- c_tags integer NOT NULL DEFAULT 0
+ c_tags integer NOT NULL DEFAULT 0,
+ salt character(9) NOT NULL DEFAULT ''
);
-- vn
@@ -321,16 +329,6 @@ CREATE TABLE vn_screenshots (
PRIMARY KEY(vid, scr)
);
--- vnlists
-CREATE TABLE vnlists (
- uid integer DEFAULT 0,
- vid integer NOT NULL DEFAULT 0,
- status smallint NOT NULL DEFAULT 0,
- date bigint NOT NULL DEFAULT 0,
- comments varchar(500) NOT NULL DEFAULT '',
- PRIMARY KEY(uid, vid)
-);
-
-- votes
CREATE TABLE votes (
vid integer NOT NULL DEFAULT 0,
@@ -358,53 +356,52 @@ CREATE TABLE wlists (
-----------------------------------------------
-ALTER TABLE changes ADD FOREIGN KEY (requester) REFERENCES users (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE changes ADD FOREIGN KEY (causedby) REFERENCES changes (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE changes ADD FOREIGN KEY (requester) REFERENCES users (id);
+ALTER TABLE changes ADD FOREIGN KEY (causedby) REFERENCES changes (id);
ALTER TABLE producers ADD FOREIGN KEY (latest) REFERENCES producers_rev (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE producers_rev ADD FOREIGN KEY (id) REFERENCES changes (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE producers_rev ADD FOREIGN KEY (pid) REFERENCES producers (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE quotes ADD FOREIGN KEY (vid) REFERENCES vn (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE producers_rev ADD FOREIGN KEY (id) REFERENCES changes (id);
+ALTER TABLE producers_rev ADD FOREIGN KEY (pid) REFERENCES producers (id);
+ALTER TABLE quotes ADD FOREIGN KEY (vid) REFERENCES vn (id);
ALTER TABLE releases ADD FOREIGN KEY (latest) REFERENCES releases_rev (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE releases_lang ADD FOREIGN KEY (rid) REFERENCES releases_rev (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE releases_media ADD FOREIGN KEY (rid) REFERENCES releases_rev (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE releases_platforms ADD FOREIGN KEY (rid) REFERENCES releases_rev (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE releases_producers ADD FOREIGN KEY (rid) REFERENCES releases_rev (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE releases_producers ADD FOREIGN KEY (pid) REFERENCES producers (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE releases_rev ADD FOREIGN KEY (id) REFERENCES changes (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE releases_rev ADD FOREIGN KEY (rid) REFERENCES releases (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE releases_vn ADD FOREIGN KEY (rid) REFERENCES releases_rev (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE releases_vn ADD FOREIGN KEY (vid) REFERENCES vn (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE rlists ADD FOREIGN KEY (uid) REFERENCES users (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE rlists ADD FOREIGN KEY (rid) REFERENCES releases (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE tags ADD FOREIGN KEY (addedby) REFERENCES users (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE tags_aliases ADD FOREIGN KEY (tag) REFERENCES tags (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE tags_parents ADD FOREIGN KEY (tag) REFERENCES tags (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE tags_parents ADD FOREIGN KEY (parent) REFERENCES tags (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE tags_vn ADD FOREIGN KEY (tag) REFERENCES tags (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE tags_vn ADD FOREIGN KEY (vid) REFERENCES vn (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE tags_vn ADD FOREIGN KEY (uid) REFERENCES users (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE releases_lang ADD FOREIGN KEY (rid) REFERENCES releases_rev (id);
+ALTER TABLE releases_media ADD FOREIGN KEY (rid) REFERENCES releases_rev (id);
+ALTER TABLE releases_platforms ADD FOREIGN KEY (rid) REFERENCES releases_rev (id);
+ALTER TABLE releases_producers ADD FOREIGN KEY (rid) REFERENCES releases_rev (id);
+ALTER TABLE releases_producers ADD FOREIGN KEY (pid) REFERENCES producers (id);
+ALTER TABLE releases_rev ADD FOREIGN KEY (id) REFERENCES changes (id);
+ALTER TABLE releases_rev ADD FOREIGN KEY (rid) REFERENCES releases (id);
+ALTER TABLE releases_vn ADD FOREIGN KEY (rid) REFERENCES releases_rev (id);
+ALTER TABLE releases_vn ADD FOREIGN KEY (vid) REFERENCES vn (id);
+ALTER TABLE rlists ADD FOREIGN KEY (uid) REFERENCES users (id);
+ALTER TABLE rlists ADD FOREIGN KEY (rid) REFERENCES releases (id);
+ALTER TABLE sessions ADD FOREIGN KEY (uid) REFERENCES users (id);
+ALTER TABLE tags ADD FOREIGN KEY (addedby) REFERENCES users (id);
+ALTER TABLE tags_aliases ADD FOREIGN KEY (tag) REFERENCES tags (id);
+ALTER TABLE tags_parents ADD FOREIGN KEY (tag) REFERENCES tags (id);
+ALTER TABLE tags_parents ADD FOREIGN KEY (parent) REFERENCES tags (id);
+ALTER TABLE tags_vn ADD FOREIGN KEY (tag) REFERENCES tags (id);
+ALTER TABLE tags_vn ADD FOREIGN KEY (vid) REFERENCES vn (id);
+ALTER TABLE tags_vn ADD FOREIGN KEY (uid) REFERENCES users (id);
ALTER TABLE threads ADD FOREIGN KEY (id, count) REFERENCES threads_posts (tid, num) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE threads_posts ADD FOREIGN KEY (tid) REFERENCES threads (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE threads_posts ADD FOREIGN KEY (uid) REFERENCES users (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE threads_boards ADD FOREIGN KEY (tid) REFERENCES threads (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE threads_posts ADD FOREIGN KEY (tid) REFERENCES threads (id);
+ALTER TABLE threads_posts ADD FOREIGN KEY (uid) REFERENCES users (id);
+ALTER TABLE threads_boards ADD FOREIGN KEY (tid) REFERENCES threads (id);
ALTER TABLE vn ADD FOREIGN KEY (latest) REFERENCES vn_rev (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE vn ADD FOREIGN KEY (rgraph) REFERENCES relgraph (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE vn_anime ADD FOREIGN KEY (aid) REFERENCES anime (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE vn_anime ADD FOREIGN KEY (vid) REFERENCES vn_rev (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE vn_categories ADD FOREIGN KEY (vid) REFERENCES vn_rev (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE vn_relations ADD FOREIGN KEY (vid1) REFERENCES vn_rev (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE vn_relations ADD FOREIGN KEY (vid2) REFERENCES vn (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE vn_rev ADD FOREIGN KEY (id) REFERENCES changes (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE vn_rev ADD FOREIGN KEY (vid) REFERENCES vn (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE vn_screenshots ADD FOREIGN KEY (vid) REFERENCES vn_rev (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE vn_screenshots ADD FOREIGN KEY (scr) REFERENCES screenshots (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE vn_screenshots ADD FOREIGN KEY (rid) REFERENCES releases (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE vnlists ADD FOREIGN KEY (uid) REFERENCES users (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE vnlists ADD FOREIGN KEY (vid) REFERENCES vn (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE votes ADD FOREIGN KEY (uid) REFERENCES users (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE votes ADD FOREIGN KEY (vid) REFERENCES vn (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE wlists ADD FOREIGN KEY (uid) REFERENCES users (id) DEFERRABLE INITIALLY DEFERRED;
-ALTER TABLE wlists ADD FOREIGN KEY (vid) REFERENCES vn (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE vn ADD FOREIGN KEY (rgraph) REFERENCES relgraph (id);
+ALTER TABLE vn_anime ADD FOREIGN KEY (aid) REFERENCES anime (id);
+ALTER TABLE vn_anime ADD FOREIGN KEY (vid) REFERENCES vn_rev (id);
+ALTER TABLE vn_categories ADD FOREIGN KEY (vid) REFERENCES vn_rev (id);
+ALTER TABLE vn_relations ADD FOREIGN KEY (vid1) REFERENCES vn_rev (id);
+ALTER TABLE vn_relations ADD FOREIGN KEY (vid2) REFERENCES vn (id);
+ALTER TABLE vn_rev ADD FOREIGN KEY (id) REFERENCES changes (id);
+ALTER TABLE vn_rev ADD FOREIGN KEY (vid) REFERENCES vn (id);
+ALTER TABLE vn_screenshots ADD FOREIGN KEY (vid) REFERENCES vn_rev (id);
+ALTER TABLE vn_screenshots ADD FOREIGN KEY (scr) REFERENCES screenshots (id);
+ALTER TABLE vn_screenshots ADD FOREIGN KEY (rid) REFERENCES releases (id);
+ALTER TABLE votes ADD FOREIGN KEY (uid) REFERENCES users (id);
+ALTER TABLE votes ADD FOREIGN KEY (vid) REFERENCES vn (id);
+ALTER TABLE wlists ADD FOREIGN KEY (uid) REFERENCES users (id);
+ALTER TABLE wlists ADD FOREIGN KEY (vid) REFERENCES vn (id);
@@ -415,7 +412,7 @@ ALTER TABLE wlists ADD FOREIGN KEY (vid) REFERENCES vn
-- F U N C T I O N S --
-------------------------
-
+
-- update_rev(table, ids) - updates the rev column in the changes table
CREATE FUNCTION update_rev(tbl text, ids text) RETURNS void AS $$
DECLARE
@@ -449,7 +446,7 @@ BEGIN
IF id > 0 THEN
w := ' WHERE id = '||id;
END IF;
- EXECUTE 'UPDATE vn SET
+ EXECUTE 'UPDATE vn SET
c_released = COALESCE((SELECT
MIN(rr1.released)
FROM releases_rev rr1
@@ -698,6 +695,113 @@ CREATE TRIGGER threads_posts_stats_update AFTER INSERT OR UPDATE ON threads_post
CREATE TRIGGER users_stats_update AFTER INSERT OR DELETE ON users FOR EACH ROW EXECUTE PROCEDURE update_stats_cache();
+-- insert rows into anime for new vn_anime.aid items
+CREATE OR REPLACE FUNCTION vn_anime_aid() RETURNS trigger AS $$
+BEGIN
+ IF NOT EXISTS(SELECT 1 FROM anime WHERE id = NEW.aid) THEN
+ INSERT INTO anime (id) VALUES (NEW.aid);
+ END IF;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER vn_anime_aid BEFORE INSERT OR UPDATE ON vn_anime FOR EACH ROW EXECUTE PROCEDURE vn_anime_aid();
+
+
+-- Send a notify whenever anime info should be fetched
+CREATE OR REPLACE FUNCTION anime_fetch_notify() RETURNS trigger AS $$
+BEGIN
+ IF NEW.lastfetch IS NULL THEN
+ NOTIFY anime;
+ END IF;
+ RETURN NULL;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER anime_fetch_notify AFTER INSERT OR UPDATE ON anime FOR EACH ROW EXECUTE PROCEDURE anime_fetch_notify();
+
+
+-- Send a notify when a new cover image is uploaded
+CREATE OR REPLACE FUNCTION vn_rev_image_notify() RETURNS trigger AS $$
+BEGIN
+ IF NEW.image < 0 THEN
+ NOTIFY coverimage;
+ END IF;
+ RETURN NULL;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER vn_rev_image_notify AFTER INSERT OR UPDATE ON vn_rev FOR EACH ROW EXECUTE PROCEDURE vn_rev_image_notify();
+
+
+-- Send a notify when a screenshot needs to be processed
+CREATE OR REPLACE FUNCTION screenshot_process_notify() RETURNS trigger AS $$
+BEGIN
+ IF NEW.processed = FALSE THEN
+ NOTIFY screenshot;
+ END IF;
+ RETURN NULL;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER screenshot_process_notify AFTER INSERT OR UPDATE ON screenshots FOR EACH ROW EXECUTE PROCEDURE screenshot_process_notify();
+
+
+-- Update vn.rgraph column and send notify when a relation graph needs to be regenerated
+-- 1. NOTIFY is sent on an UPDATE or INSERT on vn with rgraph = NULL and with entries in vn_relations (deferred)
+-- vn.rgraph is set to NULL when:
+-- 2. UPDATE on vn where c_released or c_languages has changed (deferred, but doesn't have to be)
+-- 3. New VN revision of which the title differs from previous revision (deferred)
+-- 4. New VN revision with items in vn_relations that differ from previous revision (deferred)
+CREATE OR REPLACE FUNCTION vn_relgraph_notify() RETURNS trigger AS $$
+BEGIN
+ -- 1.
+ IF TG_TABLE_NAME = 'vn' THEN
+ IF NEW.rgraph IS NULL AND EXISTS(SELECT 1 FROM vn_relations WHERE vid1 = NEW.latest) THEN
+ NOTIFY relgraph;
+ END IF;
+ END IF;
+ IF TG_TABLE_NAME = 'vn' AND TG_OP = 'UPDATE' THEN
+ IF NEW.rgraph IS NOT NULL AND OLD.latest > 0 THEN
+ -- 2.
+ IF OLD.c_released <> NEW.c_released OR OLD.c_languages <> NEW.c_languages THEN
+ UPDATE vn SET rgraph = NULL WHERE id = NEW.id;
+ END IF;
+ -- 3 & 4
+ IF OLD.latest <> NEW.latest AND (
+ EXISTS(SELECT 1 FROM vn_rev v1, vn_rev v2 WHERE v2.title <> v1.title AND v1.id = OLD.latest AND v2.id = NEW.latest)
+ OR EXISTS(SELECT v1.vid2, v1.relation FROM vn_relations v1 WHERE v1.vid1 = OLD.latest EXCEPT SELECT v2.vid2, v2.relation FROM vn_relations v2 WHERE v2.vid1 = NEW.latest)
+ OR EXISTS(SELECT v1.vid2, v1.relation FROM vn_relations v1 WHERE v1.vid1 = NEW.latest EXCEPT SELECT v2.vid2, v2.relation FROM vn_relations v2 WHERE v2.vid1 = OLD.latest)
+ ) THEN
+ UPDATE vn SET rgraph = NULL WHERE id = NEW.id;
+ END IF;
+ END IF;
+ END IF;
+ RETURN NULL;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE CONSTRAINT TRIGGER vn_relgraph_notify AFTER INSERT OR UPDATE ON vn DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE PROCEDURE vn_relgraph_notify();
+
+
+-- NOTIFY on insert into changes/posts/tags
+CREATE OR REPLACE FUNCTION insert_notify() RETURNS trigger AS $$
+BEGIN
+ IF TG_TABLE_NAME = 'changes' THEN
+ NOTIFY newrevision;
+ ELSIF TG_TABLE_NAME = 'threads_posts' THEN
+ NOTIFY newpost;
+ ELSIF TG_TABLE_NAME = 'tags' THEN
+ NOTIFY newtag;
+ END IF;
+ RETURN NULL;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER insert_notify AFTER INSERT ON changes FOR EACH STATEMENT EXECUTE PROCEDURE insert_notify();
+CREATE TRIGGER insert_notify AFTER INSERT ON threads_posts FOR EACH STATEMENT EXECUTE PROCEDURE insert_notify();
+CREATE TRIGGER insert_notify AFTER INSERT ON tags FOR EACH STATEMENT EXECUTE PROCEDURE insert_notify();
+
diff --git a/util/multi.pl b/util/multi.pl
index e234534d..010951a2 100755
--- a/util/multi.pl
+++ b/util/multi.pl
@@ -1,36 +1,17 @@
#!/usr/bin/perl
-# Usage:
-# ./multi.pl [-c] [-s] [-a] [cmd1] [cmd2] ..
-# -c Do not daemonize, just execute the commands specified
-# on the command line and exit.
-# -s Same as -c, but also execute commands in the shared
-# memory processing queue.
-# -a Don't do anything, just add the commands specified on
-# the command line to the shared memory processing queue.
-
#
# Multi - core namespace for initialisation and global variables
#
-# NOTE: in case of errors, clearing the shared memory might work:
-# $ ipcrm -S 0x42444e56 -M 0x42444e56
-
package Multi;
use strict;
use warnings;
-no warnings 'once';
-use Tie::ShareLite ':lock';
-use Time::HiRes;
-use POE;
-use DBI;
use Cwd 'abs_path';
-# loading & initialization
-
our $ROOT;
BEGIN { ($ROOT = abs_path $0) =~ s{/util/multi\.pl$}{}; *VNDB::ROOT = \$ROOT }
use lib $VNDB::ROOT.'/lib';
@@ -38,42 +19,6 @@ use lib $VNDB::ROOT.'/lib';
use Multi::Core;
require $VNDB::ROOT.'/data/global.pl';
-our $STOP = 0;
-our $DAEMONIZE = (grep /^-c$/, @ARGV) ? 1 : (grep /^-s$/, @ARGV) ? 2 : 0;
-
-
-
-# only add commands with the -a argument
-
-if(grep /^-a$/, @ARGV) {
- my $s = tie my %s, 'Tie::ShareLite', -key => $VNDB::S{sharedmem_key}, -create => 'yes', -destroy => 'no', -mode => 0666;
- $s->lock(LOCK_EX);
- my @q = ( ($s{queue} ? @{$s{queue}} : ()), (grep !/^-/, @ARGV) );
- $s{queue} = \@q;
- $s->unlock();
- exit;
-}
-
-# one shared pgsql connection for all sessions
-our $SQL = DBI->connect(@{$VNDB::O{db_login}},
- { PrintError => 1, RaiseError => 0, AutoCommit => 1, pg_enable_utf8 => 1 });
-
-
-Multi::Core->spawn();
-
-# dynamically load and spawn modules
-for (keys %{$VNDB::M{modules}}) {
- my($mod, $args) = ($_, $VNDB::M{modules}{$_});
- next if !$args || ref($args) ne 'HASH';
- require "Multi/$mod.pm";
- # I'm surprised the strict pagma isn't complaining about this
- "Multi::$mod"->spawn(%$args);
-}
-
-$SIG{__WARN__} = sub {(local$_=shift)=~s/\r?\n//;$poe_kernel->call(core=>log=>1,'__WARN__: '.$_)};
-
-$poe_kernel->run();
-exec $0, grep /^-/, @ARGV if $STOP == 2;
-
+Multi::Core->run();
diff --git a/util/updates/update_2.6.sql b/util/updates/update_2.6.sql
new file mode 100644
index 00000000..06722cc2
--- /dev/null
+++ b/util/updates/update_2.6.sql
@@ -0,0 +1,261 @@
+
+
+-- Create table for session data storage
+CREATE TABLE sessions (
+ uid integer NOT NULL REFERENCES users(id),
+ token bytea NOT NULL,
+ expiration timestamptz NOT NULL DEFAULT (NOW() + '1 year'::interval),
+ PRIMARY KEY (uid, token)
+);
+
+-- Add column to users for salt storage
+ALTER TABLE users ADD COLUMN salt character(9) NOT NULL DEFAULT ''::bpchar;
+
+
+
+-- The anime table:
+-- - use timestamp data type for anime.lastfetch
+-- - allow NULL for all columns except id
+ALTER TABLE anime ALTER COLUMN lastfetch DROP NOT NULL;
+ALTER TABLE anime ALTER COLUMN lastfetch DROP DEFAULT;
+UPDATE anime SET lastfetch = NULL WHERE lastfetch <= 0;
+ALTER TABLE anime ALTER COLUMN lastfetch TYPE timestamptz USING 'epoch'::timestamptz + lastfetch * interval '1 second';
+
+ALTER TABLE anime ALTER COLUMN ann_id DROP NOT NULL;
+ALTER TABLE anime ALTER COLUMN ann_id DROP DEFAULT;
+UPDATE anime SET ann_id = NULL WHERE ann_id = 0;
+
+ALTER TABLE anime ALTER COLUMN nfo_id DROP NOT NULL;
+ALTER TABLE anime ALTER COLUMN nfo_id DROP DEFAULT;
+UPDATE anime SET nfo_id = NULL WHERE nfo_id = '';
+
+ALTER TABLE anime ALTER COLUMN title_kanji DROP NOT NULL;
+ALTER TABLE anime ALTER COLUMN title_kanji DROP DEFAULT;
+UPDATE anime SET title_kanji = NULL WHERE title_kanji = '';
+
+ALTER TABLE anime ALTER COLUMN title_romaji DROP NOT NULL;
+ALTER TABLE anime ALTER COLUMN title_romaji DROP DEFAULT;
+UPDATE anime SET title_romaji = NULL WHERE title_romaji = '';
+
+ALTER TABLE anime ALTER COLUMN type DROP NOT NULL;
+ALTER TABLE anime ALTER COLUMN type DROP DEFAULT;
+UPDATE anime SET type = NULL WHERE type = 0;
+UPDATE anime SET type = type-1;
+
+ALTER TABLE anime ALTER COLUMN year DROP NOT NULL;
+ALTER TABLE anime ALTER COLUMN year DROP DEFAULT;
+UPDATE anime SET year = NULL WHERE year = 0;
+
+
+
+-- screenshots.status (smallint) -> screenshots.processed (boolean)
+ALTER TABLE screenshots RENAME COLUMN status TO processed;
+ALTER TABLE screenshots ALTER COLUMN processed DROP DEFAULT;
+ALTER TABLE screenshots ALTER COLUMN processed TYPE boolean USING processed::int::boolean;
+ALTER TABLE screenshots ALTER COLUMN processed SET DEFAULT FALSE;
+
+
+
+-- two new resolutions have been added, array indexes have changed
+UPDATE releases_rev SET resolution = resolution + 1 WHERE resolution >= 5;
+UPDATE releases_rev SET resolution = resolution + 1 WHERE resolution >= 7;
+
+
+
+-- remove the DEFERRED attribute on all foreign key checks on which it isn't necessary
+-- (note: these queries all assume the foreign keys have their default names, as given
+-- by PostgreSQL. This shouldn't be a problem, provided if you haven't touched them.)
+ALTER TABLE changes DROP CONSTRAINT changes_requester_fkey;
+ALTER TABLE changes DROP CONSTRAINT changes_causedby_fkey;
+ALTER TABLE producers_rev DROP CONSTRAINT producers_rev_id_fkey;
+ALTER TABLE producers_rev DROP CONSTRAINT producers_rev_pid_fkey;
+ALTER TABLE quotes DROP CONSTRAINT quotes_vid_fkey;
+ALTER TABLE releases_lang DROP CONSTRAINT releases_lang_rid_fkey;
+ALTER TABLE releases_media DROP CONSTRAINT releases_media_rid_fkey;
+ALTER TABLE releases_platforms DROP CONSTRAINT releases_platforms_rid_fkey;
+ALTER TABLE releases_producers DROP CONSTRAINT releases_producers_rid_fkey;
+ALTER TABLE releases_producers DROP CONSTRAINT releases_producers_pid_fkey;
+ALTER TABLE releases_rev DROP CONSTRAINT releases_rev_id_fkey;
+ALTER TABLE releases_rev DROP CONSTRAINT releases_rev_rid_fkey;
+ALTER TABLE releases_vn DROP CONSTRAINT releases_vn_rid_fkey;
+ALTER TABLE releases_vn DROP CONSTRAINT releases_vn_vid_fkey;
+ALTER TABLE rlists DROP CONSTRAINT rlists_uid_fkey;
+ALTER TABLE rlists DROP CONSTRAINT rlists_rid_fkey;
+ALTER TABLE tags DROP CONSTRAINT tags_addedby_fkey;
+ALTER TABLE tags_aliases DROP CONSTRAINT tags_aliases_tag_fkey;
+ALTER TABLE tags_parents DROP CONSTRAINT tags_parents_tag_fkey;
+ALTER TABLE tags_parents DROP CONSTRAINT tags_parents_parent_fkey;
+ALTER TABLE tags_vn DROP CONSTRAINT tags_vn_tag_fkey;
+ALTER TABLE tags_vn DROP CONSTRAINT tags_vn_vid_fkey;
+ALTER TABLE tags_vn DROP CONSTRAINT tags_vn_uid_fkey;
+ALTER TABLE threads_posts DROP CONSTRAINT threads_posts_tid_fkey;
+ALTER TABLE threads_posts DROP CONSTRAINT threads_posts_uid_fkey;
+ALTER TABLE threads_boards DROP CONSTRAINT threads_tags_tid_fkey; -- threads_boards used to be called threads_tags
+ALTER TABLE vn DROP CONSTRAINT vn_rgraph_fkey;
+ALTER TABLE vn_anime DROP CONSTRAINT vn_anime_aid_fkey;
+ALTER TABLE vn_anime DROP CONSTRAINT vn_anime_vid_fkey;
+ALTER TABLE vn_categories DROP CONSTRAINT vn_categories_vid_fkey;
+ALTER TABLE vn_relations DROP CONSTRAINT vn_relations_vid1_fkey;
+ALTER TABLE vn_relations DROP CONSTRAINT vn_relations_vid2_fkey;
+ALTER TABLE vn_rev DROP CONSTRAINT vn_rev_id_fkey;
+ALTER TABLE vn_rev DROP CONSTRAINT vn_rev_vid_fkey;
+ALTER TABLE vn_screenshots DROP CONSTRAINT vn_screenshots_vid_fkey;
+ALTER TABLE vn_screenshots DROP CONSTRAINT vn_screenshots_scr_fkey;
+ALTER TABLE vn_screenshots DROP CONSTRAINT vn_screenshots_rid_fkey;
+ALTER TABLE votes DROP CONSTRAINT votes_uid_fkey;
+ALTER TABLE votes DROP CONSTRAINT votes_vid_fkey;
+ALTER TABLE wlists DROP CONSTRAINT wlists_uid_fkey;
+ALTER TABLE wlists DROP CONSTRAINT wlists_vid_fkey;
+
+ALTER TABLE changes ADD FOREIGN KEY (requester) REFERENCES users (id);
+ALTER TABLE changes ADD FOREIGN KEY (causedby) REFERENCES changes (id);
+ALTER TABLE producers_rev ADD FOREIGN KEY (id) REFERENCES changes (id);
+ALTER TABLE producers_rev ADD FOREIGN KEY (pid) REFERENCES producers (id);
+ALTER TABLE quotes ADD FOREIGN KEY (vid) REFERENCES vn (id);
+ALTER TABLE releases_lang ADD FOREIGN KEY (rid) REFERENCES releases_rev (id);
+ALTER TABLE releases_media ADD FOREIGN KEY (rid) REFERENCES releases_rev (id);
+ALTER TABLE releases_platforms ADD FOREIGN KEY (rid) REFERENCES releases_rev (id);
+ALTER TABLE releases_producers ADD FOREIGN KEY (rid) REFERENCES releases_rev (id);
+ALTER TABLE releases_producers ADD FOREIGN KEY (pid) REFERENCES producers (id);
+ALTER TABLE releases_rev ADD FOREIGN KEY (id) REFERENCES changes (id);
+ALTER TABLE releases_rev ADD FOREIGN KEY (rid) REFERENCES releases (id);
+ALTER TABLE releases_vn ADD FOREIGN KEY (rid) REFERENCES releases_rev (id);
+ALTER TABLE releases_vn ADD FOREIGN KEY (vid) REFERENCES vn (id);
+ALTER TABLE rlists ADD FOREIGN KEY (uid) REFERENCES users (id);
+ALTER TABLE rlists ADD FOREIGN KEY (rid) REFERENCES releases (id);
+ALTER TABLE tags ADD FOREIGN KEY (addedby) REFERENCES users (id);
+ALTER TABLE tags_aliases ADD FOREIGN KEY (tag) REFERENCES tags (id);
+ALTER TABLE tags_parents ADD FOREIGN KEY (tag) REFERENCES tags (id);
+ALTER TABLE tags_parents ADD FOREIGN KEY (parent) REFERENCES tags (id);
+ALTER TABLE tags_vn ADD FOREIGN KEY (tag) REFERENCES tags (id);
+ALTER TABLE tags_vn ADD FOREIGN KEY (vid) REFERENCES vn (id);
+ALTER TABLE tags_vn ADD FOREIGN KEY (uid) REFERENCES users (id);
+ALTER TABLE threads_posts ADD FOREIGN KEY (tid) REFERENCES threads (id);
+ALTER TABLE threads_posts ADD FOREIGN KEY (uid) REFERENCES users (id);
+ALTER TABLE threads_boards ADD FOREIGN KEY (tid) REFERENCES threads (id);
+ALTER TABLE vn ADD FOREIGN KEY (rgraph) REFERENCES relgraph (id);
+ALTER TABLE vn_anime ADD FOREIGN KEY (aid) REFERENCES anime (id);
+ALTER TABLE vn_anime ADD FOREIGN KEY (vid) REFERENCES vn_rev (id);
+ALTER TABLE vn_categories ADD FOREIGN KEY (vid) REFERENCES vn_rev (id);
+ALTER TABLE vn_relations ADD FOREIGN KEY (vid1) REFERENCES vn_rev (id);
+ALTER TABLE vn_relations ADD FOREIGN KEY (vid2) REFERENCES vn (id);
+ALTER TABLE vn_rev ADD FOREIGN KEY (id) REFERENCES changes (id);
+ALTER TABLE vn_rev ADD FOREIGN KEY (vid) REFERENCES vn (id);
+ALTER TABLE vn_screenshots ADD FOREIGN KEY (vid) REFERENCES vn_rev (id);
+ALTER TABLE vn_screenshots ADD FOREIGN KEY (scr) REFERENCES screenshots (id);
+ALTER TABLE vn_screenshots ADD FOREIGN KEY (rid) REFERENCES releases (id);
+ALTER TABLE votes ADD FOREIGN KEY (uid) REFERENCES users (id);
+ALTER TABLE votes ADD FOREIGN KEY (vid) REFERENCES vn (id);
+ALTER TABLE wlists ADD FOREIGN KEY (uid) REFERENCES users (id);
+ALTER TABLE wlists ADD FOREIGN KEY (vid) REFERENCES vn (id);
+
+
+
+-- automatically insert rows into the anime table for unknown aids
+-- when inserted into vn_anime
+CREATE OR REPLACE FUNCTION vn_anime_aid() RETURNS trigger AS $$
+BEGIN
+ IF NOT EXISTS(SELECT 1 FROM anime WHERE id = NEW.aid) THEN
+ INSERT INTO anime (id) VALUES (NEW.aid);
+ END IF;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER vn_anime_aid BEFORE INSERT OR UPDATE ON vn_anime FOR EACH ROW EXECUTE PROCEDURE vn_anime_aid();
+
+
+-- Send a notify whenever anime info should be fetched
+CREATE OR REPLACE FUNCTION anime_fetch_notify() RETURNS trigger AS $$
+BEGIN
+ IF NEW.lastfetch IS NULL THEN
+ NOTIFY anime;
+ END IF;
+ RETURN NULL;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER anime_fetch_notify AFTER INSERT OR UPDATE ON anime FOR EACH ROW EXECUTE PROCEDURE anime_fetch_notify();
+
+
+-- Send a notify when a new cover image is uploaded
+CREATE OR REPLACE FUNCTION vn_rev_image_notify() RETURNS trigger AS $$
+BEGIN
+ IF NEW.image < 0 THEN
+ NOTIFY coverimage;
+ END IF;
+ RETURN NULL;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER vn_rev_image_notify AFTER INSERT OR UPDATE ON vn_rev FOR EACH ROW EXECUTE PROCEDURE vn_rev_image_notify();
+
+
+-- Send a notify when a screenshot needs to be processed
+CREATE OR REPLACE FUNCTION screenshot_process_notify() RETURNS trigger AS $$
+BEGIN
+ IF NEW.processed = FALSE THEN
+ NOTIFY screenshot;
+ END IF;
+ RETURN NULL;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER screenshot_process_notify AFTER INSERT OR UPDATE ON screenshots FOR EACH ROW EXECUTE PROCEDURE screenshot_process_notify();
+
+
+-- Update vn.rgraph column and send notify when a relation graph needs to be regenerated
+-- 1. NOTIFY is sent on an UPDATE or INSERT on vn with rgraph = NULL and with entries in vn_relations (deferred)
+-- vn.rgraph is set to NULL when:
+-- 2. UPDATE on vn where c_released or c_languages has changed (deferred, but doesn't have to be)
+-- 3. New VN revision of which the title differs from previous revision (deferred)
+-- 4. New VN revision with items in vn_relations that differ from previous revision (deferred)
+CREATE OR REPLACE FUNCTION vn_relgraph_notify() RETURNS trigger AS $$
+BEGIN
+ -- 1.
+ IF TG_TABLE_NAME = 'vn' THEN
+ IF NEW.rgraph IS NULL AND EXISTS(SELECT 1 FROM vn_relations WHERE vid1 = NEW.latest) THEN
+ NOTIFY relgraph;
+ END IF;
+ END IF;
+ IF TG_TABLE_NAME = 'vn' AND TG_OP = 'UPDATE' THEN
+ IF NEW.rgraph IS NOT NULL AND OLD.latest > 0 THEN
+ -- 2.
+ IF OLD.c_released <> NEW.c_released OR OLD.c_languages <> NEW.c_languages THEN
+ UPDATE vn SET rgraph = NULL WHERE id = NEW.id;
+ END IF;
+ -- 3 & 4
+ IF OLD.latest <> NEW.latest AND (
+ EXISTS(SELECT 1 FROM vn_rev v1, vn_rev v2 WHERE v2.title <> v1.title AND v1.id = OLD.latest AND v2.id = NEW.latest)
+ OR EXISTS(SELECT v1.vid2, v1.relation FROM vn_relations v1 WHERE v1.vid1 = OLD.latest EXCEPT SELECT v2.vid2, v2.relation FROM vn_relations v2 WHERE v2.vid1 = NEW.latest)
+ OR EXISTS(SELECT v1.vid2, v1.relation FROM vn_relations v1 WHERE v1.vid1 = NEW.latest EXCEPT SELECT v2.vid2, v2.relation FROM vn_relations v2 WHERE v2.vid1 = OLD.latest)
+ ) THEN
+ UPDATE vn SET rgraph = NULL WHERE id = NEW.id;
+ END IF;
+ END IF;
+ END IF;
+ RETURN NULL;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE CONSTRAINT TRIGGER vn_relgraph_notify AFTER INSERT OR UPDATE ON vn DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE PROCEDURE vn_relgraph_notify();
+
+
+-- NOTIFY on insert into changes/posts/tags
+CREATE OR REPLACE FUNCTION insert_notify() RETURNS trigger AS $$
+BEGIN
+ IF TG_TABLE_NAME = 'changes' THEN
+ NOTIFY newrevision;
+ ELSIF TG_TABLE_NAME = 'threads_posts' THEN
+ NOTIFY newpost;
+ ELSIF TG_TABLE_NAME = 'tags' THEN
+ NOTIFY newtag;
+ END IF;
+ RETURN NULL;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER insert_notify AFTER INSERT ON changes FOR EACH STATEMENT EXECUTE PROCEDURE insert_notify();
+CREATE TRIGGER insert_notify AFTER INSERT ON threads_posts FOR EACH STATEMENT EXECUTE PROCEDURE insert_notify();
+CREATE TRIGGER insert_notify AFTER INSERT ON tags FOR EACH STATEMENT EXECUTE PROCEDURE insert_notify();
+
diff --git a/util/vndb.pl b/util/vndb.pl
index 8a9a83e5..455a1c23 100755
--- a/util/vndb.pl
+++ b/util/vndb.pl
@@ -36,7 +36,6 @@ YAWF::init(
namespace => 'VNDB',
object_data => \%S,
pre_request_handler => \&reqinit,
- post_request_handler => \&reqdone,
error_404_handler => \&handle404,
);
@@ -58,13 +57,6 @@ sub reqinit {
}
-sub reqdone {
- my $self = shift;
- $self->dbCommit;
- $self->multiCmd;
-}
-
-
sub handle404 {
my $self = shift;
$self->resStatus(404);