├── .gitignore ├── Board.pm ├── Board ├── Errors.pm ├── Local.pm ├── Mysql.pm ├── Request.pm ├── Sphinx_Mysql.pm ├── Utilities.pm ├── WWW.pm └── Yotsuba.pm ├── COPYRIGHT ├── README ├── README.md ├── board-config.pl ├── board-dump.pl ├── board-reports-demon.pl ├── cgi-board.pl ├── examples ├── .htaccess ├── screen-archive.example └── sphinx.conf.example ├── media ├── board.js ├── calendar.css ├── calendar.js ├── deleted.png ├── error.png ├── favicon.ico ├── favicon.png ├── fuuka.css ├── god-left.png ├── god-right.png ├── internal.png ├── spoiler.png ├── spoilers.png └── sticky.png ├── messages.pl ├── reports ├── activity ├── activity-archive ├── activity-hourly ├── availability ├── graphs │ ├── activity-archive.graph │ ├── activity-hourly.graph │ ├── activity.graph │ ├── karma.graph │ └── population.graph ├── image-reposts ├── karma ├── new-users ├── population ├── post-count ├── post-rate ├── post-rate-archive ├── users-online └── users-online-internal ├── sql ├── r0070.sql ├── r0114.pl ├── r0114.sql ├── r0142.pl ├── r0142.sql ├── r0142_2.pl ├── r0142_2.sql ├── r0181.pl ├── r0181.sql ├── r0183.pl └── r0183.sql └── templates.pl /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swp 3 | .htaccess 4 | board-config-local.pl 5 | brd 6 | reports/graphs/*.graph+ 7 | reports/graphs/*.data 8 | reports/status 9 | panic.txt 10 | sql/*.mine.sql 11 | *.iml -------------------------------------------------------------------------------- /Board.pm: -------------------------------------------------------------------------------- 1 | package Board; 2 | 3 | use strict; 4 | use warnings; 5 | use Carp qw/confess cluck/; 6 | 7 | require Exporter; 8 | our @ISA=qw/Exporter/; 9 | 10 | use Board::Request; 11 | use Board::Errors; 12 | 13 | sub not_supported(){ 14 | confess "Not supported!"; 15 | } 16 | 17 | sub new($;%){ 18 | my $class=shift; 19 | my(%info)=@_; 20 | 21 | confess "This is an abstract class, dumbass!" 22 | if $class eq "Board"; 23 | 24 | return bless{ 25 | %info, 26 | 27 | opts => [{%info}], 28 | classname => $class, 29 | },$class; 30 | } 31 | 32 | sub clone{ 33 | my($self,%info)=@_; 34 | 35 | my($newinfo,@args)=@{ $self->{opts} }; 36 | 37 | $newinfo->{$_}=$info{$_} foreach keys %info; 38 | 39 | $self->{classname}->new(@args,%$newinfo); 40 | } 41 | 42 | sub new_post($@){ 43 | my $self=shift; 44 | bless {@_},"Board::Post"; 45 | } 46 | sub new_thread($@){ 47 | my $self=shift; 48 | bless {@_},"Board::Thread"; 49 | } 50 | sub new_page($$){ 51 | my $self=shift; 52 | bless {num=>$_[0],threads=>[]},"Board::Page"; 53 | } 54 | sub new_media_post($@){ 55 | my $self=shift; 56 | bless {@_},"Board::MediaPost"; 57 | } 58 | 59 | sub flattern($$){ 60 | my $self=shift; 61 | my($ref)=@_; 62 | 63 | return map{@{$_->{posts}}} @{$ref->{threads}} 64 | if ref $ref eq "Board::Page"; 65 | 66 | return @{$ref->{posts}} 67 | if ref $ref eq "Board::Thread"; 68 | } 69 | 70 | sub get_media_preview($$){not_supported} 71 | sub get_media($$){not_supported} 72 | sub get_post($$){not_supported} 73 | sub get_thread($$){not_supported} 74 | sub get_thread_range($$$){not_supported} 75 | sub get_page($$){not_supported} 76 | 77 | sub post($;%){not_supported} 78 | sub insert($$$){not_supported} 79 | sub insert_media($$@){not_supported} 80 | sub insert_media_preview($$@){not_supported} 81 | sub delete($$;$){not_supported} 82 | sub clean($){not_supported} 83 | 84 | sub warn{ 85 | my($self,$cat,$text)=@_; 86 | $text||=$self->errstr; 87 | $cat||='general'; 88 | 89 | # print "$cat: $text\n"; 90 | } 91 | 92 | sub ok{ 93 | my($self)=@_; 94 | 95 | $self->error(0) 96 | } 97 | 98 | sub error{ 99 | my($self,$code,$str)=@_; 100 | 101 | return $self->{errcode} unless defined $code; 102 | 103 | $self->{errcode}=$code; 104 | $self->{errstr}=$code?$str:""; 105 | } 106 | 107 | sub errstr{ 108 | my($self)=@_; 109 | 110 | $self->{errstr} 111 | } 112 | 113 | sub content($){ 114 | my $self=shift; 115 | my($ref)=@_; 116 | 117 | confess "arg '$ref' is not a valid reference" 118 | unless ref $ref and (ref $ref)=~/^Board::Request::(THREAD|RANGE|PAGE|POST|MEDIA)$/; 119 | 120 | my $sub; 121 | for($1){ 122 | /RANGE/ and $sub=sub{$self->get_thread_range(@$ref[0], @$ref[1])},last; 123 | /THREAD/ and $sub=sub{$self->get_thread(@$ref[0], @$ref[1])},last; 124 | /PAGE/ and $sub=sub{$self->get_page(@$ref[0], @$ref[1])},last; 125 | /POST/ and $sub=sub{$self->get_post($$ref)},last; 126 | /MEDIA/ and $sub=sub{$self->get_image($$ref)},last; 127 | } 128 | 129 | REDO: 130 | my $contents=$sub->(); 131 | $self->warn($self->error) and goto REDO 132 | if $self->error and $self->error==TRY_AGAIN; 133 | 134 | # print "c ".(ref $ref)." ".$$ref."\n"; 135 | 136 | $contents 137 | } 138 | 139 | sub threads($$){ 140 | my $self=shift; 141 | my($page)=@_; 142 | 143 | my $p=$self->content(PAGE $page); 144 | map{$_->{num}}@{$p->{threads}}; 145 | } 146 | 147 | sub bump($$$%){ 148 | my $self=shift; 149 | my($num,$text,%args)=@_; 150 | 151 | my($err,$last)=$self->post( 152 | parent => $num, 153 | comment => $text, 154 | %args, 155 | ); 156 | 157 | return $err if $err; 158 | return "Unknown error when ghost bumping" unless $last; 159 | 160 | ($self->delete($last),$last); 161 | } 162 | 163 | sub text($$){ 164 | my $self=shift; 165 | my($ref)=@_; 166 | 167 | my $content=$self->content($ref); 168 | 169 | grep{$_} $self->clean_text( 170 | map{$_->{comment}} $self->flattern($content) 171 | ); 172 | } 173 | 174 | sub do_clean($$){ 175 | my($self)=shift; 176 | 177 | for(shift){ 178 | s/&\#(\d+);/chr $1/gxse; 179 | s!>!>!g; 180 | s!<!do_clean($_) if $_; 196 | } 197 | @v; 198 | } 199 | 200 | sub tripcode{ 201 | my($self,$name)=@_; 202 | 203 | if($name=~/^(.*?)(#)(.*)$/){ 204 | my($namepart,$marker,$trippart)=($1,$2,$3); 205 | my($trip,$sectrip)=$trippart=~/^(.*?)(?:#+(.*))?$/; 206 | 207 | if($sectrip){ 208 | eval{ 209 | use Digest::SHA1 'sha1_base64'; 210 | use MIME::Base64; 211 | 212 | $sectrip='!!'.substr(sha1_base64($sectrip.decode_base64($self->{secret})), 0, 11); 213 | }; 214 | 215 | $sectrip='' if $@; 216 | } 217 | 218 | if($trip){ 219 | # Actually, I am already relying on 5.10 features (lexical $_), 220 | # so there's nothing wrong in assuming that I have Encode, 221 | # but oh well 222 | eval{ 223 | # 2ch trips are processed as Shift_JIS whenever possible 224 | use Encode qw/decode encode/; 225 | 226 | $trip=encode("Shift_JIS",$trip); 227 | }; 228 | 229 | my $salt=substr $trip."H..",1,2; 230 | $salt=~s/[^\.-z]/./g; 231 | $salt=~tr/:;<=>?@[\\]^_`/ABCDEFGabcdef/; 232 | $trip="!".substr crypt($trip,$salt),-10; 233 | } 234 | 235 | return $namepart,$trip.$sectrip; 236 | } 237 | 238 | $name,"" 239 | } 240 | 241 | sub troubles($@){ 242 | my $self=shift; 243 | 244 | open HANDLE,">>panic.txt"; 245 | print HANDLE @_; 246 | close HANDLE; 247 | 248 | cluck @_; 249 | } 250 | 251 | 1; 252 | -------------------------------------------------------------------------------- /Board/Errors.pm: -------------------------------------------------------------------------------- 1 | package Board::Errors; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | require Exporter; 7 | our @ISA=qw/Exporter/; 8 | our @EXPORT=qw/ALL_OK TRY_AGAIN FORGET_IT NONEXIST ALREADY_EXISTS THREAD_FULL TOO_LARGE/; 9 | 10 | use constant ALL_OK => 0; 11 | use constant TRY_AGAIN => 1; 12 | use constant FORGET_IT => 2; 13 | use constant NONEXIST => 3; 14 | use constant ALREADY_EXISTS => 4; 15 | use constant THREAD_FULL => 5; 16 | use constant TOO_LARGE => 6; 17 | -------------------------------------------------------------------------------- /Board/Local.pm: -------------------------------------------------------------------------------- 1 | package Board::Local; 2 | 3 | use strict; 4 | use warnings; 5 | use Carp qw/confess/; 6 | 7 | use Board; 8 | our @ISA=qw/Board/; 9 | 10 | use File::Path; 11 | 12 | sub new($$;%){ 13 | my $class=shift; 14 | my $name=shift; 15 | my(%info)=(@_); 16 | 17 | my $path=(delete $info{images} or die); 18 | my $web_group=(delete $info{web_group} or ""); 19 | my $self=$class->SUPER::new(@_); 20 | 21 | $path=~s!\\!/!g; 22 | $path=~s!/$!!g; 23 | 24 | $self->{path}="$path/$name"; 25 | $self->{name}=$name; 26 | 27 | $self->{full}=(delete $info{full_pictures} or ""); 28 | $self->{webgid} = getgrnam($web_group) if $^O !~ /MSWin32/i and $web_group; 29 | 30 | mkdir $path; 31 | mkdir $self->{path}; 32 | 33 | bless $self,$class; 34 | } 35 | 36 | sub magnitude($$){ 37 | my $self=shift; 38 | 39 | $self->SUPER::magnitude($_[0]); 40 | } 41 | 42 | sub get_subdirs($$){ 43 | my $self=shift; 44 | my($num)=@_; 45 | 46 | my($subdir,$sub2dir)=$num=~/(\d+?)(\d{2})\d{0,3}$/; 47 | (sprintf "%04d",$subdir),(sprintf "%02d",$sub2dir); 48 | } 49 | 50 | sub get_dirs($$){ 51 | my $self=shift; 52 | my($num)=@_; 53 | my($path)=$self->{path}; 54 | 55 | my($subdir,$sub2dir)=$self->get_subdirs($num); 56 | ("$path/thumb/$subdir/$sub2dir","$path/img/$subdir/$sub2dir"); 57 | } 58 | 59 | sub make_dirs($;$){ 60 | my $self=shift; 61 | my($num)=@_; 62 | my($path)=$self->{path}; 63 | 64 | mkdir "$path"; 65 | mkdir "$path/thumb"; 66 | mkdir "$path/img" if $self->{full}; 67 | 68 | my($subdir,$sub2dir)=$self->get_subdirs($num); 69 | if($subdir){ 70 | mkdir "$path/thumb/$subdir"; 71 | mkdir "$path/thumb/$subdir/$sub2dir"; 72 | chmod 0775, "$path/thumb/$subdir", "$path/thumb/$subdir/$sub2dir"; 73 | chown $<, $self->{webgid}, "$path/thumb/$subdir", "$path/thumb/$subdir/$sub2dir" if $self->{webgid}; 74 | if($self->{full}){ 75 | mkdir "$path/img/$subdir"; 76 | mkdir "$path/img/$subdir/$sub2dir"; 77 | chmod 0775, "$path/img/$subdir", "$path/img/$subdir/$sub2dir"; 78 | chown $<, $self->{webgid}, "$path/img/$subdir", "$path/img/$subdir/$sub2dir" if $self->{webgid}; 79 | } 80 | } 81 | 82 | ("$path/thumb/$subdir/$sub2dir","$path/img/$subdir/$sub2dir"); 83 | } 84 | 85 | sub get_media_preview($$){ 86 | my $self=shift; 87 | 88 | my($err,$filename)=$self->get_media_preview_location(@_); 89 | $err and return $err; 90 | 91 | open HANDLE,"$filename" or return "$! - $filename"; 92 | binmode HANDLE; 93 | local $/; 94 | my $content=; 95 | close HANDLE; 96 | 97 | \$content; 98 | } 99 | 100 | sub get_media_location{ 101 | my $self=shift; 102 | my($arg1,$arg2)=@_; 103 | 104 | for(ref $arg1){ 105 | /^Board::Post/ and do{ 106 | $arg2=$arg1->{media_filename}; 107 | $arg1=($arg1->{parent} or $arg1->{num}); 108 | last; 109 | }; 110 | /^$/ and last; 111 | 112 | confess qq{Arguments can be either Board::Post, or two scalars}; 113 | } 114 | 115 | my(undef,$dir)=$self->get_dirs($arg1); 116 | 117 | (0,"$dir/".($arg2 or "")) 118 | } 119 | 120 | sub get_media_preview_location{ 121 | my $self=shift; 122 | my($arg1,$arg2)=@_; 123 | 124 | for(ref $arg1){ 125 | /^Board::Post/ and do{ 126 | $arg2=$arg1->{preview}; 127 | $arg1=($arg1->{parent} or $arg1->{num}); 128 | last; 129 | }; 130 | /^$/ and last; 131 | 132 | confess qq{Arguments can be either Board::Post, or two scalars}; 133 | } 134 | 135 | my($dir)=$self->get_dirs($arg1); 136 | 137 | (0,"$dir/".($arg2 or "")) 138 | } 139 | 140 | sub insert($$){ 141 | my $self=shift; 142 | 143 | $self->ok; 144 | } 145 | 146 | sub insert_media_preview{ 147 | my $self=shift; 148 | my($h,$source)=@_; 149 | 150 | ref $h eq "Board::Post" or ref $h eq "Board::MediaPost" 151 | or die "Can only insert Board::[Media]Post, tried to insert ".ref $h; 152 | 153 | my($thumb_dir)=$self->make_dirs($h->{parent} or $h->{num}); 154 | 155 | return 0 unless $h->{preview}; 156 | return 1 if -e "$thumb_dir/$h->{preview}"; 157 | 158 | my($ref)=$source->get_media_preview($h); 159 | return 2 if $source->error; 160 | 161 | open HANDLE,">$thumb_dir/$h->{preview}" 162 | or die "$! - $thumb_dir/$h->{preview}"; 163 | binmode HANDLE; 164 | print HANDLE $$ref; 165 | close HANDLE; 166 | 167 | chmod 0664, "$thumb_dir/$h->{preview}"; 168 | chown $<, $self->{webgid}, "$thumb_dir/$h->{preview}" if $self->{webgid}; 169 | 170 | $self->ok; 171 | 172 | 1; 173 | } 174 | 175 | sub delete_media_preview($) { 176 | my $self=shift; 177 | my ($num) = @_; 178 | 179 | my $h = $self->get_post($num); 180 | 181 | my($thumb_dir)=$self->get_dirs($h->{parent} or $h->{num}); 182 | 183 | unlink "$thumb_dir/$h->{preview}" if $h->{preview} and $self->media_preview_exists($h); 184 | } 185 | 186 | sub insert_media{ 187 | my $self=shift; 188 | my($h,$source)=@_; 189 | 190 | ref $h eq "Board::Post" or ref $h eq "Board::MediaPost" 191 | or die "Can only insert Board::[Media]Post, tried to insert ".ref $h; 192 | 193 | my(undef,$media_dir)=$self->make_dirs($h->{parent} or $h->{num}); 194 | 195 | return 0 unless $h->{media_filename}; 196 | return 1 if -e "$media_dir/$h->{media_filename}"; 197 | 198 | my($ref)=$source->get_media($h); 199 | return 2 if $source->error; 200 | 201 | open HANDLE,">$media_dir/$h->{media_filename}" 202 | or die "$! - $media_dir/$h->{media_filename}"; 203 | binmode HANDLE; 204 | print HANDLE $$ref; 205 | close HANDLE; 206 | 207 | chmod 0664, "$media_dir/$h->{preview}"; 208 | #chown $<, 80, "$media_dir/$h->{preview}"; 209 | 210 | $self->ok; 211 | 212 | 1; 213 | } 214 | 215 | sub media_preview_exists{ 216 | my $self=shift; 217 | my($h,$source)=@_; 218 | 219 | ref $h eq "Board::Post" 220 | or die "Can work with Board::Post, received ".ref $h; 221 | 222 | my($thumb_dir)=$self->make_dirs($h->{parent} or $h->{num}); 223 | 224 | return 1 if -e "$thumb_dir/$h->{preview}"; 225 | 226 | 0; 227 | } 228 | -------------------------------------------------------------------------------- /Board/Mysql.pm: -------------------------------------------------------------------------------- 1 | package Board::Mysql; 2 | 3 | use strict; 4 | use warnings; 5 | use Carp qw/confess cluck/; 6 | use DBI; 7 | 8 | use Date::Parse; 9 | 10 | use Board::Local; 11 | use Board::Errors; 12 | our @ISA = qw/Board::Local/; 13 | 14 | sub new($$;%){ 15 | my $class=shift; 16 | my $path=shift; 17 | my(%info)=(@_); 18 | 19 | my $opts=[{@_},$path]; 20 | 21 | my($tname)=$path=~/(\w+)$/; 22 | 23 | my $database =(delete $info{database} or "Yotsuba"); 24 | my $table =(delete $info{table} or $tname or "proast"); 25 | my $host =(delete $info{host} or "localhost"); 26 | my $name =(delete $info{name} or "root"); 27 | my $password =(delete $info{password} or ""); 28 | my $connstr =(delete $info{connstr} or ""); 29 | my $charset =(delete $info{charset} or "utf8"); 30 | my $create_new = delete $info{create}; 31 | 32 | my $self=$class->SUPER::new($path,%info); 33 | 34 | $self->{db_name} = $database; 35 | $self->{db_host} = $host; 36 | $self->{db_connstr} = $connstr; 37 | $self->{db_username} = $name; 38 | $self->{db_password} = $password; 39 | $self->{db_charset} = $charset; 40 | 41 | my $dbh = $self->_connect or die $DBI::errstr; 42 | 43 | $self->{dbh} = $dbh; 44 | $self->{table} = $table; 45 | $self->{spam_table} = "${table}_spam"; 46 | $self->{threads_per_page} = 20; 47 | $self->{opts} = $opts; 48 | 49 | $self->_create_table if $create_new; 50 | 51 | bless $self,$class; 52 | } 53 | 54 | sub _connect { 55 | my $self=shift; 56 | 57 | my $dbh = DBI->connect( 58 | ($self->{db_connstr} or "DBI:mysql:database=$self->{db_name};host=$self->{db_host}"), 59 | $self->{db_username}, 60 | $self->{db_password}, 61 | {AutoCommit=>1,PrintError=>0,mysql_enable_utf8=>1}, 62 | ); 63 | return if !$dbh; 64 | $dbh->do("set names $self->{db_charset}"); 65 | 66 | $dbh 67 | } 68 | 69 | sub _create_table($){ 70 | my $self=shift; 71 | 72 | $self->{dbh}->do(<{table} ( 74 | doc_id int unsigned not null auto_increment, 75 | id decimal(39,0) unsigned not null default '0', 76 | num int unsigned not null, 77 | subnum int unsigned not null, 78 | parent int unsigned not null default '0', 79 | timestamp int unsigned not null, 80 | preview varchar(20), 81 | preview_w smallint unsigned not null default '0', 82 | preview_h smallint unsigned not null default '0', 83 | media text, 84 | media_w smallint unsigned not null default '0', 85 | media_h smallint unsigned not null default '0', 86 | media_size int unsigned not null default '0', 87 | media_hash varchar(25), 88 | media_filename varchar(20), 89 | 90 | spoiler bool not null default '0', 91 | deleted bool not null default '0', 92 | capcode enum('N', 'M', 'A', 'G') not null default 'N', 93 | 94 | email varchar(100), 95 | name varchar(100), 96 | trip varchar(25), 97 | title varchar(100), 98 | comment text, 99 | delpass tinytext, 100 | sticky bool not null default '0', 101 | 102 | primary key (doc_id), 103 | 104 | unique num_subnum_index (num, subnum), 105 | index id_index(id), 106 | index num_index(num), 107 | index subnum_index(subnum), 108 | index parent_index(parent), 109 | index timestamp_index(timestamp), 110 | index media_hash_index(media_hash), 111 | index email_index(email), 112 | index name_index(name), 113 | index trip_index(trip), 114 | index fullname_index(name,trip), 115 | fulltext index comment_index(comment) 116 | ) engine=myisam 117 | collate $self->{db_charset}_general_ci; 118 | HERE 119 | } 120 | 121 | sub _read_post($$){ 122 | my $self=shift; 123 | my($doc_id,$id,$num,$subnum,$parent,$date,$preview,$preview_w,$preview_h, 124 | $media,$media_w,$media_h,$media_size,$media_hash,$media_filename,$spoiler, 125 | $deleted,$capcode,$email,$name,$trip,$title,$comment,$delpass,$sticky 126 | )=@{ $_[0] }; 127 | 128 | $self->new_post( 129 | media => $media, 130 | media_hash => $media_hash, 131 | media_filename=> $media_filename, 132 | media_size =>($media_size or 0), 133 | media_w =>($media_w or 0), 134 | media_h =>($media_h or 0), 135 | preview => $preview, 136 | preview_w =>($preview_w or 0), 137 | preview_h =>($preview_h or 0), 138 | num => $num, 139 | subnum => $subnum, 140 | parent => $parent, 141 | title => $title, 142 | email => $email, 143 | name => $name, 144 | trip => $trip, 145 | date => $date, 146 | comment => $comment, 147 | password => $delpass, 148 | spoiler => $spoiler, 149 | deleted => $deleted, 150 | sticky => $sticky, 151 | capcode => $capcode, 152 | userid => $id, 153 | ) 154 | } 155 | 156 | sub _read_thread($@){ 157 | my $self=shift; 158 | my($list)=@_; 159 | 160 | my $t=$self->new_thread( 161 | omposts => 0, 162 | omimages => 0, 163 | posts => [], 164 | ); 165 | 166 | for my $ref(@$list){ 167 | my($post)=$self->_read_post($ref); 168 | 169 | push @{$t->{posts}},$post; 170 | 171 | $t->{num}||=$post->{num}; 172 | } 173 | 174 | $t; 175 | } 176 | sub get_post($$;$){ 177 | my $self=shift; 178 | my($num)=(@_); 179 | ($num,my $subnum)=((split /,/,$num),0); 180 | 181 | my($ref)=$self->query("select * from $self->{table} where num=? and subnum=?",$num,$subnum) or return; 182 | $ref->[0] or $self->error(FORGET_IT,"Post not found"),return; 183 | 184 | $self->ok; 185 | 186 | $self->_read_post($ref->[0]); 187 | } 188 | 189 | sub get_thread($$){ 190 | my $self=shift; 191 | my($thread)=@_; 192 | 193 | $self->_read_thread($self->query(<{table} where num=? union select * from $self->{table} where parent=? order by num,subnum asc 195 | HERE 196 | } 197 | 198 | sub get_image($$){ 199 | my $self=shift; 200 | my($media)=(@_); 201 | 202 | my($ref)=$self->query("select * from $self->{table} where media_filename=?",$media) or return; 203 | $ref->[0] or $self->error(FORGET_IT,"Image not found in database: $media"),return; 204 | 205 | $self->ok; 206 | 207 | $self->_read_post($ref->[0]); 208 | } 209 | 210 | sub get_thread_range($$$){ 211 | my $self=shift; 212 | my($thread,$limit)=@_; 213 | 214 | $self->_read_thread($self->query(<{table} where num=? union 217 | select * from $self->{table} where parent=? order by num desc,subnum desc limit ?) as tbl 218 | order by tbl.num asc,subnum asc 219 | HERE 220 | } 221 | 222 | sub get_page($$){ 223 | my $self=shift; 224 | my($pagetext)=@_; 225 | 226 | my($shadow,$page)=$pagetext=~/^(S)?(\d+)$/; 227 | 228 | $page-=1; 229 | $page=0 if $page<0; 230 | 231 | my $p=$self->new_page($page); 232 | my @list; 233 | 234 | my @results=@{ $self->query($shadow?<{threads_per_page},$self->{threads_per_page}*$page):(),$self->{threads_per_page},$self->{threads_per_page}*$page) or return }; 235 | select * from 236 | (select $self->{table}.*,time_ghost_bump from 237 | $self->{table} 238 | join 239 | (select parent, time_ghost_bump from $self->{table}_threads order by time_ghost_bump desc limit ? offset ?) as threads 240 | on threads.parent=$self->{table}.num 241 | union 242 | select $self->{table}.*,time_ghost_bump from 243 | $self->{table} 244 | join 245 | (select parent, time_ghost_bump from $self->{table}_threads order by time_ghost_bump desc limit ? offset ?) as threads 246 | on threads.parent=$self->{table}.parent 247 | ) as posts 248 | where time_ghost_bump is not null order by time_ghost_bump desc,num,subnum asc; 249 | HERE 250 | select $self->{table}.* from 251 | (select parent from $self->{table}_threads order by parent desc limit ? offset ?) as threads join $self->{table} 252 | on threads.parent=$self->{table}.num or threads.parent=$self->{table}.parent 253 | order by threads.parent desc,num,subnum asc 254 | THERE 255 | for my $ref(@results){ 256 | my($doc_id,$id,$num,$subnum,$parent)=@$ref; 257 | 258 | unless($parent){ 259 | push @{$p->{threads}},$self->_read_thread(\@list) if @list; 260 | @list=($ref); 261 | } elsif(@list){ 262 | push @list,$ref; 263 | } 264 | } 265 | push @{$p->{threads}},$self->_read_thread(\@list) if @list; 266 | 267 | $self->ok; 268 | 269 | $p; 270 | } 271 | 272 | sub search($$$$){ 273 | my $self=shift; 274 | my($text,$limit,$offset,%settings)=@_; 275 | my $dbh=$self->{dbh}; 276 | 277 | $limit=int $limit; 278 | $offset=defined $offset ? int $offset : 0; 279 | 280 | my @conditions; 281 | my @index_hint; 282 | 283 | push @conditions,"name=".$dbh->quote($settings{name}) and 284 | push @index_hint,"name_index" 285 | if $settings{name}; 286 | 287 | push @conditions,"trip=".$dbh->quote($settings{tripcode}) and 288 | push @index_hint,"trip_index" 289 | if $settings{tripcode}; 290 | 291 | push @conditions,"email=".$dbh->quote($settings{email}) and 292 | push @index_hint,"email_index" 293 | if $settings{email}; 294 | 295 | push @conditions,"timestamp > " . str2time($settings{datefrom}) 296 | if str2time($settings{datefrom}); 297 | 298 | push @conditions,"timestamp < " . str2time($settings{dateto}) 299 | if str2time($settings{dateto}); 300 | 301 | push @conditions,"media_hash=".$dbh->quote($settings{media_hash}) and 302 | push @index_hint,"media_hash_index" 303 | if $settings{media_hash}; 304 | 305 | my $cap = substr(ucfirst($settings{cap}), 0, 1); 306 | push @conditions,"capcode=".$dbh->quote($cap) 307 | if $settings{cap} and not $settings{cap} eq 'all'; 308 | 309 | push @conditions,"deleted=1" 310 | if $settings{showdel} and not $settings{shownodel}; 311 | 312 | push @conditions,"deleted=0" 313 | if $settings{shownodel} and not $settings{showdel}; 314 | 315 | push @conditions,"subnum!=0" 316 | if $settings{showint} and not $settings{showext}; 317 | 318 | push @conditions,"subnum=0" 319 | if $settings{showext} and not $settings{showint}; 320 | 321 | push @conditions,"parent=0" 322 | if $settings{op}; 323 | 324 | my $ord=$settings{ord}; 325 | my $query_ord="timestamp desc"; 326 | 327 | $query_ord="timestamp asc" if $ord and $ord eq 'old'; 328 | 329 | my $condition=join "",map{"$_ and "}@conditions; 330 | 331 | my $index_hint=@index_hint? 332 | "use index(".(join ",",@index_hint).")": 333 | ""; 334 | 335 | my $query=(0 and $text and $ord eq 'rel' and $text!~/[\*\+\-]/)? 336 | "select *,match(comment) against(". 337 | $dbh->quote($text). 338 | ") as score from $self->{table} $index_hint where $condition match(comment) against(". 339 | $dbh->quote(join " ",map{"+$_"}split /\s+/,$text). 340 | " in boolean mode) order by score desc, timestamp desc limit $limit offset $offset;": 341 | 342 | $text? 343 | "select * from $self->{table} $index_hint where $condition match(comment) against(". 344 | $dbh->quote($text). 345 | " in boolean mode) order by $query_ord limit $limit offset $offset;": 346 | 347 | "select * from $self->{table} $index_hint where $condition 1 order by $query_ord limit $limit offset $offset"; 348 | 349 | my($ref)=($self->query($query) or return); 350 | 351 | map{$self->_read_post($_)} @$ref 352 | } 353 | 354 | sub post($;%){ 355 | my $self=shift; 356 | my(%info)=@_; 357 | my($thread)=($info{parent} or die "can only post replies to threads, not create new threads"); 358 | my $date=($info{date} or time); 359 | my($ref); 360 | 361 | $ref=$self->query("select count(*) from $self->{table} where id=? and timestamp>?",$info{id},$date-$self->{renzoku}); 362 | $self->error(TRY_AGAIN,"You can't post that fast"),return 363 | if $ref->[0]->[0]; 364 | 365 | $ref=$self->query("select count(*) from $self->{table} where id=? and timestamp>? and comment=?",$info{id},$date-$self->{renzoku3},$info{comment}); 366 | $self->error(TRY_AGAIN,"You already posted that, cowboy!"),return 367 | if $ref->[0]->[0]; 368 | 369 | ($info{name},$info{trip})=$self->tripcode($info{name}); 370 | 371 | $self->insert({ 372 | %info 373 | }) or return; 374 | 375 | $ref=$self->query("select num,subnum from $self->{table} where id=? and timestamp=?",$info{id},$date) or return; 376 | 377 | $ref and $ref->[0] and $ref->[0] and (ref $ref->[0] eq 'ARRAY') or $self->error(FORGET_IT,"I forgot where I put it"); 378 | 379 | $self->ok; 380 | 381 | $ref->[0]->[0].($ref->[0]->[1]?",$ref->[0]->[1]":"") 382 | } 383 | 384 | sub delete{ 385 | my $self=shift; 386 | my($num,$pass,$uid)=@_; 387 | ($num,my $subnum)=((split /,/,$num),0); 388 | my($ref); 389 | 390 | $ref=$self->query("select delpass,deleted,id from $self->{table} where num=? and subnum=?",$num,$subnum) or return; 391 | $self->error(FORGET_IT,"Post not found") unless $ref->[0]; 392 | 393 | my($delpass,$deleted,$id)=@{ $ref->[0] }; 394 | $self->error(FORGET_IT,"Post already deleted"),return if $deleted; 395 | 396 | if($uid ne $id){ 397 | $self->error(FORGET_IT,"Wrong password"),return if $delpass ne $pass or not $delpass; 398 | } 399 | 400 | $self->query("update $self->{table} set deleted=1 where num=? and subnum=?",$num,$subnum); 401 | 402 | $self->ok; 403 | } 404 | 405 | sub database_delete{ 406 | my $self=shift; 407 | my($num)=@_; 408 | ($num,my $subnum)=((split /,/,$num),0); 409 | 410 | $self->query("delete from $self->{table} where num=? and subnum=?",$num,$subnum); 411 | } 412 | 413 | sub mark_deleted{ 414 | my $self=shift; 415 | my($num)=@_; 416 | ($num,my $subnum)=((split /,/,$num),0); 417 | 418 | $self->query("update $self->{table} set deleted = 1 where num=? and subnum=?",$num,$subnum); 419 | } 420 | 421 | sub insert{ 422 | # That sprintf below spouts a billion of uninitialized value warnings 423 | # really needs to be fixed, my error logs can't take it easy like this 424 | no warnings; 425 | 426 | my $self=shift; 427 | my($thread)=@_; 428 | my $dbh=$self->{dbh}; 429 | my($num,$parent,@posts); 430 | 431 | if(ref $thread eq 'HASH'){ 432 | $parent=$thread->{parent}; 433 | @posts=($thread); 434 | } elsif(ref $thread eq 'Board::Thread'){ 435 | $num=$thread->{num}; 436 | @posts=@{$thread->{posts}} 437 | } else{ 438 | confess qq{Can only insert threads or hashes, not "}.(ref $thread).qq{"}; 439 | } 440 | 441 | $num or $parent or $self->error(FORGET_IT,"Must specify a thread number for this board"),return 0; 442 | 443 | while($#posts >= 0) { # 1 post or more 444 | my @postbatch; 445 | if($#posts >= 499) { # 500 posts or more 446 | @postbatch = splice(@posts, 0, 500, ()); # get 500 posts for this batch 447 | } else { 448 | @postbatch = @posts; 449 | @posts = (); 450 | } 451 | 452 | $self->query("insert into $self->{table} values ".join(",",map{ 453 | my $h=$_; 454 | 455 | my($location)=$num? 456 | # insert a post with specified number 457 | sprintf "%d,%d",$h->{num},($h->{subnum} or 0): 458 | 459 | # insert a post into thread, automatically get num and subnum 460 | sprintf "(select max(num) from (select * from $self->{table} where parent=%d or num=%d) as x),". 461 | "(select max(subnum)+1 from (select * from $self->{table} where num=(select max(num) from $self->{table} where parent=%d or num=%d)) as x)", 462 | $parent,$parent,$parent,$parent; 463 | 464 | sprintf "(NULL, %s,$location,%u,%u,%s,%d,%d,%s,%d,%d,%d,%s,%s,%d,%d,%s,%s,%s,%s,%s,%s,%s,%d)", 465 | defined $h->{id} ? $h->{id}->bstr() : 0, 466 | $h->{parent}, 467 | $h->{date}, 468 | $h->{preview} ? $dbh->quote($h->{preview}) : 'NULL', 469 | $h->{preview_w}, 470 | $h->{preview_h}, 471 | $h->{media} ? $dbh->quote($h->{media}) : 'NULL', 472 | $h->{media_w}, 473 | $h->{media_h}, 474 | $h->{media_size}, 475 | $h->{media_hash} ? $dbh->quote($h->{media_hash}) : 'NULL', 476 | $h->{media_filename} ? $dbh->quote($h->{media_filename}) : 'NULL', 477 | $h->{spoiler}, 478 | $h->{deleted}, 479 | $h->{capcode} ? $dbh->quote($h->{capcode}) : "'N'", 480 | $h->{email} ? $dbh->quote($h->{email}) : 'NULL', 481 | $h->{name} ? $dbh->quote($h->{name}) : 'NULL', 482 | $h->{trip} ? $dbh->quote($h->{trip}) : 'NULL', 483 | $h->{title} ? $dbh->quote($h->{title}) : 'NULL', 484 | $h->{comment} ? $dbh->quote($h->{comment}) : 'NULL', 485 | $h->{password} ? $dbh->quote($h->{password}) : 'NULL', 486 | $h->{sticky}; 487 | 488 | }@postbatch) . " on duplicate key update comment = values(comment), deleted = values(deleted), 489 | media = coalesce(values(media), media), sticky = (values(sticky) || sticky), 490 | preview = coalesce(values(preview), preview), preview_w = greatest(values(preview_w), preview_w), 491 | preview_h = greatest(values(preview_h), preview_h), media_w = greatest(values(media_w), media_w), 492 | media_h = greatest(values(media_h), media_h), media_size = greatest(values(media_size), media_size), 493 | media_hash = coalesce(values(media_hash), media_hash), media_filename = coalesce(values(media_filename), media_filename)") or return 0; 494 | } 495 | 496 | $self->ok; 497 | 498 | 1; 499 | } 500 | 501 | sub query($$;@){ 502 | my($self,$query)=(shift,shift); 503 | unless($self->{dbh} and $self->{dbh}->ping) { 504 | $self->{dbh} = $self->_connect or ($self->error(FORGET_IT,"Lost connection, cannot reconnect to database."),return 0); 505 | } 506 | 507 | my $dbh=$self->{dbh}; 508 | 509 | my $sth=$dbh->prepare($query) or ($self->error(FORGET_IT,$dbh->errstr),return 0); 510 | 511 | $sth->execute(@_) or ($self->error(FORGET_IT,$dbh->errstr),return 0); 512 | 513 | my $ref=($sth->fetchall_arrayref() or []); 514 | 515 | $sth->finish; 516 | 517 | $self->ok; 518 | 519 | $ref 520 | } 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 1; 540 | -------------------------------------------------------------------------------- /Board/Request.pm: -------------------------------------------------------------------------------- 1 | package Board::Request; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | require Exporter; 7 | our @ISA=qw/Exporter/; 8 | our @EXPORT=qw/THREAD RANGE PAGE POSTNO MEDIA/; 9 | 10 | sub THREAD($;$){ 11 | my $num=$_[0]; 12 | my $lastmod=$_[1]; 13 | my @treq = ($num, $lastmod); 14 | bless \@treq,"Board::Request::THREAD"; 15 | } 16 | sub RANGE($$){ 17 | my $num=$_[0]; 18 | my $limit=$_[1]; 19 | my @range = ($num, $limit); 20 | bless \@range,"Board::Request::RANGE"; 21 | } 22 | sub PAGE($;$){ 23 | my $num=$_[0]; 24 | my $lastmod=$_[1]; 25 | my @preq = ($num, $lastmod); 26 | bless \@preq,"Board::Request::PAGE"; 27 | } 28 | 29 | sub POSTNO($){ 30 | my $num=$_[0]; 31 | bless \$num,"Board::Request::POST"; 32 | } 33 | sub MEDIA($){ 34 | my $media=$_[0]; 35 | bless \$media,"Board::Request::MEDIA"; 36 | } 37 | -------------------------------------------------------------------------------- /Board/Sphinx_Mysql.pm: -------------------------------------------------------------------------------- 1 | package Board::Sphinx_Mysql; 2 | 3 | use strict; 4 | use warnings; 5 | use Carp qw/confess cluck/; 6 | use DBI; 7 | 8 | use Date::Parse; 9 | 10 | use Board::Local; 11 | use Board::Errors; 12 | use Board::Mysql; 13 | our @ISA = qw/Board::Mysql/; 14 | 15 | sub new($$;%){ 16 | my $class=shift; 17 | my $path=shift; 18 | my(%info)=(@_); 19 | 20 | my $sx_host =(delete $info{sx_host} or "127.0.0.1"); 21 | my $sx_port =(delete $info{sx_port} or 9306); 22 | 23 | my $self=$class->SUPER::new($path,%info); 24 | 25 | $self->{sx_host} = $sx_host; 26 | $self->{sx_port} = $sx_port; 27 | 28 | 29 | bless $self,$class; 30 | } 31 | 32 | 33 | sub _connect_sphinx { 34 | my $self=shift; 35 | 36 | return DBI->connect( 37 | "DBI:mysql:database=sphinx;host=$self->{sx_host}:$self->{sx_port}", 38 | '', 39 | '', 40 | {AutoCommit=>1,PrintError=>0,mysql_enable_utf8=>1}, 41 | ) 42 | } 43 | 44 | sub search($$$$){ 45 | my $self=shift; 46 | my($text,$limit,$offset,%settings)=@_; 47 | my $dbh=$self->{dbh}; 48 | 49 | $limit=int $limit; 50 | $offset=defined $offset ? int $offset : 0; 51 | 52 | my @matches; 53 | my @conditions; 54 | my @sql_conditions; 55 | my @index_hint; 56 | 57 | push @matches,'@title '.$self->_sphinx_escape($settings{subject}).' ' 58 | if $settings{subject}; 59 | 60 | push @matches,'@name '.$self->_sphinx_full_escape($settings{name}).' ' 61 | if $settings{name}; 62 | 63 | push @matches,'@trip '.$self->_sphinx_full_escape($settings{tripcode}).' ' 64 | if $settings{tripcode}; 65 | 66 | push @matches,'@media '.$self->_sphinx_full_escape($settings{filename}).' ' 67 | if $settings{filename}; 68 | 69 | push @matches,'@email '.$self->_sphinx_full_escape($settings{email}).' ' 70 | if $settings{email}; 71 | 72 | push @matches,'@comment '.$self->_sphinx_escape($text).' ' 73 | if $text; 74 | 75 | push @conditions,"timestamp > " . str2time($settings{datefrom}) and 76 | push @sql_conditions,"timestamp > " . str2time($settings{datefrom}) 77 | if str2time($settings{datefrom}); 78 | 79 | push @conditions,"timestamp < " . str2time($settings{dateto}) and 80 | push @sql_conditions,"timestamp < " . str2time($settings{dateto}) 81 | if str2time($settings{dateto}); 82 | 83 | push @sql_conditions,"media_hash=".$dbh->quote($settings{media_hash}) and 84 | push @index_hint,"media_hash_index" 85 | if $settings{media_hash}; 86 | 87 | push @conditions,"is_op=1" and 88 | push @sql_conditions,"parent=0" 89 | if $settings{op}; 90 | 91 | push @conditions,"is_deleted=1" and 92 | push @sql_conditions,"deleted=1" 93 | if $settings{showdel} and not $settings{shownodel}; 94 | 95 | push @conditions,"is_deleted=0" and 96 | push @sql_conditions,"deleted=0" 97 | if $settings{shownodel} and not $settings{showdel}; 98 | 99 | push @conditions,"is_internal=1" and 100 | push @sql_conditions,"subnum!=0" 101 | if $settings{showint} and not $settings{showext}; 102 | 103 | push @conditions,"is_internal=0" and 104 | push @sql_conditions,"subnum=0" 105 | if $settings{showext} and not $settings{showint}; 106 | 107 | my $cap = substr(ucfirst($settings{cap}), 0, 1); 108 | push @conditions,"cap=".ord($cap) and 109 | push @sql_conditions,"capcode=".$dbh->quote($cap) 110 | if $settings{cap} and not $settings{cap} eq 'all'; 111 | 112 | my $ord=$settings{ord}; 113 | my $query_ord="timestamp desc"; 114 | 115 | $query_ord="timestamp asc" if $ord and $ord eq 'old'; 116 | 117 | my $res = $settings{res}; 118 | my $op = 0; 119 | $op = 1 if $res and $res eq 'op'; 120 | 121 | my $condition=join "",map{" and $_"}@conditions; 122 | my $match=$dbh->quote(join "",@matches); 123 | 124 | my $sql_condition=join "",map{"$_ and "}@sql_conditions; 125 | my $index_hint=@index_hint? 126 | "use index(".(join ",",@index_hint).")": 127 | ""; 128 | 129 | my $query; 130 | if($match eq "''" and !$op) { 131 | $query = "select * from $self->{table} $index_hint where $sql_condition 1 order by $query_ord limit $offset, $limit"; 132 | } else { 133 | my $sel_id = "id"; 134 | my $query_grp = ""; 135 | if($op) { 136 | $sel_id = "tnum"; 137 | $query_grp = "group by tnum"; 138 | } 139 | 140 | my $squery="select $sel_id from $self->{table}_ancient, $self->{table}_main, $self->{table}_delta 141 | where match($match) $condition $query_grp order by $query_ord limit $offset, $limit option max_matches=5000;"; 142 | my($sref)=($self->query_sphinx($squery) or return); 143 | return if !@$sref; 144 | 145 | if(!$op) { 146 | $query = "select * from $self->{table} where doc_id in (". join(",",map{@$_[0]} @$sref) . ") order by $query_ord;"; 147 | } else { 148 | $query = "select * from $self->{table} where num in (". join(",",map{@$_[0]} @$sref) . ") and subnum = 0 order by $query_ord;"; 149 | } 150 | } 151 | 152 | my($ref)=($self->query($query) or return); 153 | 154 | map{$self->_read_post($_)} @$ref 155 | } 156 | 157 | sub _sphinx_full_escape($) { 158 | my ($self, $query)=(shift,shift); 159 | $query=~ s/([=\(\)|\-!@~"&\/\\\^\$\=])/\\$1/g; 160 | return $query; 161 | } 162 | 163 | sub _sphinx_escape($) { 164 | my ($self, $query)=(shift,shift); 165 | $query=~ s/([=\(\)\!@~&\/\\\^\$\=])/\\$1/g; 166 | $query=~ s/\"([^\s]+)-([^\s]*)\"/$1-$2/g; 167 | $query=~ s/([^\s]+)-([^\s]*)/"$1\\-$2"/g; 168 | return $query; 169 | } 170 | 171 | sub _log_bad_query($) { 172 | my ($self,$query) = (shift,shift); 173 | open HANDLE,">>bad_queries.txt"; 174 | print HANDLE "Bad query: $query\n"; 175 | close HANDLE; 176 | } 177 | 178 | sub query_sphinx($$;@){ 179 | my($self,$query)=(shift,shift); 180 | my $dbh_sphinx = $self->_connect_sphinx or ($self->error(FORGET_IT,"Search backend seems to be offline. Contact website admin?"),return 0);; 181 | 182 | unless($dbh_sphinx and $dbh_sphinx->ping) { 183 | $dbh_sphinx = $self->_connect_sphinx or ($self->error(FORGET_IT,"Lost connection, cannot reconnect to search backend."),return 0); 184 | } 185 | 186 | my $sth=$dbh_sphinx->prepare($query) or return []; 187 | 188 | $sth->execute(@_) or ($self->error(FORGET_IT,"I can't figure your search query out! Try reading the search FAQ. Report a new bug or send an email if you think your query should have worked."),return 0); 189 | 190 | my $ref=($sth->fetchall_arrayref() or []); 191 | $sth->finish; 192 | $self->ok; 193 | $dbh_sphinx->disconnect(); 194 | 195 | $ref 196 | } 197 | 198 | 199 | 200 | 201 | 202 | 1; 203 | -------------------------------------------------------------------------------- /Board/Utilities.pm: -------------------------------------------------------------------------------- 1 | package Board::Utilities; 2 | 3 | use strict; 4 | 5 | use Exporter qw/import/; 6 | our @EXPORT=qw/size_string file_copy status cat now vsleep usage $board $num $home %FLAGS $USAGE_ARGS $USAGE_TEXT DOCS/; 7 | our($board,$num,$home,%FLAGS,$USAGE_ARGS,$USAGE_TEXT),; 8 | 9 | use Time::HiRes qw/usleep gettimeofday/; 10 | $|++; 11 | 12 | use constant DOCS => 'b:/doc'; 13 | 14 | -d DOCS or die "Directory ".DOCS." doesn't exist"; 15 | 16 | our($home); 17 | BEGIN{($home)=$0=~/(.*)[\/\\].*/;$home||=".";} 18 | use lib $home; 19 | 20 | sub usage(){ 21 | print <; 47 | 48 | close HANDLE; 49 | 50 | $data 51 | } @_; 52 | } 53 | sub file_copy($$;$){ 54 | my($if,$of,$noise)=@_; 55 | my $buf; 56 | 57 | CORE::open O,">",$of or die "$! - $of"; 58 | 59 | binmode O; 60 | 61 | print O cat $if; 62 | print O $noise if $noise; 63 | 64 | close O; 65 | } 66 | 67 | BEGIN{ 68 | my($snow,$sus)=gettimeofday; 69 | sub now(){ 70 | my($now,$us)=gettimeofday; 71 | return 1000000*($now-$snow)+($us-$sus); 72 | } 73 | } 74 | BEGIN{ 75 | my $length=0; 76 | sub status(@){ 77 | print "\b"x$length," "x$length,"\b"x$length; 78 | 79 | my(@lines)=split /\r?\n/,join "",@_; 80 | print join "\n",@lines; 81 | $length=length pop @lines; 82 | } 83 | } 84 | sub vsleep($;$){ 85 | my($duration,$period)=(@_,100000); 86 | my $end=now+$duration; 87 | 88 | while($end>now){ 89 | status sprintf "sleeping %.1fs",($end- now)/1000000; 90 | 91 | usleep $period; 92 | } 93 | 94 | status; 95 | } 96 | 97 | my(@ARGS); 98 | while($_=shift @ARGV){ 99 | /^--(\w+)$/ and do{ 100 | $FLAGS{$1}=shift @ARGV; 101 | },next; 102 | 103 | /^--(\w+)=(.*)$/ and do{ 104 | $FLAGS{$1}=$2; 105 | },next; 106 | 107 | /^-(\w+)$/ and do{ 108 | $FLAGS{$_}=1 109 | foreach split //,$1; 110 | },next; 111 | 112 | push @ARGS,$_; 113 | } 114 | 115 | @ARGV=@ARGS; 116 | 117 | my $board_line=shift @ARGV 118 | or die "You didn't specify board name\n"; 119 | 120 | our($name,$num)=($board_line or "")=~m!^>*(?:/?([a-z]+))?(?:/?(\d+))?$!i 121 | or die "Wrong format: $board_line\n"; 122 | 123 | my(@OPTS)=(($name or 'jp'),timeout=>24); 124 | 125 | push @OPTS,proxy=>$FLAGS{proxy} if $FLAGS{proxy}; 126 | 127 | use Board::Yotsuba; 128 | our $board=Board::Yotsuba->new(@OPTS) 129 | or die "No such board: $name\n"; 130 | 131 | 1; 132 | -------------------------------------------------------------------------------- /Board/WWW.pm: -------------------------------------------------------------------------------- 1 | package Board::WWW; 2 | 3 | use strict; 4 | use warnings; 5 | use Carp qw/confess/; 6 | 7 | use Board; 8 | use Board::Errors; 9 | our @ISA=qw/Board/; 10 | 11 | use LWP::UserAgent; 12 | use LWP::ConnCache; 13 | use HTTP::Request::Common; 14 | 15 | sub new{ 16 | my $class=shift; 17 | my(%info)=@_; 18 | 19 | push(@LWP::Protocol::http::EXTRA_SOCK_OPTS, "LocalAddr" => $info{ipaddr}) if $info{ipaddr}; 20 | 21 | my $ua=LWP::UserAgent->new; 22 | $ua->agent(delete $info{agent} or "Fuuka Dumper/0.10"); 23 | $ua->proxy('http', "http://".($info{proxy})) if $info{proxy}; 24 | $ua->timeout($info{timeout}) if $info{timeout}; 25 | 26 | my $conn_cache = LWP::ConnCache->new; 27 | $conn_cache->total_capacity([1]) ; 28 | $ua->conn_cache($conn_cache) ; 29 | 30 | my $self=$class->SUPER::new(%info); 31 | 32 | $self->{agent}=$ua; 33 | $self->{proxy}=$info{proxy} if $info{proxy}; 34 | 35 | bless $self,$class; 36 | } 37 | 38 | sub wget($$;$$){ 39 | my($self,$link,$referer,$lastmod)=@_; 40 | my($res,$text); 41 | 42 | my $req=(GET $link); 43 | $req->referer($referer) if $referer; 44 | $req->accept_decodable() if $req->can('accept_decodable'); 45 | $req->header("If-Modified-Since", $lastmod) if $lastmod; 46 | 47 | my $retrycount = 3; 48 | 49 | MAINLOOP: 50 | $res=$self->{agent}->request($req); 51 | 52 | if($res->is_success) { 53 | my $dec_error = 0; 54 | eval { 55 | local $SIG{__DIE__} = sub{$dec_error=1}; 56 | $text=$res->decoded_content(); 57 | }; 58 | 59 | (--$retrycount and sleep(1) and goto MAINLOOP) if $dec_error; 60 | $self->error(FORGET_IT,"Can't decode content"),return if $dec_error; 61 | $self->error(0),return ($text,$res); 62 | } else { 63 | my($no,$line)=$res->status_line=~/(\d+) (.*)/; 64 | ($retrycount-- and goto MAINLOOP) if($no =~ /^500/ and $retrycount > 0); 65 | $self->error(FORGET_IT,$line); 66 | } 67 | } 68 | 69 | sub wget_ref($$;$$) { 70 | my($self,$link,$referer,$lastmod)=@_; 71 | my($res,$text); 72 | 73 | my $req=(GET $link); 74 | $req->referer($referer) if $referer; 75 | $req->header("If-Modified-Since", $lastmod) if $lastmod; 76 | 77 | my $retrycount = 3; 78 | 79 | MAINLOOP: 80 | $res=$self->{agent}->request($req); 81 | 82 | $self->error(0),return $res->content_ref if $res->is_success; 83 | my($no,$line)=$res->status_line=~/(\d+) (.*)/; 84 | ($retrycount-- and goto MAINLOOP) if($no =~ /^500/ and $retrycount > 0); 85 | 86 | $self->error(FORGET_IT,$line); 87 | } 88 | 89 | sub wpost_ext($$$$%){ 90 | my($self,$link,$referer,$contenttype,%params)=@_; 91 | my($res,$text); 92 | 93 | my $req=(POST $link, 94 | Content_Type =>$contenttype, 95 | Content =>[%params], 96 | ); 97 | $req->referer($referer) if $referer; 98 | 99 | 100 | MAINLOOP: 101 | $res=$self->{agent}->request($req); 102 | $text=$res->content; 103 | 104 | $self->error(0),return $text if $res->is_success; 105 | my($no,$line)=$res->status_line=~/(\d+) (.*)/; 106 | for($res->status_line){ 107 | /^500/ and $self->warn("www","$_") and goto MAINLOOP; 108 | } 109 | 110 | $self->error(FORGET_IT,$line); 111 | } 112 | 113 | sub wpost($$$%){ 114 | my($self,$link,$referer,%params)=@_; 115 | $self->wpost_ext($link,$referer,'multipart/form-data;boundary=1',%params); 116 | } 117 | 118 | sub wpost_x_www($$$%){ 119 | my($self,$link,$referer,%params)=@_; 120 | $self->wpost_ext($link,$referer,'application/x-www-form-urlencoded',%params); 121 | } 122 | 123 | sub do_clean($$){ 124 | my($self)=shift; 125 | 126 | for(shift){ 127 | s/&\#(\d+);/chr $1/gxse; 128 | s!>!>!g; 129 | s!<! "http://boards.4chan.org/$board", 16 | img_link => "http://i.4cdn.org/$board", 17 | preview_link => "http://0.t.4cdn.org/$board", 18 | html => "http://boards.4chan.org/$board/", 19 | script => "http://sys.4chan.org/$board/imgboard.php" 20 | }; 21 | } 22 | 23 | sub new{ 24 | my $class=shift; 25 | my($board)=shift; 26 | my $self=$class->SUPER::new(@_); 27 | 28 | $self->{name}=$board; 29 | $self->{renzoku}=20*1000000; 30 | my $board_list = get_board_list($board); 31 | $self->{$_} = $board_list->{$_} 32 | foreach keys %$board_list; 33 | 34 | $self->{opts}=[{@_},$board]; 35 | 36 | 37 | bless $self,$class; 38 | } 39 | 40 | my %size_multipliers=( 41 | B => 1, 42 | KB => 1024, 43 | MB => 1024*1024, 44 | ); 45 | 46 | sub parse_filesize($$){ 47 | my $self=shift; 48 | my($text)=@_; 49 | 50 | my($v,$m)=$text=~/([\.\d]+) \s (.*)/x; 51 | 52 | $size_multipliers{$m} or $self->troubles("error parsing filesize: '$text'") and return 0; 53 | $v*$size_multipliers{$m}; 54 | } 55 | 56 | sub parse_date($$){ 57 | my $self=shift; 58 | my($text)=@_; 59 | 60 | my($mon,$mday,$year,$hour,$min,$sec)= 61 | $text=~m!(\d+)/(\d+)/(\d+) \(\w+\) (\d+):(\d+)(?::(\d+))?!x; 62 | 63 | use Time::Local; 64 | timegm(($sec or (time%60)),$min,$hour,$mday,$mon-1,$year); 65 | } 66 | 67 | 68 | sub new_yotsuba_post($$$$$$$$$$$$){ 69 | my $self=shift; 70 | my($link,$orig_filename,$spoiler,$filesize,$width,$height,$filename,$twidth,$theight, 71 | $md5,$num,$title,$email,$name,$trip,$capcode,$date,$sticky,$comment, 72 | $omitted,$parent) = @_; 73 | 74 | my($type, $media, $preview, $timestamp); 75 | 76 | # Extract extra info we need from media links 77 | if($link){ 78 | (my $number, $type)=$link=~m!/src/(\d+)\.(\w+)!; 79 | $orig_filename //= "$number.$type"; 80 | $media = ($filename or "$number.$type"); 81 | $preview = "${number}s.jpg"; 82 | } else { 83 | ($type, $media, $preview) = ("", "", ""); 84 | } 85 | 86 | # Thumbnail dimensions are meaningless if the image is spoilered 87 | if($spoiler) { 88 | $twidth = 0; 89 | $theight = 0; 90 | } 91 | 92 | $timestamp = $self->parse_date($date); 93 | 94 | $self->new_post( 95 | link =>($link or ""), 96 | type =>($type or ""), 97 | media => $media, 98 | media_hash => $md5, 99 | media_filename => $orig_filename, 100 | media_size => ($filesize and $self->parse_filesize($filesize) or 0), 101 | media_w => ($width or 0), 102 | media_h => ($height or 0), 103 | preview => $preview, 104 | preview_w => ($twidth or 0), 105 | preview_h => ($theight or 0), 106 | num => $num, 107 | parent => $parent, 108 | title => ($title and $self->_clean_simple($title) or ""), 109 | email => ($email or ""), 110 | name => $self->_clean_simple($name), 111 | trip => ($trip or ""), 112 | date => $timestamp, 113 | comment => $self->do_clean($comment), 114 | spoiler => ($spoiler ? 1 : 0), 115 | deleted => 0, 116 | sticky => ($sticky ? 1 : 0), 117 | capcode => ($capcode or 'N'), 118 | omitted => ($omitted ? 1 : 0) 119 | ); 120 | } 121 | 122 | sub parse_thread($$){ 123 | my $self=shift; 124 | my($text)=@_; 125 | my $post = $self->parse_post($text,0); 126 | 127 | $self->troubles("Error parsing thread (see failed post above)\n------\n") and return 128 | unless defined $post->{num}; 129 | 130 | my $omposts = $1 if 131 | $text=~m!\s*([0-9]*) \s posts \s omitted!xs; 132 | 133 | my $omimages = $1 if 134 | $text=~m!\(([0-9]*) \s have \s images\)!xs; 135 | 136 | $self->new_thread( 137 | num => $post->{num}, 138 | omposts => ($omposts or 0), 139 | omimages => ($omimages or 0), 140 | posts => [$post], 141 | allposts => [$post->{num}] 142 | ) 143 | } 144 | 145 | sub parse_post($$$){ 146 | my $self = shift; 147 | my($post,$parent) = @_; 148 | my ($num, $title, $email, $name, $trip, $capcode, $capalt, $uid, $date, $link, 149 | $spoiler, $filesize, $width, $height, $media, $md5, $twidth, $theight, $comment, 150 | $omitted, $sticky, $filename, $capold, $spoilerfn); 151 | 152 | $num = $1 if 153 | $post=~m!
!xs; 154 | 155 | $title = $1 if 156 | $post=~m!([^<]*)!xs; 157 | 158 | $email = $1 if 159 | $post=~m!!xs; 160 | 161 | ($name, $trip, $capcode, $capalt, $uid) = ($1, $2, $3, $4, $5) if 162 | $post=~m!(?:]*>)?([^<]*)(?:)? \s* 163 | (?:(?:]*>)?([^<]*)(?:)?)? \s* 164 | (?:\(ID: \s (?: ]*>(.)[^)]* 167 | | ([^)]*))\))? 168 | !xs; 169 | 170 | $capold = $1 if 171 | $post=~m!]*>\#\# \s (.)[^<]*!xs; 172 | 173 | $capcode //= $capalt // $capold; 174 | 175 | $date = $1 if 176 | $post=~m!]*>([^<]*)!xs; 177 | 178 | ($spoilerfn, $link, $spoiler, $filesize, $width, $height, $filename, $md5, $theight, $twidth) = 179 | ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) if 180 | $post=~m!
File: \s -\((Spoiler \s Image,)? \s* ([\d\sGMKB\.]+), 181 | \s* (\d+)x(\d+) (?:, \s* ([^<]*))?.*? 182 | ]*>(.*?)!xs; 189 | 190 | $sticky = 1 if 191 | $post=~m!Sticky]*>!xs; 192 | 193 | $omitted = 1 if 194 | $post=~m!Comment \s too \s long!xs; 195 | 196 | $self->troubles("Error parsing post $num:\n------\n$post\n------\n") and return 197 | unless ($num and defined $name and $date and defined $comment); 198 | 199 | $self->new_yotsuba_post( 200 | $link,undef,$spoiler,$filesize,$width,$height,$filename,$twidth,$theight, 201 | $md5,$num,$title,$email,$name,$trip,$capcode,$date,$sticky,$comment,$omitted, 202 | $parent 203 | ) 204 | } 205 | 206 | sub link_page($$){ 207 | my $self=shift; 208 | my($page)=@_; 209 | 210 | $page||=""; 211 | 212 | "$self->{link}/$page"; 213 | } 214 | 215 | sub link_thread($$){ 216 | my $self=shift; 217 | my($thread)=@_; 218 | 219 | $thread? 220 | "$self->{link}/res/$thread": 221 | $self->link_page(0); 222 | } 223 | 224 | sub link_post($$){ 225 | my $self=shift; 226 | my($postno)=@_; 227 | 228 | "$self->{link}/imgboard.php?res=$postno" 229 | } 230 | 231 | sub magnitude($$){ 232 | my $self=shift; 233 | local($_)=@_; 234 | 235 | /Flood detected/ and return (TRY_AGAIN,"flood"); 236 | 237 | /Thread specified does not exist./ and return (NONEXIST,"thread doesn't exist"); 238 | 239 | /Duplicate file entry detected./ and return (ALREADY_EXISTS,"duplicate file"); 240 | /File too large/ and return (TOO_LARGE,"file too large"); 241 | 242 | /^\d+ / and return (FORGET_IT,$_); 243 | 244 | /Max limit of \d+ image replies has been reached./ and return (THREAD_FULL,"image limit"); 245 | 246 | /No text entered/ and return (FORGET_IT,"no text entered"); 247 | 248 | /Can't find the post / and return (NONEXIST,"post doesn't exist"); 249 | 250 | /No file selected./ and return (FORGET_IT,"no file selected"); 251 | die $_; 252 | } 253 | 254 | 255 | sub get_media_preview($$){ 256 | my $self=shift; 257 | my($post)=@_; 258 | 259 | $post->{preview} or $self->error(FORGET_IT,"This post doesn't have any media preview"),return; 260 | 261 | my $data=$self->wget_ref("$self->{preview_link}/thumb/$post->{preview}?" . time); 262 | 263 | $data; 264 | } 265 | 266 | 267 | sub get_media($$){ 268 | my $self=shift; 269 | my($post)=@_; 270 | 271 | $post->{media_filename} or $self->error(FORGET_IT,"This post doesn't have any media"),return; 272 | 273 | my $data=$self->wget_ref("$self->{img_link}/src/$post->{media_filename}?" . time); 274 | 275 | $data; 276 | } 277 | 278 | sub get_post($$){ 279 | my $self=shift; 280 | my($postno)=@_; 281 | 282 | my($res,undef)=$self->wget($self->link_post($postno)); 283 | return if $self->error; 284 | 285 | my($thread)=$res=~m!"0;URL=http://.*/res/(\d+)\.html#$postno"! 286 | or $self->error(FORGET_IT,"Couldn't find post $postno"),return; 287 | 288 | my $contents=$self->get_thread($thread); 289 | return if $self->error; 290 | 291 | my($post)=grep{$_->{num}==$postno} @{$contents->{posts}} 292 | or $self->error(FORGET_IT,"Couldn't find post $postno"),return; 293 | 294 | 295 | $self->error(0); 296 | $post 297 | } 298 | 299 | sub get_thread($$;$){ 300 | my $self=shift; 301 | my($thread,$lastmod)=@_; 302 | 303 | my ($res,$httpres)=$self->wget($self->link_thread($thread),undef,$lastmod); 304 | return if $self->error; 305 | 306 | my $t=undef; 307 | while($res=~m! 308 | (
]*>.*? 309 | \s*
310 | (?: \s*
]*> \s* .*?)?) 311 | !gxs) { 312 | my($text,$type)=($1,$2); 313 | if($type eq 'opContainer') { 314 | $self->troubles("Two thread posts in one thread at " . $self->link_thread($thread) . "(thread $thread). Already had $t->{num}, trying to parse:\n$text\n\n------\n\n") if $t; 315 | $t=$self->parse_thread($text); 316 | } else { 317 | $self->troubles("posts without thread:\n$res\n\n------\n\n") unless $t; 318 | my $pt = $self->parse_post($text,$t->{num}); 319 | next unless $pt; 320 | push @{$t->{posts}},$pt; 321 | push @{$t->{allposts}},$pt->{num}; 322 | } 323 | } 324 | 325 | $t->{lastmod} = $httpres->header("Last-Modified"); 326 | 327 | $self->ok; 328 | $t 329 | } 330 | 331 | sub get_page($$){ 332 | my $self=shift; 333 | my($page,$lastmod)=@_; 334 | 335 | my($res,$httpres)=$self->wget($self->link_page($page),undef,$lastmod); 336 | return if $self->error; 337 | 338 | my $t=undef; 339 | my $p=$self->new_page($page); 340 | while($res=~m! 341 | (
]*>.*? 342 | \s*
343 | (?: \s*
]*> \s* .*?)?) 344 | !gxs) { 345 | my($text,$type)=($1,$2); 346 | if($type eq 'opContainer') { 347 | $t=$self->parse_thread($text); 348 | push @{$p->{threads}},$t if $t; 349 | } else { 350 | my $pt = $self->parse_post($text,$t->{num}); 351 | next unless $pt; 352 | push @{$t->{posts}},$pt; 353 | push @{$t->{allposts}},$pt->{num}; 354 | } 355 | } 356 | 357 | $p->{lastmod} = $httpres->header("Last-Modified"); 358 | 359 | $self->error(0); 360 | $p 361 | } 362 | 363 | sub post($;%){ 364 | my $self=shift; 365 | my(%info)=@_; 366 | my($thread)=($info{parent} or 0); 367 | 368 | local $_=$self->wpost( 369 | $self->{script}, 370 | $self->link_thread($thread), 371 | 372 | MAX_FILE_SIZE => '2097152', 373 | resto => $thread, 374 | name =>($info{name} or ''), 375 | email =>($info{email} or $info{mail} or ''), 376 | sub =>($info{title} or ''), 377 | com =>($info{comment} or ''), 378 | upfile =>($info{file} or []), 379 | pwd =>($info{password} or rand 0xffffffff), 380 | mode => 'regist', 381 | ); 382 | 383 | return if $self->error; 384 | my($last)=(//,//); 385 | $self->error(0),return $last if /pdating page/; 386 | $self->error($self->magnitude($1)),return if /]*>(?:Error:)?\s*(.*?)

/; 387 | die "Unknown error when posting:-------$_--------" 388 | } 389 | 390 | sub delete{ 391 | my $self=shift; 392 | my($num,$pass)=@_; 393 | 394 | local $_=$self->wpost_x_www( 395 | $self->{script}, 396 | $self->link_page(0), 397 | 398 | $num , 'delete', 399 | mode => 'usrdel', 400 | pwd =>($pass or 'wwwwww'), 401 | ); 402 | 403 | return if $self->error; 404 | $self->error(0),return 0 if //; 405 | $self->error($self->magnitude($1)),return if /]*>(?:Error:)?(.*?)

/; 406 | die "Unknown error when deleting post:-------$_--------" 407 | } 408 | 409 | sub _clean_simple($$){ 410 | my($self)=shift; 411 | my($val)=@_; 412 | return $self->SUPER::do_clean($val); 413 | } 414 | 415 | sub do_clean($$){ 416 | (my($self),local($_))=@_; 417 | 418 | # SOPA spoilers 419 | #s!]*>(.*?)()?!$1!g; 420 | 421 | # Escaping tags we don't want users to use 422 | s!\[(banned|moot)\]![${1}:lit]!g; 423 | 424 | # code tags 425 | s!
]*>![code]!g;
426 | 	s!
![/code]!g; 427 | 428 | # Comment too long. Also, exif tag toggle 429 | s!.*?!!g; 430 | 431 | # (USER WAS BANNED|WARNED FOR THIS POST) 432 | s!<(?:b|strong) style="color:\s*red;">(.*?)![banned]${1}[/banned]!g; 433 | 434 | # moot text 435 | s!
(.*?)
![moot]${1}[/moot]!g; 436 | 437 | # Bold text 438 | s!<(?:b|strong)>(.*?)![b]${1}[/b]!g; 439 | 440 | # Who are you quoting? (we reparse quotes on our side) 441 | s!(.*?)!$1!g; 442 | s!(.*?)!$1!g; 443 | s!(.*?)!$1!g; 444 | 445 | # Get rid of links (we recreate them on our side) 446 | s!]*>(.*?)
!$1!g; 447 | 448 | # Spoilers 449 | s!]*>![spoiler]!g; 450 | s!![/spoiler]!g; 451 | 452 | s!![spoiler]!g; 453 | s!![/spoiler]!g; 454 | 455 | # 456 | s!!!g; 457 | 458 | # Newlines 459 | s!
!\n!gx; 460 | 461 | $self->_clean_simple($_); 462 | } 463 | 464 | 465 | 1; 466 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright 2009, Osenenko Andrey. 2 | 3 | This library is free software; you can redistribute it and/or 4 | modify it under the same terms as Perl itself. 5 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Fuuka 2 | ------- 3 | 4 | Fuuka is a program that that lets you archive 4chan imageboards. 5 | 6 | It's written in Perl. It uses MySQL for its local database, CGI or 7 | mod_perl if you want a web interface and gnuplot to create graphs with 8 | statistics. 9 | 10 | Check the wiki articles at http://code.google.com/p/fuuka/w/list for the system 11 | requirements and advanced tips on how to run it. 12 | 13 | 14 | Written from scratch by Andrey, all credit goes to him. Maintained by Eksopl 15 | from 2009 onwards. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fuuka 2 | 3 | Fuuka is a program that that lets you archive 4chan imageboards. 4 | 5 | It's written in Perl. It uses MySQL for its local database, CGI or mod\_perl if you want a web interface and gnuplot to create graphs with statistics. 6 | 7 | Check the [wiki articles](https://github.com/eksopl/fuuka/wiki/_pages) for the system requirements and advanced tips on how to run it. 8 | 9 | 10 | Written from scratch by Andrey, all credit goes to him. Maintained by Eksopl from 2009-2013. 11 | -------------------------------------------------------------------------------- /board-config.pl: -------------------------------------------------------------------------------- 1 | #!/usr/local/perl 2 | 3 | # 4 | # General 5 | # 6 | 7 | # list of boards you'll be archiving 8 | use constant BOARD_SETTINGS => { 9 | # a => { 10 | # name => "Anime & Manga", # Name, as it will appear on web page 11 | # 12 | # pages => [[[0],15],[[1..15],7200]], 13 | # # Each element of a list is processed in own thread. 14 | # # Each element is a list with two elements 15 | # # first: list of pages to check periodically, e.g. [0,1,2] 16 | # # second: how long to sleep in seconds after checking all of them 17 | # 18 | # "thread-refresh-rate" => 16, # Time, in minutes, for how long to wait before updating thread. 19 | # 20 | # # How many requests should be carried out simultaneously to: 21 | # "new-thread-threads" => 6, # get new threads and refresh old ones 22 | # "thumb-threads" => 6, # get thumbs 23 | # "media-threads" => 0, # get pictures 24 | # 25 | # link => "http://boards.4chan.org/a", 26 | # img_link => "http://i.4cdn.org/a", 27 | # "database" => "Sphinx_Mysql", 28 | # "disable-posting" => 0, 29 | # }, 30 | # jp => { 31 | # name => "Otaku Culture", 32 | # pages => [[[0],30],[[1..15],7200]], 33 | # "thread-refresh-rate" => 12, 34 | # "new-thread-threads" => 3, 35 | # "thumb-threads" => 3, 36 | # "media-threads" => 0, 37 | # 38 | # link => "http://boards.4chan.org/jp", 39 | # img_link => "http://i.4cdn.org/jp", 40 | # "database" => "Mysql", 41 | # }, 42 | # hr => { 43 | # name => "High Resolution", 44 | # pages => [[[0],240],[[0..10],3600]], 45 | # "thread-refresh-rate" => 120, 46 | # "new-thread-threads" => 3, 47 | # "thumb-threads" => 12, 48 | # "media-threads" => 4, 49 | # 50 | # link => "http://boards.4chan.org/hr", 51 | # }, 52 | # b => { 53 | # name => "Random", 54 | # pages => [[[0],0],[[1],0],[[2],0],[[3],0]], 55 | # "thread-refresh-rate" => 120, 56 | # "new-thread-threads" => 10, 57 | # "thumb-threads" => 10, 58 | # 59 | # link => "http://img.4chan.org/b", 60 | # }, 61 | # e => { 62 | # name => "Ecchi", 63 | # pages => [[[0],240],[[0..10],3600]], 64 | # "thread-refresh-rate" => 120, 65 | # "new-thread-threads" => 3, 66 | # "thumb-threads" => 12, 67 | # "media-threads" => 4, 68 | 69 | # link => "http://boards.4chan.org/e", 70 | # }, 71 | 72 | 73 | }; 74 | 75 | # where to put images and thumbs from archived boards 76 | use constant IMAGES_LOCATION => "f:/board"; 77 | use constant IMAGES_LOCATION_HTTP => "/board"; 78 | 79 | # where your files with reports located 80 | use constant REPORTS_LOCATION => "b:/server-data/board/reports"; 81 | 82 | # where all web files (pictures, js, css, etc.) are located 83 | use constant MEDIA_LOCATION_HTTP => "/media"; 84 | 85 | # how to run the program for plotting 86 | use constant GNUPLOT => 'wgnuplot'; 87 | 88 | # path to script, relative to HTTP root. Use together with mod_rewrite rules 89 | use constant LOCATION_HTTP => $ENV{SCRIPT_NAME}; 90 | 91 | # terminal type for gnuplot. If you have gnuplot 4.4+ compiled with cairo 92 | # support, switch this to pngcairo for prettier graphs. 93 | use constant GNUPLOT_TERMINAL => 'png'; 94 | 95 | # 96 | # Database 97 | # 98 | 99 | # default db engine 100 | use constant DEFAULT_ENGINE => "Sphinx_Mysql"; 101 | 102 | # it's ok to leave this empty 103 | use constant DB_CONNECTION_STRING => ""; 104 | 105 | # these will be used to construct connection string if you leave it empty, 106 | # and will be ignored if you provide connection string) 107 | use constant DB_HOST => "localhost"; 108 | use constant DB_DATABSE_NAME => "Yotsuba"; 109 | 110 | use constant DB_USERNAME => "root"; 111 | use constant DB_PASSWORD => "qwerty"; 112 | 113 | # The charset to use for all text fields in the database. Leave it as utf8mb4 114 | # if you are running MySQL 5.5 or above. If you're running a lower version, 115 | # you will have to set it to utf8, but you will not be able to archive some 116 | # (very rare) non-BMP Unicode characters, so consider upgrading. 117 | # Setting it to anything other than utf8mb4 or utf8 will break things in 118 | # very unexpected ways, so don't do it. 119 | use constant DB_CHARSET => "utf8mb4"; 120 | 121 | 122 | # Fill this to use Sphinx for searching. 123 | # You must have Sphinx configured with MySQL protocol support (mysql41 listener) 124 | # Do not use 'localhost'; use an IP address (or any other FQDN, really). 125 | # Otherwise it will not work, as the MySQL drivers will attempt to use the 126 | # default Unix domain MySQL socket if you input 'localhost'. 127 | use constant SPHINX_HOST => "127.0.0.1"; 128 | use constant SPHINX_PORT => 9306; 129 | 130 | # 131 | # Posting 132 | # 133 | 134 | # Password to actually delete files and not just put a a trash bin icon next to them. 135 | use constant DELPASS => 'TOPSECRET'; 136 | 137 | # Password to delete images 138 | use constant IMGDELPASS => 'TOPSECRET2'; 139 | 140 | # Cryptographic secret encoded in base 64, used for secure tripcodes. 141 | # Default is world4chan's (dis.4chan.org) former secret. 142 | use constant SECRET => ' 143 | FW6I5Es311r2JV6EJSnrR2+hw37jIfGI0FB0XU5+9lua9iCCrwgkZDVRZ+1PuClqC+78FiA6hhhX 144 | U1oq6OyFx/MWYx6tKsYeSA8cAs969NNMQ98SzdLFD7ZifHFreNdrfub3xNQBU21rknftdESFRTUr 145 | 44nqCZ0wyzVVDySGUZkbtyHhnj+cknbZqDu/wjhX/HjSitRbtotpozhF4C9F+MoQCr3LgKg+CiYH 146 | s3Phd3xk6UC2BG2EU83PignJMOCfxzA02gpVHuwy3sx7hX4yvOYBvo0kCsk7B5DURBaNWH0srWz4 147 | MpXRcDletGGCeKOz9Hn1WXJu78ZdxC58VDl20UIT9er5QLnWiF1giIGQXQMqBB+Rd48/suEWAOH2 148 | H9WYimTJWTrK397HMWepK6LJaUB5GdIk56ZAULjgZB29qx8Cl+1K0JWQ0SI5LrdjgyZZUTX8LB/6 149 | Coix9e6+3c05Pk6Bi1GWsMWcJUf7rL9tpsxROtq0AAQBPQ0rTlstFEziwm3vRaTZvPRboQfREta0 150 | 9VA+tRiWfN3XP+1bbMS9exKacGLMxR/bmO5A57AgQF+bPjhif5M/OOJ6J/76q0JDHA=='; 151 | 152 | # Maximum number of characters in subject, name, and email 153 | use constant MAX_FIELD_LENGTH => 100; 154 | 155 | # Maximum number of characters in a comment 156 | use constant MAX_COMMENT_LENGTH => 4096; 157 | 158 | # Maximum number of lines in a comment 159 | use constant MAX_COMMENT_LINES => 40; 160 | 161 | 162 | # Seconds between posts (floodcheck) 163 | use constant RENZOKU => 5; 164 | 165 | # Seconds between identical posts (floodcheck) 166 | use constant RENZOKU3 => 900; 167 | 168 | # Set to 1 to enable the sage feature in ghost posts 169 | use constant ENABLE_SAGE => 0; 170 | 171 | # Set to the group your webserver perl processes run as, so image/thumbs stored 172 | # locally can be deleted with the thumbnail deletion password. 173 | # You will need to add the user that is running the dumper daemon as a 174 | # member of said group. 175 | # 176 | # Leave blank to disable changing the group on downloaded images. 177 | # 178 | # See ThumbnailDeletionSupport on the wiki for more info. 179 | use constant WEBSERVER_GROUP => 'www'; 180 | 181 | # 182 | # that's it folks, move along, nothing to see here. 183 | # I am putting code into config file 184 | # 185 | 186 | use constant SPAWNER => sub{my $board_name=shift; 187 | my $board_engine = "Board::".(BOARD_SETTINGS->{$board_name}->{"database"} or DEFAULT_ENGINE); 188 | $board_engine->new($board_name, 189 | connstr => DB_CONNECTION_STRING, 190 | host => DB_HOST, 191 | database => DB_DATABSE_NAME, 192 | name => DB_USERNAME, 193 | password => DB_PASSWORD, 194 | charset => DB_CHARSET, 195 | images => IMAGES_LOCATION, 196 | create => 1, 197 | full_pictures => BOARD_SETTINGS->{$board_name}->{"media-threads"}?1:0, 198 | web_group => WEBSERVER_GROUP, 199 | ) or die "Couldn't use mysql board with table $board_name"}; 200 | 201 | sub yotsutime(){ 202 | use DateTime; 203 | use DateTime::TimeZone; 204 | time+DateTime::TimeZone->new(name => 'America/New_York')->offset_for_datetime(DateTime->now()) 205 | } 206 | 207 | 1; 208 | -------------------------------------------------------------------------------- /board-dump.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings; 5 | use threads; 6 | use threads::shared 1.21; 7 | 8 | use Carp qw/confess/; 9 | use Data::Dumper; 10 | 11 | use Board::Request; 12 | use Board::Errors; 13 | use Board::Yotsuba; 14 | use Board::Mysql; 15 | use Board::Sphinx_Mysql; 16 | $|++; 17 | 18 | BEGIN{-e "board-config-local.pl" ? 19 | require "board-config-local.pl" : require "board-config.pl"} 20 | my $board_name=shift or usage(); 21 | my $bind_ip=shift; 22 | (my $settings=BOARD_SETTINGS->{$board_name}) 23 | or die "Can't archive $board_name until you add it to board-config.pl"; 24 | 25 | my $board_spawner=sub{Board::Yotsuba->new($board_name,timeout=>12,ipaddr=>$bind_ip) 26 | or die "No such board: $board_name"}; 27 | 28 | sub usage{ 29 | print < 1; 49 | use constant WARN => 2; 50 | use constant TALK => 3; 51 | 52 | use constant PAGELIMBO => 8; 53 | 54 | sub debug($@){ 55 | my $level=shift; 56 | print "[", 57 | scalar keys %threads," ", 58 | scalar @newthreads," ", 59 | scalar @thread_updates," ", 60 | scalar @media_updates," ", 61 | scalar @media_preview_updates," ", 62 | "] ",@_,"\n" 63 | if $level-1<$debug_level; 64 | } 65 | 66 | # Receives a thread reference and a post num. 67 | # The post corresponding to that num if it exists in said thread. 68 | sub find_post($$){ 69 | my($thread, $num) = @_; 70 | for(@{$thread->{allposts}}){ 71 | return $_ if $_==$num; 72 | } 73 | } 74 | 75 | # Gets two threads, the old thread and the new one we just got. 76 | # Returns: 1 if there's been a change in the deletion status of any post in the new thread 77 | # 0 otherwise 78 | # If the third argument is false, it won't actually mark anything as deleted. 79 | sub find_deleted($$$){ 80 | my($old, $new, $mark) = @_; 81 | my $changed = 0; 82 | 83 | # Return if the old thread has no posts 84 | return 0 unless $old and $old->{posts}; 85 | 86 | my(@posts) = @{$old->{allposts}}; 87 | 88 | return unless @posts; 89 | 90 | foreach(@posts[0, $new->{omposts}+1..$#posts]) { 91 | my $post = $_; 92 | if(not find_post $new, $post) { 93 | return 1 if not $mark; 94 | 95 | $changed = 1; 96 | 97 | foreach(@{$old->{allposts}}) { 98 | $_ = undef if $_ == $post; 99 | } 100 | 101 | push @deleted_posts, $post; 102 | debug TALK,"$post (post): deleted"; 103 | } 104 | } 105 | 106 | $changed 107 | } 108 | 109 | # Thumb Fetcher 110 | # fetch thumbs 111 | async{my $board=$board_spawner->();my $local_board=SPAWNER->($board_name);while(1) { 112 | my $ref; 113 | { 114 | lock @media_preview_updates; 115 | $ref=shift @media_preview_updates; 116 | } 117 | 118 | sleep 1 and next unless $ref; 119 | 120 | $local_board->insert_media_preview($ref, $board); 121 | 122 | debug ERROR,"Couldn't insert posts into $local_board: ".$board->errstr 123 | and next if $local_board->error; 124 | }} foreach 1..$settings->{"thumb-threads"}; 125 | 126 | # Media Fetcher 127 | # fetch pics 128 | async{my $board=$board_spawner->();my $local_board=SPAWNER->($board_name);while(1) { 129 | my $ref; 130 | { 131 | lock @media_updates; 132 | $ref = shift @media_updates; 133 | } 134 | 135 | sleep 1 and next unless $ref; 136 | 137 | $local_board->insert_media($ref, $board); 138 | 139 | debug ERROR,"Couldn't insert posts into $local_board: ".$board->errstr 140 | and next if $local_board->error; 141 | }} foreach 1..$settings->{"media-threads"}; 142 | 143 | # Thread Inserter 144 | # insert updates into database 145 | async{my $local_board=SPAWNER->($board_name);while(1){ 146 | while(my $thread = pop @thread_updates) { 147 | { 148 | lock($thread); 149 | next if not $thread->{ref}; 150 | 151 | $local_board->insert($thread->{ref}); 152 | 153 | debug ERROR,"Couldn't insert posts into database: ".$local_board->errstr 154 | if $local_board->error; 155 | 156 | foreach(@{$thread->{ref}->{posts}}) { 157 | my $mediapost; 158 | if($_->{preview} or $_->{media_filename}) { 159 | $mediapost = Board->new_media_post( 160 | num => $_->{num}, 161 | parent => $_->{parent}, 162 | preview => $_->{preview}, 163 | media_filename => $_->{media_filename}, 164 | media_hash => $_->{media_hash} 165 | ); 166 | } 167 | push @media_preview_updates, shared_clone($mediapost) 168 | if $_->{preview} and $settings->{"thumb-threads"}; 169 | push @media_updates, shared_clone($mediapost) 170 | if $_->{media_filename} and $settings->{"media-threads"}; 171 | } 172 | 173 | delete $thread->{ref}->{posts}; 174 | $thread->{ref}->{posts} = shared_clone([]); 175 | } 176 | } 177 | sleep 1; 178 | }}; 179 | 180 | # Post Deleter 181 | # mark posts as deleted 182 | async{my $local_board=SPAWNER->($board_name);while(1){ 183 | my $ref; 184 | { 185 | lock @deleted_posts; 186 | $ref = shift @deleted_posts; 187 | } 188 | 189 | sleep 1 and next unless $ref; 190 | 191 | $local_board->mark_deleted($ref); 192 | 193 | debug ERROR,"Couldn't update deleted status of post $ref: ".$local_board->errstr 194 | if $local_board->error; 195 | sleep 5; 196 | }}; 197 | 198 | # Page Scanner 199 | # Scan pages 200 | async { 201 | my $board = $board_spawner->(); 202 | 203 | # $pagenos is a list with the page numbers 204 | # $wait is the refresh time for those pages 205 | # %lastmods contains last modification dates for each page 206 | my($pagenos, $wait) = @$_; 207 | my %lastmods; 208 | while(1) { 209 | my $now = time; 210 | 211 | # Scan through pages with the same wait period 212 | foreach my $pageno(@$pagenos){ 213 | # If there's a lastmod in the lastmods hash, we pass it to content()... 214 | my $lastmod = defined $lastmods{$pageno} ? $lastmods{$pageno} : undef; 215 | my $starttime = time; 216 | my $list = $board->content(PAGE($pageno, $lastmod)); 217 | 218 | # Just move on if it hasn't been modified 219 | if($board->error and $board->errstr eq 'Not Modified') { 220 | debug TALK, ($pageno == 0 ? "front page" : "page $pageno") 221 | . ": wasn't modified"; 222 | next; 223 | } 224 | sleep 1 and print $board->errstr,"\n" and next if $board->error; 225 | 226 | # ...and then we store the lastmod date for the page we just got 227 | delete $lastmods{$pageno}; 228 | $lastmods{$pageno} = $list->{lastmod}; 229 | 230 | # Scan through threads on that page 231 | for(@{$list->{threads}}) { 232 | my $nthread = $_; 233 | my $num = $_->{num}; 234 | 235 | # Push thread into new threads queue and skips 236 | # if we haven't seen it before 237 | push @newthreads, $num and next unless $threads{$num}; 238 | 239 | # Otherwise we get the thread we had already 240 | # previously seen 241 | my $thread = ${$threads{$num}}; 242 | lock $thread; 243 | next unless defined $threads{$num}; 244 | 245 | my(@posts) = @{$nthread->{posts}}; 246 | 247 | next if $thread->{lasthit} > $starttime; 248 | 249 | delete $thread->{lastpage}; 250 | $thread->{lastpage} = $pageno; 251 | 252 | my($old, $new, $must_refresh) = (0, 0, 0); 253 | 254 | # We check for any posts that got deleted. 255 | if(find_deleted($thread->{ref}, $nthread, 0)) { 256 | $must_refresh = 1; 257 | ++$new; 258 | } 259 | 260 | for(@posts){ 261 | # This post was already in topics map. Next post 262 | ++$old and next if find_post($thread->{ref}, $_->{num}); 263 | 264 | # Looks like it's new 265 | ++$new; 266 | 267 | # If it's new, deep copies the post into our thread hash 268 | push @{$thread->{ref}->{posts}}, shared_clone($_); 269 | push @{$thread->{ref}->{allposts}}, $_->{num}; 270 | 271 | # Comment too long. Click here to view the full text. 272 | # This means we have to refresh the full thread 273 | $must_refresh=1 if $_->{omitted}; 274 | } 275 | 276 | # Update the time we last hit this thread 277 | $thread->{lasthit}=time; 278 | 279 | # No new posts 280 | next if $old!=0 and $new==0; 281 | 282 | debug TALK, "$num: " . ($pageno == 0 ? "front page" : "page $pageno") 283 | . " update"; 284 | 285 | # Push new posts/images/thumbs to their queues 286 | push @thread_updates, $thread; 287 | 288 | # And send the thread to the new threads queue if we were 289 | # forced to refresh earlier or if the only old post we 290 | # saw was the OP, as that means we're missing posts from inside the thread 291 | if($must_refresh or $old < 2) { 292 | debug TALK, "$num: must refresh"; 293 | push @newthreads, $num ; 294 | } 295 | } 296 | } 297 | my $left = $wait - (time - $now); 298 | sleep $left if $left > 0; 299 | } 300 | } foreach @{$settings->{pages}}; 301 | 302 | # Topic Fetcher 303 | # Rebuild whole thread, either because it's new or because it's too old 304 | async{my $board=$board_spawner->();while(1){ 305 | use threads; 306 | use threads::shared; 307 | 308 | local $_; 309 | { 310 | lock @newthreads; 311 | $_ = shift @newthreads; 312 | } 313 | 314 | my $num = $_; 315 | 316 | if(not $num or $num !~ /^\d+$/) { 317 | sleep 1; 318 | next; 319 | } 320 | 321 | { 322 | my $oldthread = defined $threads{$num} ? ${$threads{$num}} : undef; 323 | lock($oldthread) if defined $oldthread; 324 | next if defined $oldthread and not defined $threads{$num}; 325 | 326 | my $lastmod = defined $oldthread ? $oldthread->{ref}->{lastmod} : undef; 327 | my $starttime=time; 328 | my $thread = $board->content(THREAD($num, $lastmod)); 329 | 330 | if($board->error){ 331 | if($board->errstr eq 'Not Modified') { 332 | debug TALK,"$num: wasn't modified"; 333 | if(defined $oldthread) { 334 | delete $oldthread->{lasthit}; 335 | delete $oldthread->{busy}; 336 | $oldthread->{lasthit} = time; 337 | } 338 | } elsif($board->errstr eq 'Not Found' and defined $oldthread) { 339 | if($oldthread->{lastpage} < PAGELIMBO) { 340 | push @deleted_posts, $num; 341 | debug TALK, "$num: deleted (last seen on page " 342 | . $oldthread->{lastpage} . ")"; 343 | } 344 | delete $oldthread->{ref}; 345 | delete $threads{$num}; 346 | } else { 347 | debug ERROR, "$num: error: ". $board->errstr; 348 | delete $oldthread->{busy}; 349 | } 350 | next; 351 | } 352 | 353 | next if not $thread->{num}; 354 | 355 | my $lastpage = 0; 356 | if(defined $oldthread) { 357 | next if $oldthread->{lasthit} > $starttime; 358 | find_deleted $oldthread->{ref}, $thread, 1; 359 | $lastpage = $oldthread->{lastpage}; 360 | 361 | delete $oldthread->{ref}; 362 | delete $oldthread->{lasthit}; 363 | delete $oldthread->{busy}; 364 | $oldthread->{ref} = shared_clone($thread); 365 | $oldthread->{lasthit} = $starttime; 366 | } else { 367 | my $newthread :shared = shared_clone({ 368 | num => $num, 369 | lasthit => $starttime, 370 | ref => $thread, 371 | lastpage => 0 372 | }); 373 | $threads{$num} = \$newthread; 374 | } 375 | 376 | push @thread_updates, ${$threads{$num}}; 377 | debug TALK, "$num: " . (${$threads{$num}} ? "updated" : "new"); 378 | } 379 | }} foreach 1..$settings->{"new-thread-threads"}; 380 | 381 | # Topic Rebuilder 382 | # check for old threads to rebuild 383 | while(1) { 384 | for(keys %threads) { 385 | my $num = $_; 386 | next unless $num and defined $threads{$num}; 387 | my $thread = ${$threads{$num}}; 388 | lock($thread); 389 | next unless defined $threads{$num}; 390 | 391 | next if $thread->{busy}; 392 | 393 | my $lasthit = time - $thread->{lasthit}; 394 | 395 | next unless $lasthit > $settings->{"thread-refresh-rate"}*60; 396 | 397 | $thread->{busy} = 1; 398 | push @newthreads, $num; 399 | } 400 | 401 | exit if $panic; 402 | sleep 1; 403 | } 404 | 405 | 406 | # vim: set ts=4 sw=4 noexpandtab: 407 | 408 | -------------------------------------------------------------------------------- /board-reports-demon.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings; 5 | use utf8; 6 | 7 | use 5.010; 8 | 9 | sub info(@); 10 | 11 | BEGIN{-e "board-config-local.pl" ? 12 | require "board-config-local.pl" : require "board-config.pl"} 13 | 14 | use Board::Mysql; 15 | use Board::Sphinx_Mysql; 16 | 17 | binmode *STDOUT,":utf8"; 18 | 19 | use Time::HiRes qw/gettimeofday usleep/; 20 | 21 | sub process(); 22 | sub usage(); 23 | sub reload_reports(); 24 | 25 | my $settings=BOARD_SETTINGS; 26 | my $board_names=[keys %$settings]; 27 | my @boards=map{SPAWNER->($_)} @$board_names; 28 | my $loc=REPORTS_LOCATION; 29 | my $imgloc=IMAGES_LOCATION; 30 | my $term=GNUPLOT_TERMINAL; 31 | my @reports; 32 | my @report_files; 33 | my %mtimes; 34 | 35 | mkdir "$imgloc"; 36 | mkdir "$imgloc/graphs"; 37 | mkdir "$imgloc/graphs/$_" foreach @$board_names; 38 | mkdir "$loc"; 39 | mkdir "$loc/status"; 40 | mkdir "$loc/status/$_" foreach @$board_names; 41 | 42 | sub uncrlf($){ 43 | $_[0]=~s/\r?\n?\r?$//; 44 | 45 | $_[0] 46 | } 47 | 48 | sub mtime($){ 49 | my($filename)=@_; 50 | 51 | my(@stat)=stat $filename or return 0; 52 | 53 | $stat[9] 54 | } 55 | 56 | sub reload_reports(){ 57 | @reports=(); 58 | @report_files=(); 59 | push @report_files,$loc; 60 | $mtimes{$loc}=mtime $loc; 61 | 62 | opendir DIRHANDLE,$loc or die "$! - $loc"; 63 | MAINLOOP: 64 | while($_=readdir DIRHANDLE){ 65 | next if $_ =~ /^\./; 66 | my $filename="$loc/$_"; 67 | 68 | next unless -f $filename; 69 | my %opts; 70 | 71 | push @report_files,$filename; 72 | $mtimes{$filename}=mtime $filename; 73 | 74 | open HANDLE,$filename or die "$! - $filename"; 75 | for(){ 76 | uncrlf($_); 77 | 78 | /([\w\d\-]*)\s*:\s*(.*)/ or warn "$filename: wrong report file format: $_" and next; 79 | 80 | $opts{lc $1}=$2; 81 | } 82 | close HANDLE; 83 | 84 | warn "$filename: wrong format: must have field $_" and next MAINLOOP 85 | foreach grep{not defined $opts{$_}} "query","mode","refresh-rate"; 86 | 87 | next if not $opts{'refresh-rate'}; 88 | 89 | $opts{filename}="$_"; 90 | 91 | if($opts{mode} eq 'graph'){ 92 | $opts{'result-location'}="$imgloc/graphs"; 93 | $opts{'result'}="$_.png"; 94 | } else{ 95 | $opts{'result-location'}="$loc/status"; 96 | $opts{'result'}=$_; 97 | } 98 | 99 | push @reports,\%opts; 100 | } 101 | 102 | closedir DIRHANDLE; 103 | } 104 | 105 | reload_reports; 106 | 107 | my $lastmtime=0; 108 | while(1){ 109 | for(@report_files){ 110 | reload_reports if mtime $_>$mtimes{$_}; 111 | } 112 | 113 | for my $board(@boards){ 114 | for(@reports){ 115 | my $remake_time=$_->{"refresh-rate"}-(time- mtime "$_->{'result-location'}/$board->{name}/$_->{result}"); 116 | do_report($_,$board) if $remake_time<0; 117 | } 118 | } 119 | 120 | sleep 1; 121 | } 122 | 123 | sub benchmark($){ 124 | my $sub=shift; 125 | my($s,$us)=gettimeofday; 126 | $sub->(); 127 | my($s_new,$us_new)=gettimeofday; 128 | ($s_new-$s)+($us_new-$us)/1_000_000; 129 | } 130 | 131 | sub do_report{ 132 | my($ref,$board)=@_; 133 | my $name=$ref->{filename}; 134 | 135 | my $list; 136 | info sprintf "$name, $board->{name}: %.3f seconds taken",benchmark sub{ 137 | my $query=$ref->{query}; 138 | $query=~s/%%BOARD%%/$board->{name}/g; 139 | $query=~s/%%NOW%%/yotsutime/ge; 140 | $list=$board->query($query); 141 | }; 142 | 143 | die $board->errstr if $board->error; 144 | 145 | if($ref->{mode} eq 'graph' and GNUPLOT){ 146 | my $xstart = $list->[0][0]; 147 | my $xend = $list->[-1][0]; 148 | open HANDLE,">$loc/graphs/$name.data" or die "$! - $loc/graphs/$name.data"; 149 | print HANDLE "0\t0\t0\t0\n" if !defined $list->[0][0]; 150 | print HANDLE (join "\t",@$_),"\n" foreach @$list; 151 | close HANDLE; 152 | 153 | open INFILE,"$loc/graphs/$name.graph" or die "$! - $loc/graphs/$name.graph"; 154 | open OUTFILE,">$loc/graphs/$name.graph+" or die "$! - $loc/graphs/$name.graph+"; 155 | 156 | while(defined(local $_=)){ 157 | s!%%INFILE%%!$loc/graphs/$name.data!g; 158 | s!%%OUTFILE%%!$ref->{'result-location'}/$board->{name}/$ref->{result}!g; 159 | s!%%XSTART%%!$xstart!g; 160 | s!%%XEND%%!$xend!g; 161 | s!%%TERM%%!$term!g; 162 | print OUTFILE $_; 163 | } 164 | 165 | close OUTFILE; 166 | close INFILE; 167 | 168 | system(GNUPLOT,"$loc/graphs/$name.graph+"); 169 | 170 | # unlink "$loc/graphs/$name.graph+","$loc/graphs/$name.data"; 171 | } else { 172 | open HANDLE,">$ref->{'result-location'}/$board->{name}/$ref->{result}" or die "$! - $ref->{'result-location'}/$board->{name}/$ref->{result}"; 173 | binmode HANDLE,":utf8"; 174 | print HANDLE time,"\n"; 175 | for(@$list){ 176 | print HANDLE (join "|",map{$_ //= '';s !\|!\\v!g;$_}@$_),"\n"; 177 | } 178 | close HANDLE; 179 | } 180 | } 181 | 182 | 183 | sub info(@){ 184 | print "".localtime()." ",@_,"\n"; 185 | } 186 | 187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /examples/.htaccess: -------------------------------------------------------------------------------- 1 | Order Deny,Allow 2 | Deny from All 3 | -------------------------------------------------------------------------------- /examples/screen-archive.example: -------------------------------------------------------------------------------- 1 | screen -t "Board reports demon" sh -c "cd /usr/local/www/data/; while true; do perl ./board-reports-demon.pl; done" 2 | screen -t "/a/ archiver" sh -c "cd /usr/local/www/data/; while true; do perl ./board-dump.pl a; done" 3 | screen -t "/jp/ archiver" sh -c "cd /usr/local/www/data/; while true; do perl ./board-dump.pl jp; done" 4 | screen -t "/m/ archiver" sh -c "cd /usr/local/www/data/; while true; do perl ./board-dump.pl m; done" 5 | screen -t "/tg/ archiver" sh -c "cd /usr/local/www/data/; while true; do perl ./board-dump.pl tg; done" 6 | select 0 7 | -------------------------------------------------------------------------------- /examples/sphinx.conf.example: -------------------------------------------------------------------------------- 1 | # 2 | # Sphinx configuration file for fuuka 3 | # 4 | 5 | ############################################################################# 6 | ## data source definition 7 | ############################################################################# 8 | 9 | # Source template 10 | source main 11 | { 12 | type = mysql 13 | 14 | sql_host = localhost 15 | sql_user = root 16 | sql_pass = qwerty 17 | sql_db = Yotsuba 18 | # sql_sock = /tmp/mysql.sock 19 | 20 | sql_query_pre = SET NAMES utf8mb4 21 | # sql_query_pre = SET SESSION query_cache_type=OFF 22 | 23 | #sql_query_range = SELECT MIN(doc_id), MAX(doc_id) FROM a 24 | sql_range_step = 10000 25 | sql_query = \ 26 | SELECT doc_id, num, subnum, name, trip, email, media, (CASE parent WHEN 0 THEN num ELSE parent END) AS tnum, \ 27 | ASCII(capcode) AS cap, (media != '' AND media IS NOT NULL) AS has_image, (subnum != 0) AS is_internal, \ 28 | spoiler AS is_spoiler, deleted AS is_deleted, sticky as is_sticky, (parent = 0) AS is_op, timestamp, title, comment \ 29 | FROM a LIMIT 1 30 | 31 | sql_attr_uint = num 32 | sql_attr_uint = subnum 33 | sql_attr_uint = tnum 34 | sql_attr_uint = cap 35 | sql_attr_bool = has_image 36 | sql_attr_bool = is_internal 37 | sql_attr_bool = is_spoiler 38 | sql_attr_bool = is_deleted 39 | sql_attr_bool = is_sticky 40 | sql_attr_bool = is_op 41 | sql_attr_timestamp = timestamp 42 | 43 | sql_query_post_index = 44 | sql_query_info = 45 | } 46 | 47 | 48 | # /a/ 49 | source a_main : main 50 | { 51 | sql_query = \ 52 | SELECT doc_id, num, subnum, name, trip, email, media, (CASE parent WHEN 0 THEN num ELSE parent END) AS tnum, \ 53 | ASCII(capcode) AS cap, (media != '' AND media IS NOT NULL) AS has_image, (subnum != 0) AS is_internal, \ 54 | spoiler AS is_spoiler, deleted AS is_deleted, sticky as is_sticky, (parent = 0) AS is_op, timestamp, title, comment \ 55 | FROM a WHERE doc_id >= $start AND doc_id <= $end 56 | sql_query_info = SELECT * FROM a WHERE doc_id=$id 57 | 58 | sql_query_range = SELECT (SELECT val FROM index_counters WHERE id = 'max_ancient_id_a'), (SELECT MAX(doc_id) FROM a) 59 | sql_query_post_index = REPLACE INTO index_counters (id, val) \ 60 | VALUES ('max_indexed_id_a', $maxid) 61 | } 62 | 63 | source a_ancient : a_main 64 | { 65 | sql_query_range = SELECT MIN(doc_id), MAX(doc_id) FROM a 66 | sql_query_post_index = REPLACE INTO index_counters (id, val) \ 67 | VALUES ('max_ancient_id_a', $maxid) 68 | } 69 | 70 | source a_delta : a_main 71 | { 72 | sql_query_range = SELECT (SELECT val FROM index_counters WHERE id = 'max_indexed_id_a'), (SELECT MAX(doc_id) FROM a) 73 | sql_query_post_index = 74 | } 75 | 76 | 77 | # /jp/ 78 | source jp_main : main 79 | { 80 | sql_query = \ 81 | SELECT doc_id, num, subnum, name, trip, email, media, (CASE parent WHEN 0 THEN num ELSE parent END) AS tnum, \ 82 | ASCII(capcode) AS cap, (media != '' AND media IS NOT NULL) AS has_image, (subnum != 0) AS is_internal, \ 83 | spoiler AS is_spoiler, deleted AS is_deleted, sticky as is_sticky, (parent = 0) AS is_op, timestamp, title, comment \ 84 | FROM jp WHERE doc_id >= $start AND doc_id <= $end 85 | sql_query_info = SELECT * FROM jp WHERE doc_id=$id 86 | 87 | sql_query_range = SELECT (SELECT val FROM index_counters WHERE id = 'max_ancient_id_jp'), (SELECT MAX(doc_id) FROM jp) 88 | sql_query_post_index = REPLACE INTO index_counters (id, val) \ 89 | VALUES ('max_indexed_id_jp', $maxid) 90 | } 91 | 92 | source jp_ancient : jp_main 93 | { 94 | sql_query_range = SELECT MIN(doc_id), MAX(doc_id) FROM jp 95 | sql_query_post_index = REPLACE INTO index_counters (id, val) \ 96 | VALUES ('max_ancient_id_jp', $maxid) 97 | } 98 | 99 | source jp_delta : jp_main 100 | { 101 | sql_query_range = SELECT (SELECT val FROM index_counters WHERE id = 'max_indexed_id_jp'), (SELECT MAX(doc_id) FROM jp) 102 | sql_query_post_index = 103 | } 104 | 105 | 106 | # /m/ 107 | source m_main : main 108 | { 109 | sql_query = \ 110 | SELECT doc_id, num, subnum, name, trip, email, media, (CASE parent WHEN 0 THEN num ELSE parent END) AS tnum, \ 111 | ASCII(capcode) AS cap, (media != '' AND media IS NOT NULL) AS has_image, (subnum != 0) AS is_internal, \ 112 | spoiler AS is_spoiler, deleted AS is_deleted, sticky as is_sticky, (parent = 0) AS is_op, timestamp, title, comment \ 113 | FROM m WHERE doc_id >= $start AND doc_id <= $end 114 | sql_query_info = SELECT * FROM m WHERE doc_id=$id 115 | 116 | sql_query_range = SELECT (SELECT val FROM index_counters WHERE id = 'max_ancient_id_m'), (SELECT MAX(doc_id) FROM m) 117 | sql_query_post_index = REPLACE INTO index_counters (id, val) \ 118 | VALUES ('max_indexed_id_m', $maxid) 119 | } 120 | 121 | source m_ancient : m_main 122 | { 123 | sql_query_range = SELECT MIN(doc_id), MAX(doc_id) FROM m 124 | sql_query_post_index = REPLACE INTO index_counters (id, val) \ 125 | VALUES ('max_ancient_id_m', $maxid) 126 | } 127 | 128 | source m_delta : m_main 129 | { 130 | sql_query_range = SELECT (SELECT val FROM index_counters WHERE id = 'max_indexed_id_m'), (SELECT MAX(doc_id) FROM m) 131 | sql_query_post_index = 132 | } 133 | 134 | 135 | # /tg/ 136 | source tg_main : main 137 | { 138 | sql_query = \ 139 | SELECT doc_id, num, subnum, name, trip, email, media, (CASE parent WHEN 0 THEN num ELSE parent END) AS tnum, \ 140 | ASCII(capcode) AS cap, (media != '' AND media IS NOT NULL) AS has_image, (subnum != 0) AS is_internal, \ 141 | spoiler AS is_spoiler, deleted AS is_deleted, sticky as is_sticky, (parent = 0) AS is_op, timestamp, title, comment \ 142 | FROM tg WHERE doc_id >= $start AND doc_id <= $end 143 | sql_query_info = SELECT * FROM tg WHERE doc_id=$id 144 | 145 | sql_query_range = SELECT (SELECT val FROM index_counters WHERE id = 'max_ancient_id_tg'), (SELECT MAX(doc_id) FROM tg) 146 | sql_query_post_index = REPLACE INTO index_counters (id, val) \ 147 | VALUES ('max_indexed_id_tg', $maxid) 148 | } 149 | 150 | source tg_ancient : tg_main 151 | { 152 | sql_query_range = SELECT MIN(doc_id), MAX(doc_id) FROM tg 153 | sql_query_post_index = REPLACE INTO index_counters (id, val) \ 154 | VALUES ('max_ancient_id_tg', $maxid) 155 | } 156 | 157 | source tg_delta : tg_main 158 | { 159 | sql_query_range = SELECT (SELECT val FROM index_counters WHERE id = 'max_indexed_id_tg'), (SELECT MAX(doc_id) FROM tg) 160 | sql_query_post_index = 161 | } 162 | 163 | 164 | ############################################################################# 165 | ## index definition 166 | ############################################################################# 167 | 168 | index main 169 | { 170 | # type = plain 171 | source = main 172 | path = /var/db/sphinxsearch/data/main 173 | docinfo = extern 174 | mlock = 0 175 | 176 | morphology = none 177 | # min_stemming_len = 1 178 | # 179 | # stopwords = /var/db/sphinxsearch/data/stopwords.txt 180 | # wordforms = /var/db/sphinxsearch/data/wordforms.txt 181 | # exceptions = /var/db/sphinxsearch/data/exceptions.txt 182 | 183 | min_word_len = 2 184 | charset_type = utf-8 185 | 186 | # optional, default value depends on charset_type 187 | # 188 | # defaults are configured to include English and Russian characters only 189 | # you need to change the table to include additional ones 190 | # this behavior MAY change in future versions 191 | # 192 | # 'sbcs' default value is 193 | # charset_table = 0..9, A..Z->a..z, _, a..z, U+A8->U+B8, U+B8, U+C0..U+DF->U+E0..U+FF, U+E0..U+FF 194 | # 195 | # 'utf-8' default value is 196 | # charset_table = 0..9, A..Z->a..z, _, a..z, U+410..U+42F->U+430..U+44F, U+430..U+44F 197 | charset_table=0..9, A..Z->a..z, _, a..z, _, \ 198 | U+410..U+42F->U+430..U+44F, U+430..U+44F, \ 199 | U+C0->a, U+C1->a, U+C2->a, U+C3->a, U+C7->c, U+C8->e, U+C9->e, U+CA->e, U+CB->e, U+CC->i, U+CD->i, \ 200 | U+CE->i, U+CF->i, U+D2->o, U+D3->o, U+D4->o, U+D5->o, U+D9->u, U+DA->u, U+DB->u, U+E0->a, U+E1->a, \ 201 | U+E2->a, U+E3->a, U+E7->c, U+E8->e, U+E9->e, U+EA->e, U+EB->e, U+EC->i, U+ED->i, U+EE->i, U+EF->i, \ 202 | U+F2->o, U+F3->o, U+F4->o, U+F5->o, U+F9->u, U+FA->u, U+FB->u, U+FF->y, U+102->a, U+103->a, U+15E->s, \ 203 | U+15F->s, U+162->t, U+163->t, U+178->y, \ 204 | U+FF10..U+FF19->0..9, U+FF21..U+FF3A->a..z, \ 205 | U+FF41..U+FF5A->a..z, U+4E00..U+9FCF, U+3400..U+4DBF, \ 206 | U+20000..U+2A6DF, U+3040..U+309F, U+30A0..U+30FF, U+3000..U+303F, U+3042->U+3041, \ 207 | U+3044->U+3043, U+3046->U+3045, U+3048->U+3047, U+304A->U+3049, \ 208 | U+304C->U+304B, U+304E->U+304D, U+3050->U+304F, U+3052->U+3051, \ 209 | U+3054->U+3053, U+3056->U+3055, U+3058->U+3057, U+305A->U+3059, \ 210 | U+305C->U+305B, U+305E->U+305D, U+3060->U+305F, U+3062->U+3061, \ 211 | U+3064->U+3063, U+3065->U+3063, U+3067->U+3066, U+3069->U+3068, \ 212 | U+3070->U+306F, U+3071->U+306F, U+3073->U+3072, U+3074->U+3072, \ 213 | U+3076->U+3075, U+3077->U+3075, U+3079->U+3078, U+307A->U+3078, \ 214 | U+307C->U+307B, U+307D->U+307B, U+3084->U+3083, U+3086->U+3085, \ 215 | U+3088->U+3087, U+308F->U+308E, U+3094->U+3046, U+3095->U+304B, \ 216 | U+3096->U+3051, U+30A2->U+30A1, U+30A4->U+30A3, U+30A6->U+30A5, \ 217 | U+30A8->U+30A7, U+30AA->U+30A9, U+30AC->U+30AB, U+30AE->U+30AD, \ 218 | U+30B0->U+30AF, U+30B2->U+30B1, U+30B4->U+30B3, U+30B6->U+30B5, \ 219 | U+30B8->U+30B7, U+30BA->U+30B9, U+30BC->U+30BB, U+30BE->U+30BD, \ 220 | U+30C0->U+30BF, U+30C2->U+30C1, U+30C5->U+30C4, U+30C7->U+30C6, \ 221 | U+30C9->U+30C8, U+30D0->U+30CF, U+30D1->U+30CF, U+30D3->U+30D2, \ 222 | U+30D4->U+30D2, U+30D6->U+30D5, U+30D7->U+30D5, U+30D9->U+30D8, \ 223 | U+30DA->U+30D8, U+30DC->U+30DB, U+30DD->U+30DB, U+30E4->U+30E3, \ 224 | U+30E6->U+30E5, U+30E8->U+30E7, U+30EF->U+30EE, U+30F4->U+30A6, \ 225 | U+30AB->U+30F5, U+30B1->U+30F6, U+30F7->U+30EF, U+30F8->U+30F0, \ 226 | U+30F9->U+30F1, U+30FA->U+30F2, U+30AF->U+31F0, U+30B7->U+31F1, \ 227 | U+30B9->U+31F2, U+30C8->U+31F3, U+30CC->U+31F4, U+30CF->U+31F5, \ 228 | U+30D2->U+31F6, U+30D5->U+31F7, U+30D8->U+31F8, U+30DB->U+31F9, \ 229 | U+30E0->U+31FA, U+30E9->U+31FB, U+30EA->U+31FC, U+30EB->U+31FD, \ 230 | U+30EC->U+31FE, U+30ED->U+31FF, U+FF66->U+30F2, U+FF67->U+30A1, \ 231 | U+FF68->U+30A3, U+FF69->U+30A5, U+FF6A->U+30A7, U+FF6B->U+30A9, \ 232 | U+FF6C->U+30E3, U+FF6D->U+30E5, U+FF6E->U+30E7, U+FF6F->U+30C3, \ 233 | U+FF71->U+30A1, U+FF72->U+30A3, U+FF73->U+30A5, U+FF74->U+30A7, \ 234 | U+FF75->U+30A9, U+FF76->U+30AB, U+FF77->U+30AD, U+FF78->U+30AF, \ 235 | U+FF79->U+30B1, U+FF7A->U+30B3, U+FF7B->U+30B5, U+FF7C->U+30B7, \ 236 | U+FF7D->U+30B9, U+FF7E->U+30BB, U+FF7F->U+30BD, U+FF80->U+30BF, \ 237 | U+FF81->U+30C1, U+FF82->U+30C3, U+FF83->U+30C6, U+FF84->U+30C8, \ 238 | U+FF85->U+30CA, U+FF86->U+30CB, U+FF87->U+30CC, U+FF88->U+30CD, \ 239 | U+FF89->U+30CE, U+FF8A->U+30CF, U+FF8B->U+30D2, U+FF8C->U+30D5, \ 240 | U+FF8D->U+30D8, U+FF8E->U+30DB, U+FF8F->U+30DE, U+FF90->U+30DF, \ 241 | U+FF91->U+30E0, U+FF92->U+30E1, U+FF93->U+30E2, U+FF94->U+30E3, \ 242 | U+FF95->U+30E5, U+FF96->U+30E7, U+FF97->U+30E9, U+FF98->U+30EA, \ 243 | U+FF99->U+30EB, U+FF9A->U+30EC, U+FF9B->U+30ED, U+FF9C->U+30EF, \ 244 | U+FF9D->U+30F3 245 | 246 | # ignore_chars = U+00AD 247 | min_prefix_len = 3 248 | # min_infix_len = 0 249 | prefix_fields = comment, title 250 | enable_star = 1 251 | # expand_keywords = 1 252 | html_strip = 0 253 | 254 | # preopen = 1 255 | # ondisk_dict = 1 256 | # inplace_enable = 1 257 | } 258 | 259 | 260 | # /a/ 261 | index a_main : main 262 | { 263 | source = a_main 264 | path = /var/db/sphinxsearch/data/a_main 265 | } 266 | 267 | index a_ancient : a_main 268 | { 269 | source = a_ancient 270 | path = /var/db/sphinxsearch/data/a_ancient 271 | } 272 | 273 | index a_delta : a_main 274 | { 275 | source = a_delta 276 | path = /var/db/sphinxsearch/data/a_delta 277 | } 278 | 279 | 280 | # /jp/ 281 | index jp_main : main 282 | { 283 | source = jp_main 284 | path = /var/db/sphinxsearch/data/jp_main 285 | } 286 | 287 | index jp_ancient : jp_main 288 | { 289 | source = jp_ancient 290 | path = /var/db/sphinxsearch/data/jp_ancient 291 | } 292 | 293 | index jp_delta : jp_main 294 | { 295 | source = jp_delta 296 | path = /var/db/sphinxsearch/data/jp_delta 297 | } 298 | 299 | 300 | # /m/ 301 | index m_main : main 302 | { 303 | source = m_main 304 | path = /var/db/sphinxsearch/data/m_main 305 | } 306 | 307 | index m_ancient : m_main 308 | { 309 | source = m_ancient 310 | path = /var/db/sphinxsearch/data/m_ancient 311 | } 312 | 313 | index m_delta : m_main 314 | { 315 | source = m_delta 316 | path = /var/db/sphinxsearch/data/m_delta 317 | } 318 | 319 | 320 | # /tg/ 321 | index tg_main : main 322 | { 323 | source = tg_main 324 | path = /var/db/sphinxsearch/data/tg_main 325 | } 326 | 327 | index tg_ancient : tg_main 328 | { 329 | source = tg_ancient 330 | path = /var/db/sphinxsearch/data/tg_ancient 331 | } 332 | 333 | index tg_delta : tg_main 334 | { 335 | source = tg_delta 336 | path = /var/db/sphinxsearch/data/tg_delta 337 | } 338 | 339 | ############################################################################# 340 | ## indexer settings 341 | ############################################################################# 342 | 343 | indexer 344 | { 345 | # memory limit, in bytes, kiloytes (16384K) or megabytes (256M) 346 | # optional, default is 32M, max is 2047M, recommended is 256M to 1024M 347 | mem_limit = 512M 348 | 349 | # maximum IO calls per second (for I/O throttling) 350 | # optional, default is 0 (unlimited) 351 | # 352 | # max_iops = 40 353 | 354 | 355 | # maximum IO call size, bytes (for I/O throttling) 356 | # optional, default is 0 (unlimited) 357 | # 358 | # max_iosize = 1048576 359 | 360 | 361 | # maximum xmlpipe2 field length, bytes 362 | # optional, default is 2M 363 | # 364 | # max_xmlpipe2_field = 4M 365 | 366 | 367 | # write buffer size, bytes 368 | # several (currently up to 4) buffers will be allocated 369 | # write buffers are allocated in addition to mem_limit 370 | # optional, default is 1M 371 | # 372 | write_buffer = 5M 373 | 374 | 375 | # maximum file field adaptive buffer size 376 | # optional, default is 8M, minimum is 1M 377 | # 378 | # max_file_field_buffer = 32M 379 | } 380 | 381 | ############################################################################# 382 | ## searchd settings 383 | ############################################################################# 384 | 385 | searchd 386 | { 387 | # [hostname:]port[:protocol], or /unix/socket/path to listen on 388 | # known protocols are 'sphinx' (SphinxAPI) and 'mysql41' (SphinxQL) 389 | # 390 | # multi-value, multiple listen points are allowed 391 | # optional, defaults are 9312:sphinx and 9306:mysql41, as below 392 | # 393 | # listen = 127.0.0.1 394 | # listen = 192.168.0.1:9312 395 | # listen = 9312 396 | # listen = /var/run/searchd.sock 397 | listen = 127.0.0.1:9312 398 | listen = 127.0.0.1:9306:mysql41 399 | 400 | # log file, searchd run info is logged here 401 | # optional, default is 'searchd.log' 402 | log = /var/log/sphinxsearch/searchd.log 403 | 404 | # query log file, all search queries are logged here 405 | # optional, default is empty (do not log queries) 406 | query_log = /var/log/sphinxsearch/sphinx-query.log 407 | 408 | # client read timeout, seconds 409 | # optional, default is 5 410 | read_timeout = 5 411 | 412 | # request timeout, seconds 413 | # optional, default is 5 minutes 414 | client_timeout = 300 415 | 416 | # maximum amount of children to fork (concurrent searches to run) 417 | # optional, default is 0 (unlimited) 418 | max_children = 10 419 | 420 | # PID file, searchd process ID file name 421 | # mandatory 422 | pid_file = /var/run/sphinxsearch/searchd.pid 423 | 424 | # max amount of matches the daemon ever keeps in RAM, per-index 425 | # WARNING, THERE'S ALSO PER-QUERY LIMIT, SEE SetLimits() API CALL 426 | # default is 1000 (just like Google) 427 | max_matches = 5000 428 | 429 | # seamless rotate, prevents rotate stalls if precaching huge datasets 430 | # optional, default is 1 431 | seamless_rotate = 1 432 | 433 | # whether to forcibly preopen all indexes on startup 434 | # optional, default is 1 (preopen everything) 435 | preopen_indexes = 1 436 | 437 | # whether to unlink .old index copies on succesful rotation. 438 | # optional, default is 1 (do unlink) 439 | unlink_old = 1 440 | 441 | # attribute updates periodic flush timeout, seconds 442 | # updates will be automatically dumped to disk this frequently 443 | # optional, default is 0 (disable periodic flush) 444 | # 445 | # attr_flush_period = 900 446 | 447 | 448 | # instance-wide ondisk_dict defaults (per-index value take precedence) 449 | # optional, default is 0 (precache all dictionaries in RAM) 450 | # 451 | # ondisk_dict_default = 1 452 | 453 | 454 | # MVA updates pool size 455 | # shared between all instances of searchd, disables attr flushes! 456 | # optional, default size is 1M 457 | mva_updates_pool = 1M 458 | 459 | # max allowed network packet size 460 | # limits both query packets from clients, and responses from agents 461 | # optional, default size is 8M 462 | max_packet_size = 8M 463 | 464 | # crash log path 465 | # searchd will (try to) log crashed query to 'crash_log_path.PID' file 466 | # optional, default is empty (do not create crash logs) 467 | # 468 | # crash_log_path = /var/db/sphinxsearch/log/crash 469 | 470 | 471 | # max allowed per-query filter count 472 | # optional, default is 256 473 | max_filters = 256 474 | 475 | # max allowed per-filter values count 476 | # optional, default is 4096 477 | max_filter_values = 4096 478 | 479 | 480 | # socket listen queue length 481 | # optional, default is 5 482 | # 483 | # listen_backlog = 5 484 | 485 | 486 | # per-keyword read buffer size 487 | # optional, default is 256K 488 | # 489 | # read_buffer = 256K 490 | 491 | 492 | # unhinted read size (currently used when reading hits) 493 | # optional, default is 32K 494 | # 495 | # read_unhinted = 32K 496 | 497 | 498 | # max allowed per-batch query count (aka multi-query count) 499 | # optional, default is 32 500 | max_batch_queries = 32 501 | 502 | 503 | # max common subtree document cache size, per-query 504 | # optional, default is 0 (disable subtree optimization) 505 | # 506 | # subtree_docs_cache = 4M 507 | 508 | 509 | # max common subtree hit cache size, per-query 510 | # optional, default is 0 (disable subtree optimization) 511 | # 512 | # subtree_hits_cache = 8M 513 | 514 | 515 | # multi-processing mode (MPM) 516 | # known values are none, fork, prefork, and threads 517 | # optional, default is fork 518 | # 519 | workers = threads # for RT to work 520 | 521 | 522 | # max threads to create for searching local parts of a distributed index 523 | # optional, default is 0, which means disable multi-threaded searching 524 | # should work with all MPMs (ie. does NOT require workers=threads) 525 | # 526 | # dist_threads = 4 527 | 528 | 529 | # binlog files path; use empty string to disable binlog 530 | # optional, default is build-time configured data directory 531 | # 532 | # binlog_path = # disable logging 533 | binlog_path = /var/db/sphinxsearch/data # binlog.001 etc will be created there 534 | 535 | 536 | # binlog flush/sync mode 537 | # 0 means flush and sync every second 538 | # 1 means flush and sync every transaction 539 | # 2 means flush every transaction, sync every second 540 | # optional, default is 2 541 | # 542 | # binlog_flush = 2 543 | 544 | 545 | # binlog per-file size limit 546 | # optional, default is 128M, 0 means no limit 547 | # 548 | # binlog_max_log_size = 256M 549 | 550 | 551 | # per-thread stack size, only affects workers=threads mode 552 | # optional, default is 64K 553 | # 554 | # thread_stack = 128K 555 | 556 | 557 | # per-keyword expansion limit (for dict=keywords prefix searches) 558 | # optional, default is 0 (no limit) 559 | # 560 | # expansion_limit = 1000 561 | 562 | 563 | # RT RAM chunks flush period 564 | # optional, default is 0 (no periodic flush) 565 | # 566 | # rt_flush_period = 900 567 | 568 | 569 | # query log file format 570 | # optional, known values are plain and sphinxql, default is plain 571 | # 572 | # query_log_format = sphinxql 573 | 574 | 575 | # version string returned to MySQL network protocol clients 576 | # optional, default is empty (use Sphinx version) 577 | # 578 | # mysql_version_string = 5.0.37 579 | 580 | 581 | # trusted plugin directory 582 | # optional, default is empty (disable UDFs) 583 | # 584 | # plugin_dir = /usr/local/sphinx/lib 585 | 586 | 587 | # default server-wide collation 588 | # optional, default is libc_ci 589 | # 590 | collation_server = utf8_general_ci 591 | 592 | 593 | # server-wide locale for libc based collations 594 | # optional, default is C 595 | # 596 | collation_libc_locale = en_US.UTF-8 597 | 598 | 599 | # threaded server watchdog (only used in workers=threads mode) 600 | # optional, values are 0 and 1, default is 1 (watchdog on) 601 | # 602 | # watchdog = 1 603 | 604 | 605 | # SphinxQL compatibility mode (legacy columns and their names) 606 | # optional, default is 0 (SQL compliant syntax and result sets) 607 | # 608 | compat_sphinxql_magics = 0 609 | } 610 | 611 | # --eof-- 612 | -------------------------------------------------------------------------------- /media/board.js: -------------------------------------------------------------------------------- 1 | Date.prototype.getMonthName = function(lang) { 2 | lang = lang && (lang in Date.locale) ? lang : 'en'; 3 | return Date.locale[lang].month_names[this.getMonth()]; 4 | }; 5 | 6 | Date.prototype.getMonthNameShort = function(lang) { 7 | lang = lang && (lang in Date.locale) ? lang : 'en'; 8 | return Date.locale[lang].month_names_short[this.getMonth()]; 9 | }; 10 | 11 | Date.prototype.getDayName = function(lang) { 12 | lang = lang && (lang in Date.locale) ? lang : 'en'; 13 | return Date.locale[lang].day_names[this.getDay()]; 14 | }; 15 | 16 | Date.prototype.getDayNameShort = function(lang) { 17 | lang = lang && (lang in Date.locale) ? lang : 'en'; 18 | return Date.locale[lang].day_names_short[this.getDay()]; 19 | }; 20 | 21 | 22 | Date.locale = { 23 | en: { 24 | month_names: ['January', 'February', 'March', 'April', 'May', 'June', 25 | 'July', 'August', 'September', 'October', 'November', 'December'], 26 | month_names_short: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 27 | 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], 28 | day_names: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 29 | 'Friday', 'Saturday'], 30 | day_names_short: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] 31 | } 32 | }; 33 | 34 | 35 | var selected_style; 36 | function replyhighlight(id){ 37 | var tdtags=document.getElementsByTagName("td"); 38 | var new_selected_style="reply"; 39 | for(i=0; i2) return decodeURIComponent(hit[2]); 74 | else return ''; 75 | } 76 | }; 77 | 78 | function toggle(id){ 79 | var elem; 80 | 81 | if(!(elem=document.getElementById(id))) return; 82 | 83 | elem.style.display=elem.style.display?"":"none"; 84 | } 85 | 86 | function toggle_search(source, dest) { 87 | var source_form = document.forms[source + "-form"]; 88 | var dest_form = document.forms[dest + "-form"]; 89 | 90 | toggle(source); 91 | toggle(dest); 92 | 93 | dest_form.elements["search_text"].value = source_form.elements["search_text"].value; 94 | } 95 | 96 | function who_are_you_quoting(e) { 97 | var parent, d, clr, src, cnt, left, top, width, maxWidth; 98 | 99 | maxWidth = 500; 100 | 101 | e = e.target || window.event.srcElement; 102 | 103 | cnt = document.createElement('div'); 104 | cnt.id = 'q-p'; 105 | 106 | src = document.getElementById(e.getAttribute('href').split('#')[1]); 107 | 108 | width = src.offsetWidth; 109 | if (width > maxWidth) { 110 | width = maxWidth; 111 | } 112 | src = src.cloneNode(true); 113 | src.id = 'q-p-s'; 114 | if (src.tagName == 'DIV') { 115 | src.setAttribute('class', 'q-p-op'); 116 | clr = document.createElement('div'); 117 | clr.setAttribute('class', 'newthr'); 118 | src.appendChild(clr); 119 | } 120 | 121 | left = 0; 122 | top = e.offsetHeight + 1; 123 | parent = e; 124 | do { 125 | left += parent.offsetLeft; 126 | top += parent.offsetTop; 127 | } while (parent = parent.offsetParent); 128 | 129 | if ((d = document.body.offsetWidth - left - width) < 0) { 130 | left += d; 131 | } 132 | 133 | cnt.setAttribute('style', 'left:' + left + 'px;top:' + top + 'px;'); 134 | cnt.appendChild(src); 135 | document.body.appendChild(cnt); 136 | } 137 | 138 | function remove_quote_preview(e) { 139 | var cnt; 140 | if (cnt = document.getElementById('q-p')) { 141 | document.body.removeChild(cnt); 142 | } 143 | } 144 | 145 | function quotePreview() { 146 | var quotes = document.forms.postform.getElementsByClassName('backlink'); 147 | for (i = 0, j = quotes.length; i < j; ++i) { 148 | quotes[i].addEventListener('mouseover', who_are_you_quoting, false); 149 | quotes[i].addEventListener('mouseout', remove_quote_preview, false); 150 | } 151 | } 152 | 153 | function backlink() { 154 | var i, j, ii, jj, tid, bl, qb, t, form, backlinks, linklist, replies; 155 | 156 | form = document.forms.postform; 157 | 158 | if (!(replies = form.getElementsByClassName('reply'))) { 159 | return; 160 | } 161 | 162 | for (i = 0, j = replies.length; i < j; ++i) { 163 | if (!(backlinks = replies[i].getElementsByClassName('backlink'))) { 164 | continue; 165 | } 166 | linklist = {}; 167 | for (ii = 0, jj = backlinks.length; ii < jj; ++ii) { 168 | tid = backlinks[ii].getAttribute('href').split(/#/); 169 | if (!(t = document.getElementById(tid[1]))) { 170 | continue; 171 | } 172 | if (t.tagName == 'DIV') { 173 | backlinks[ii].textContent = '>>OP'; 174 | } 175 | if (linklist[tid[1]]) { 176 | continue; 177 | } 178 | bl = document.createElement('a'); 179 | bl.className = 'backlink'; 180 | bl.href = '#' + replies[i].id; 181 | bl.textContent = '>>' + replies[i].id.slice(1); 182 | bl.onclick = new Function("replyhighlight('" + replies[i].id + "')"); 183 | if (!(qb = t.getElementsByClassName('quoted-by')[0])) { 184 | linklist[tid[1]] = true; 185 | qb = document.createElement('div'); 186 | qb.className = 'quoted-by'; 187 | qb.textContent = 'Quoted by: '; 188 | qb.appendChild(bl); 189 | t.insertBefore(qb, t.getElementsByTagName('blockquote')[0]); 190 | } 191 | else { 192 | linklist[tid[1]] = true; 193 | qb.appendChild(document.createTextNode(' ')); 194 | qb.appendChild(bl); 195 | } 196 | } 197 | } 198 | } 199 | 200 | function pad(n) { 201 | return String("0" + n).slice(-2); 202 | } 203 | 204 | function localDate() { 205 | var form, dates; 206 | 207 | form = document.forms.postform; 208 | 209 | if (!(dates = form.getElementsByClassName('posttime'))) { 210 | return; 211 | } 212 | 213 | for (i = 0, j = dates.length; i < j; ++i) { 214 | var postdate = new Date(parseInt(dates[i].getAttribute("title"))); 215 | var date = postdate.getDate(); 216 | var month = postdate.getMonthNameShort("en"); 217 | var year = postdate.getFullYear(); 218 | var minutes = postdate.getMinutes(); 219 | var hours = postdate.getHours(); 220 | var seconds = postdate.getSeconds(); 221 | var day = postdate.getDayNameShort("en"); 222 | var datestring = day + " " + month + " " + date + " " + 223 | pad(hours) + ":" + pad(minutes) + ":" + pad(seconds) + " " + year; 224 | 225 | dates[i].innerHTML = datestring; 226 | } 227 | } 228 | 229 | function run() { 230 | var i, j, quotes, arr = location.href.split(/#/); 231 | 232 | if(arr[1]) 233 | replyhighlight(arr[1]); 234 | 235 | if(document.forms.postform && document.forms.postform.NAMAE) 236 | document.forms.postform.NAMAE.value=get_cookie("name"); 237 | 238 | if(document.forms.postform && document.forms.postform.MERU) 239 | document.forms.postform.MERU.value=get_cookie("email"); 240 | 241 | if(document.forms.postform && document.forms.postform.delpass) 242 | document.forms.postform.delpass.value=get_cookie("delpass"); 243 | 244 | if (document.getElementsByClassName) { 245 | backlink(); 246 | quotePreview(); 247 | localDate(); 248 | } 249 | } 250 | 251 | if (window.addEventListener) { 252 | window.addEventListener('DOMContentLoaded', run, false); 253 | } 254 | else { 255 | window.onload = run; 256 | } 257 | -------------------------------------------------------------------------------- /media/calendar.css: -------------------------------------------------------------------------------- 1 | #CalendarControlIFrame { 2 | display: none; 3 | left: 0px; 4 | position: absolute; 5 | top: 0px; 6 | height: 250px; 7 | width: 250px; 8 | z-index: 99; 9 | } 10 | 11 | #CalendarControl { 12 | position:absolute; 13 | background-color:#FFF; 14 | margin:0; 15 | padding:0; 16 | display:none; 17 | z-index: 100; 18 | } 19 | 20 | #CalendarControl table { 21 | font-family: arial, verdana, helvetica, sans-serif; 22 | font-size: 10pt; 23 | border-left: 1px solid #e04000; 24 | border-right: 1px solid #e04000; 25 | } 26 | 27 | #CalendarControl th { 28 | font-weight: normal; 29 | } 30 | 31 | #CalendarControl th a { 32 | font-weight: normal; 33 | text-decoration: none; 34 | color: #FFF; 35 | padding: 1px; 36 | } 37 | 38 | #CalendarControl td { 39 | text-align: center; 40 | } 41 | 42 | #CalendarControl .header { 43 | background-color: #e04000; 44 | } 45 | 46 | #CalendarControl .weekday { 47 | background-color: #eefff2; 48 | color: #000; 49 | } 50 | 51 | #CalendarControl .weekend { 52 | background-color: #d6f0da; 53 | color: #000; 54 | } 55 | 56 | #CalendarControl .current { 57 | border: 1px solid #e04000; 58 | background-color: #e04000; 59 | color: #FFF; 60 | } 61 | 62 | #CalendarControl .weekday, 63 | #CalendarControl .weekend, 64 | #CalendarControl .current { 65 | display: block; 66 | text-decoration: none; 67 | border: 1px solid #FFF; 68 | width: 2em; 69 | } 70 | 71 | #CalendarControl .weekday:hover, 72 | #CalendarControl .weekend:hover, 73 | #CalendarControl .current:hover { 74 | color: #FFF; 75 | background-color: #e04000; 76 | border: 1px solid #e04000; 77 | } 78 | 79 | #CalendarControl .previous { 80 | text-align: left; 81 | } 82 | 83 | #CalendarControl .next { 84 | text-align: right; 85 | } 86 | 87 | #CalendarControl .previous, 88 | #CalendarControl .next { 89 | padding: 1px 3px 1px 3px; 90 | font-size: 1.4em; 91 | } 92 | 93 | #CalendarControl .previous a, 94 | #CalendarControl .next a { 95 | color: #FFF; 96 | text-decoration: none; 97 | font-weight: bold; 98 | } 99 | 100 | #CalendarControl .title { 101 | text-align: center; 102 | font-weight: bold; 103 | color: #FFF; 104 | } 105 | 106 | #CalendarControl .empty { 107 | background-color: #98c1a9; 108 | border: 1px solid #FFF; 109 | } 110 | 111 | -------------------------------------------------------------------------------- /media/calendar.js: -------------------------------------------------------------------------------- 1 | function positionInfo(object) { 2 | 3 | var p_elm = object; 4 | 5 | this.getElementLeft = getElementLeft; 6 | function getElementLeft() { 7 | var x = 0; 8 | var elm; 9 | if(typeof(p_elm) == "object"){ 10 | elm = p_elm; 11 | } else { 12 | elm = document.getElementById(p_elm); 13 | } 14 | while (elm != null) { 15 | if(elm.style.position == 'relative') { 16 | break; 17 | } 18 | else { 19 | x += elm.offsetLeft; 20 | elm = elm.offsetParent; 21 | } 22 | } 23 | return parseInt(x); 24 | } 25 | 26 | this.getElementWidth = getElementWidth; 27 | function getElementWidth(){ 28 | var elm; 29 | if(typeof(p_elm) == "object"){ 30 | elm = p_elm; 31 | } else { 32 | elm = document.getElementById(p_elm); 33 | } 34 | return parseInt(elm.offsetWidth); 35 | } 36 | 37 | this.getElementRight = getElementRight; 38 | function getElementRight(){ 39 | return getElementLeft(p_elm) + getElementWidth(p_elm); 40 | } 41 | 42 | this.getElementTop = getElementTop; 43 | function getElementTop() { 44 | var y = 0; 45 | var elm; 46 | if(typeof(p_elm) == "object"){ 47 | elm = p_elm; 48 | } else { 49 | elm = document.getElementById(p_elm); 50 | } 51 | while (elm != null) { 52 | if(elm.style.position == 'relative') { 53 | break; 54 | } 55 | else { 56 | y+= elm.offsetTop; 57 | elm = elm.offsetParent; 58 | } 59 | } 60 | return parseInt(y); 61 | } 62 | 63 | this.getElementHeight = getElementHeight; 64 | function getElementHeight(){ 65 | var elm; 66 | if(typeof(p_elm) == "object"){ 67 | elm = p_elm; 68 | } else { 69 | elm = document.getElementById(p_elm); 70 | } 71 | return parseInt(elm.offsetHeight); 72 | } 73 | 74 | this.getElementBottom = getElementBottom; 75 | function getElementBottom(){ 76 | return getElementTop(p_elm) + getElementHeight(p_elm); 77 | } 78 | } 79 | 80 | function CalendarControl() { 81 | 82 | var calendarId = 'CalendarControl'; 83 | var currentYear = 0; 84 | var currentMonth = 0; 85 | var currentDay = 0; 86 | 87 | var selectedYear = 0; 88 | var selectedMonth = 0; 89 | var selectedDay = 0; 90 | 91 | var months = ['January','February','March','April','May','June','July','August','September','October','November','December']; 92 | var dateField = null; 93 | 94 | function getProperty(p_property){ 95 | var p_elm = calendarId; 96 | var elm = null; 97 | 98 | if(typeof(p_elm) == "object"){ 99 | elm = p_elm; 100 | } else { 101 | elm = document.getElementById(p_elm); 102 | } 103 | if (elm != null){ 104 | if(elm.style){ 105 | elm = elm.style; 106 | if(elm[p_property]){ 107 | return elm[p_property]; 108 | } else { 109 | return null; 110 | } 111 | } else { 112 | return null; 113 | } 114 | } 115 | } 116 | 117 | function setElementProperty(p_property, p_value, p_elmId){ 118 | var p_elm = p_elmId; 119 | var elm = null; 120 | 121 | if(typeof(p_elm) == "object"){ 122 | elm = p_elm; 123 | } else { 124 | elm = document.getElementById(p_elm); 125 | } 126 | if((elm != null) && (elm.style != null)){ 127 | elm = elm.style; 128 | elm[ p_property ] = p_value; 129 | } 130 | } 131 | 132 | function setProperty(p_property, p_value) { 133 | setElementProperty(p_property, p_value, calendarId); 134 | } 135 | 136 | function getDaysInMonth(year, month) { 137 | return [31,((!(year % 4 ) && ( (year % 100 ) || !( year % 400 ) ))?29:28),31,30,31,30,31,31,30,31,30,31][month-1]; 138 | } 139 | 140 | function getDayOfWeek(year, month, day) { 141 | var date = new Date(year,month-1,day) 142 | return date.getDay(); 143 | } 144 | 145 | this.clearDate = clearDate; 146 | function clearDate() { 147 | dateField.value = ''; 148 | hide(); 149 | } 150 | 151 | this.setDate = setDate; 152 | function setDate(year, month, day) { 153 | if (dateField) { 154 | if (month < 10) {month = "0" + month;} 155 | if (day < 10) {day = "0" + day;} 156 | 157 | var dateString = year+"-"+month+"-"+day; 158 | dateField.value = dateString; 159 | hide(); 160 | } 161 | return; 162 | } 163 | 164 | this.changeMonth = changeMonth; 165 | function changeMonth(change) { 166 | currentMonth += change; 167 | currentDay = 0; 168 | if(currentMonth > 12) { 169 | currentMonth = 1; 170 | currentYear++; 171 | } else if(currentMonth < 1) { 172 | currentMonth = 12; 173 | currentYear--; 174 | } 175 | 176 | calendar = document.getElementById(calendarId); 177 | calendar.innerHTML = calendarDrawTable(); 178 | } 179 | 180 | this.changeYear = changeYear; 181 | function changeYear(change) { 182 | currentYear += change; 183 | currentDay = 0; 184 | calendar = document.getElementById(calendarId); 185 | calendar.innerHTML = calendarDrawTable(); 186 | } 187 | 188 | function getCurrentYear() { 189 | var year = new Date().getYear(); 190 | if(year < 1900) year += 1900; 191 | return year; 192 | } 193 | 194 | function getCurrentMonth() { 195 | return new Date().getMonth() + 1; 196 | } 197 | 198 | function getCurrentDay() { 199 | return new Date().getDate(); 200 | } 201 | 202 | function calendarDrawTable() { 203 | 204 | var dayOfMonth = 1; 205 | var validDay = 0; 206 | var startDayOfWeek = getDayOfWeek(currentYear, currentMonth, dayOfMonth); 207 | var daysInMonth = getDaysInMonth(currentYear, currentMonth); 208 | var css_class = null; //CSS class for each day 209 | 210 | var table = ""; 211 | table = table + ""; 212 | table = table + " "; 213 | table = table + " "; 214 | table = table + " "; 215 | table = table + ""; 216 | table = table + ""; 217 | 218 | for(var week=0; week < 6; week++) { 219 | table = table + ""; 220 | for(var dayOfWeek=0; dayOfWeek < 7; dayOfWeek++) { 221 | if(week == 0 && startDayOfWeek == dayOfWeek) { 222 | validDay = 1; 223 | } else if (validDay == 1 && dayOfMonth > daysInMonth) { 224 | validDay = 0; 225 | } 226 | 227 | if(validDay) { 228 | if (dayOfMonth == selectedDay && currentYear == selectedYear && currentMonth == selectedMonth) { 229 | css_class = 'current'; 230 | } else if (dayOfWeek == 0 || dayOfWeek == 6) { 231 | css_class = 'weekend'; 232 | } else { 233 | css_class = 'weekday'; 234 | } 235 | 236 | table = table + ""; 237 | dayOfMonth++; 238 | } else { 239 | table = table + ""; 240 | } 241 | } 242 | table = table + ""; 243 | } 244 | 245 | table = table + ""; 246 | table = table + "
< «" + months[currentMonth-1] + "
" + currentYear + "
» >
SMTWTFS
"+dayOfMonth+" 
Clear | Close
"; 247 | 248 | return table; 249 | } 250 | 251 | this.show = show; 252 | function show(field) { 253 | can_hide = 0; 254 | 255 | // If the calendar is visible and associated with 256 | // this field do not do anything. 257 | if (dateField == field) { 258 | return; 259 | } else { 260 | dateField = field; 261 | } 262 | 263 | if(dateField) { 264 | try { 265 | var dateString = new String(dateField.value); 266 | var dateParts = dateString.split("-"); 267 | 268 | selectedYear = parseInt(dateParts[0],10); 269 | selectedMonth = parseInt(dateParts[1],10); 270 | selectedDay = parseInt(dateParts[2],10); 271 | } catch(e) {} 272 | } 273 | 274 | if (!(selectedYear && selectedMonth && selectedDay)) { 275 | selectedMonth = getCurrentMonth(); 276 | selectedDay = getCurrentDay(); 277 | selectedYear = getCurrentYear(); 278 | } 279 | 280 | currentMonth = selectedMonth; 281 | currentDay = selectedDay; 282 | currentYear = selectedYear; 283 | 284 | if(document.getElementById){ 285 | 286 | calendar = document.getElementById(calendarId); 287 | calendar.innerHTML = calendarDrawTable(currentYear, currentMonth); 288 | 289 | setProperty('display', 'block'); 290 | 291 | var fieldPos = new positionInfo(dateField); 292 | var calendarPos = new positionInfo(calendarId); 293 | 294 | var x = fieldPos.getElementLeft(); 295 | var y = fieldPos.getElementBottom(); 296 | 297 | setProperty('left', x + "px"); 298 | setProperty('top', y + "px"); 299 | 300 | if (document.all) { 301 | setElementProperty('display', 'block', 'CalendarControlIFrame'); 302 | setElementProperty('left', x + "px", 'CalendarControlIFrame'); 303 | setElementProperty('top', y + "px", 'CalendarControlIFrame'); 304 | setElementProperty('width', calendarPos.getElementWidth() + "px", 'CalendarControlIFrame'); 305 | setElementProperty('height', calendarPos.getElementHeight() + "px", 'CalendarControlIFrame'); 306 | } 307 | } 308 | } 309 | 310 | this.hide = hide; 311 | function hide() { 312 | if(dateField) { 313 | setProperty('display', 'none'); 314 | setElementProperty('display', 'none', 'CalendarControlIFrame'); 315 | dateField = null; 316 | } 317 | } 318 | 319 | this.visible = visible; 320 | function visible() { 321 | return dateField 322 | } 323 | 324 | this.can_hide = can_hide; 325 | var can_hide = 0; 326 | } 327 | 328 | var calendarControl = new CalendarControl(); 329 | 330 | function showCalendarControl(textField) { 331 | // textField.onblur = hideCalendarControl; 332 | calendarControl.show(textField); 333 | } 334 | 335 | function clearCalendarControl() { 336 | calendarControl.clearDate(); 337 | } 338 | 339 | function hideCalendarControl() { 340 | if (calendarControl.visible()) { 341 | calendarControl.hide(); 342 | } 343 | } 344 | 345 | function setCalendarControlDate(year, month, day) { 346 | calendarControl.setDate(year, month, day); 347 | } 348 | 349 | function changeCalendarControlYear(change) { 350 | calendarControl.changeYear(change); 351 | } 352 | 353 | function changeCalendarControlMonth(change) { 354 | calendarControl.changeMonth(change); 355 | } 356 | 357 | document.write(""); 358 | document.write("
"); 359 | 360 | -------------------------------------------------------------------------------- /media/deleted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eksopl/fuuka/ec9e325b64101d959eebd795c6e35c56bd4bd0b0/media/deleted.png -------------------------------------------------------------------------------- /media/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eksopl/fuuka/ec9e325b64101d959eebd795c6e35c56bd4bd0b0/media/error.png -------------------------------------------------------------------------------- /media/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eksopl/fuuka/ec9e325b64101d959eebd795c6e35c56bd4bd0b0/media/favicon.ico -------------------------------------------------------------------------------- /media/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eksopl/fuuka/ec9e325b64101d959eebd795c6e35c56bd4bd0b0/media/favicon.png -------------------------------------------------------------------------------- /media/fuuka.css: -------------------------------------------------------------------------------- 1 | h1{ 2 | font:bold 2em Verdana,Tahoma,sans-serif; 3 | margin:0.1em; 4 | } 5 | h2{ 6 | font:bold 1.25em Verdana,Tahoma,sans-serif; 7 | margin:0.1em; 8 | } 9 | ul{ 10 | margin:0; 11 | list-style-type:circle; 12 | } 13 | .postblock{ 14 | font-weight:bold; 15 | background:#98c1a9; 16 | text-align:center; 17 | } 18 | .postspan{ 19 | font-weight:bold; 20 | background:#d6f0da; 21 | text-align:left; 22 | padding:0.3em; 23 | margin:0.3em 0.3em 0.3em 0; 24 | } 25 | .generic-table, .generic-table-monowidth{ 26 | margin-left:auto; 27 | margin-right:auto; 28 | overflow:hidden; 29 | } 30 | .generic-table-monowidth{ 31 | width:40em; 32 | } 33 | .generic-table { 34 | margin-top:1em; 35 | } 36 | .generic-table-monowidth th { 37 | background:#88cc99; 38 | font-weight:bold; 39 | text-align:right; 40 | padding:2px; 41 | } 42 | .generic-table-monowidth tr { 43 | background:#d6f0da; 44 | padding:2px; 45 | } 46 | .generic-table th { 47 | background:#7faa66; 48 | color:#402800; 49 | font-weight:800; 50 | text-align:left; 51 | padding:2px; 52 | } 53 | .report-thumbs div{ 54 | float:left; 55 | width:300px; 56 | height:300px; 57 | 58 | } 59 | .report-thumbs table{ 60 | border-spacing:0; 61 | background:#d6f0da; 62 | border:solid 2px #8c9; 63 | margin:1em auto 1em auto; 64 | } 65 | .report-thumbs table tr { 66 | font-weight:800; 67 | text-align:center; 68 | background:#8c9; 69 | } 70 | .nothumb { 71 | color: inherit; 72 | background: #eee; 73 | border: 1px solid #aaa; 74 | text-align: center; 75 | padding: 1em; 76 | } 77 | .omittedposts { 78 | color:#707070; 79 | } 80 | 81 | .filetitle { 82 | font-size:1.1em; 83 | color:#0f0c5d; 84 | font-weight: bold; 85 | } 86 | .theader { 87 | background:#E04000; 88 | text-align:center; 89 | padding:2px; 90 | color:white; 91 | width:100%; 92 | font-weight:bold; 93 | } 94 | .newthr{ 95 | clear: left; 96 | } 97 | 98 | .warning,.info{ 99 | background: #dece7a; 100 | color: #654225; 101 | font-weight:bold; 102 | } 103 | .error{ 104 | background: #de887a; 105 | color: #652534; 106 | font-weight:bold; 107 | } 108 | 109 | .warning,.error{ padding: 0.675em 0.5em 1.325em 0.5em; } 110 | .info{ padding: 0.675em 0.5em 0.675em 0.5em; } 111 | 112 | .container{ 113 | -moz-border-radius: 0.75em; 114 | margin: 4em auto 1em auto; 115 | width: 70%; 116 | } 117 | 118 | a.tooltip span, a.tooltip-red span{ 119 | font-family:Georgia, serif; 120 | display:none; 121 | padding:6px; 122 | margin:2em 0 0 0.5em; 123 | width:25%; 124 | } 125 | a.tooltip:hover span, a.tooltip-red:hover span{ 126 | display:inline; 127 | position:absolute; 128 | border:1px solid black; 129 | background:lightyellow; 130 | color:black; 131 | font-weight:normal; 132 | text-align:left; 133 | text-decoration:none; 134 | } 135 | a.tooltip-red:hover{ 136 | color:white; 137 | } 138 | 139 | .spoiler, .spoiler:hover { 140 | color: black; 141 | background: black; 142 | text-decoration: none; 143 | } 144 | .spoiler a { 145 | color: black !important; 146 | } 147 | .spoiler:hover, .spoiler:hover a { 148 | color: white !important; 149 | } 150 | 151 | #q-p { 152 | position: absolute; 153 | color: #002200; 154 | } 155 | #q-p-s { 156 | border: 1px solid #888; 157 | padding: 3px; 158 | } 159 | .q-p-op { 160 | background-color: #EEFFF2; 161 | } 162 | .quoted-by { 163 | padding-top: 3px; 164 | color: #707070; 165 | margin-left: 25px; 166 | font-size: 0.75em; 167 | text-decoration: none; 168 | } 169 | .quoted-by a { 170 | text-decoration: none; 171 | } 172 | 173 | .trip{ 174 | font-weight:normal !important; 175 | } 176 | 177 | .invis-link { 178 | text-decoration: none; 179 | } 180 | .invis-link:hover { 181 | text-decoration: underline; 182 | } 183 | 184 | code,.code { white-space: pre } 185 | a img { vertical-align: top; } 186 | 187 | .o { text-decoration: overline; } 188 | .u { text-decoration: underline; } 189 | .aa { font-family:Mona,'MS PGothic' !important; } 190 | .s { text-decoration: line-through; } 191 | .banned { font-weight: bold; color: red; } 192 | .moot { 193 | padding: 5px; 194 | margin-left: .5em; 195 | border-color: #faa; 196 | border: 2px dashed rgba(255,0,0,.1); 197 | border-radius: 2px 198 | } 199 | .mod{ color: #800080 !important; } 200 | .admin{ color: #c00000 !important; } 201 | .dev{ color: #0000F0 !important; } 202 | -------------------------------------------------------------------------------- /media/god-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eksopl/fuuka/ec9e325b64101d959eebd795c6e35c56bd4bd0b0/media/god-left.png -------------------------------------------------------------------------------- /media/god-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eksopl/fuuka/ec9e325b64101d959eebd795c6e35c56bd4bd0b0/media/god-right.png -------------------------------------------------------------------------------- /media/internal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eksopl/fuuka/ec9e325b64101d959eebd795c6e35c56bd4bd0b0/media/internal.png -------------------------------------------------------------------------------- /media/spoiler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eksopl/fuuka/ec9e325b64101d959eebd795c6e35c56bd4bd0b0/media/spoiler.png -------------------------------------------------------------------------------- /media/spoilers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eksopl/fuuka/ec9e325b64101d959eebd795c6e35c56bd4bd0b0/media/spoilers.png -------------------------------------------------------------------------------- /media/sticky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eksopl/fuuka/ec9e325b64101d959eebd795c6e35c56bd4bd0b0/media/sticky.png -------------------------------------------------------------------------------- /reports/activity: -------------------------------------------------------------------------------- 1 | Query: select (floor(timestamp/300)%288)*300,count(*),count(media_hash),count(case when email = 'sage' then 1 else NULL end) from %%BOARD%% use index(timestamp_index) where timestamp>%%NOW%%-86400 group by floor(timestamp/300)%288 order by floor(timestamp/300)%288; 2 | Title: Daily activity 3 | Mode: graph 4 | Refresh-Rate: 3600 5 | -------------------------------------------------------------------------------- /reports/activity-archive: -------------------------------------------------------------------------------- 1 | Query: select ((floor(timestamp/3600)%24)*3600)+1800, count(*), count(case when email = 'sage' then 1 else NULL end) from %%BOARD%% use index(timestamp_index) where timestamp > %%NOW%%-86400 and subnum != 0 group by floor(timestamp/3600)%24 order by floor(timestamp/3600)%24; 2 | Title: Daily activity (Hourly, archive) 3 | Mode: graph 4 | Refresh-Rate: 3600 5 | -------------------------------------------------------------------------------- /reports/activity-hourly: -------------------------------------------------------------------------------- 1 | Query: select ((floor(timestamp/3600)%24)*3600)+1800,count(*),count(media_hash),count(case when email = 'sage' then 1 else NULL end) from %%BOARD%% use index(timestamp_index) where timestamp>%%NOW%%-86400 group by floor(timestamp/3600)%24 order by floor(timestamp/3600)%24; 2 | Title: Daily activity (Hourly) 3 | Mode: graph 4 | Refresh-Rate: 3600 5 | -------------------------------------------------------------------------------- /reports/availability: -------------------------------------------------------------------------------- 1 | Query: select name, trip, count(num), avg(timestamp%86400), stddev_pop(timestamp%86400), (avg((timestamp+43200)%86400)+43200) % 86400, stddev_pop((timestamp+43200)%86400) from %%BOARD%% where timestamp > %%NOW%% - 2592000 group by name, trip having count(*) > 4 order by name, trip 2 | Title: Availability 3 | Rows: Name,Posts last month,Availability 4 | Row-Types: username,text,fromto 5 | Mode: table 6 | Refresh-Rate: 21600 7 | -------------------------------------------------------------------------------- /reports/graphs/activity-archive.graph: -------------------------------------------------------------------------------- 1 | set terminal %%TERM%% transparent size 800,600 2 | set output "%%OUTFILE%%" 3 | show terminal 4 | 5 | set title 'Per hour' 6 | set timefmt "%s" 7 | set xdata time 8 | set yrange [ 0 : ] 9 | set xrange [ "0" : "86400" ] 10 | set format x "%H:%M" 11 | set grid 12 | set key left 13 | set boxwidth 3600 14 | set style fill solid border -1 15 | plot '%%INFILE%%' using 1:2 t 'Posts' with boxes lt rgb "#008000", \ 16 | '%%INFILE%%' using 1:3 t 'Sages' with boxes lt rgb "#ff0000" 17 | -------------------------------------------------------------------------------- /reports/graphs/activity-hourly.graph: -------------------------------------------------------------------------------- 1 | set terminal %%TERM%% transparent size 800,600 2 | set output "%%OUTFILE%%" 3 | show terminal 4 | 5 | set title 'Per hour' 6 | set timefmt "%s" 7 | set xdata time 8 | set yrange [ 0 : ] 9 | set xrange [ "0" : "86400" ] 10 | set format x "%H:%M" 11 | set grid 12 | set key left 13 | set boxwidth 3600 14 | set style fill solid border -1 15 | plot '%%INFILE%%' using 1:2 t 'Posts' with boxes lt rgb "#008000", \ 16 | '%%INFILE%%' using 1:3 t 'Images' with boxes lt rgb "#0000ff", \ 17 | '%%INFILE%%' using 1:4 t 'Sages' with boxes lt rgb "#ff0000" 18 | -------------------------------------------------------------------------------- /reports/graphs/activity.graph: -------------------------------------------------------------------------------- 1 | set terminal %%TERM%% transparent size 800,600 2 | set output "%%OUTFILE%%" 3 | show terminal 4 | 5 | set title 'Per five minutes' 6 | set style data fsteps 7 | set timefmt "%s" 8 | set yrange [ 0 : ] 9 | set xdata time 10 | set format x "%H:%M" 11 | set grid 12 | set key left 13 | plot '%%INFILE%%' using 1:2 t 'Posts' lt rgb "#008000" with filledcurve x1, \ 14 | '%%INFILE%%' using 1:3 t 'Images' lt rgb "#0000ff" with filledcurve x1, \ 15 | '%%INFILE%%' using 1:4 t 'Sages' lt rgb "#ff0000" with filledcurve x1 16 | -------------------------------------------------------------------------------- /reports/graphs/karma.graph: -------------------------------------------------------------------------------- 1 | set terminal %%TERM%% transparent size 800,600 2 | set output "%%OUTFILE%%" 3 | show terminal 4 | 5 | set title 'Per day' 6 | set style data fsteps 7 | set timefmt "%s" 8 | set yrange [ 0 : ] 9 | set xdata time 10 | set xrange [ "%%XSTART%%" : "%%XEND%%" ] 11 | set format x "%m/%y" 12 | set grid 13 | set key left 14 | plot '%%INFILE%%' using 1:2 t 'Posts' lt rgb "#008000" with filledcurve x1, \ 15 | '%%INFILE%%' using 1:3 t 'Images' lt rgb "#0000ff" with filledcurve x1, \ 16 | '%%INFILE%%' using 1:4 t 'Sages' lt rgb "#ff0000" with filledcurve x1 17 | -------------------------------------------------------------------------------- /reports/graphs/population.graph: -------------------------------------------------------------------------------- 1 | set terminal %%TERM%% transparent size 800,600 2 | set output "%%OUTFILE%%" 3 | show terminal 4 | 5 | set title 'Posts by' 6 | set style data fsteps 7 | set timefmt "%s" 8 | set yrange [ 0 : ] 9 | set xdata time 10 | set xrange [ "%%XSTART%%" : "%%XEND%%" ] 11 | set format x "%m/%y" 12 | set grid 13 | set key left 14 | 15 | plot \ 16 | '%%INFILE%%' using 1:4 t 'Anonymous' lt rgb "#008000" with filledcurve x1, \ 17 | '%%INFILE%%' using 1:2 t 'Tripfriends' lt rgb "#0000ff" with filledcurve x1, \ 18 | '%%INFILE%%' using 1:3 t 'Namefags' lt rgb "#ff0000" with filledcurve x1 19 | -------------------------------------------------------------------------------- /reports/image-reposts: -------------------------------------------------------------------------------- 1 | Query: select preview,num,subnum,parent,media_hash,total from %%BOARD%%_images order by total desc limit 32; 2 | Title: Most Reposted Images 3 | Rows: Image,Reposts 4 | Row-Types: image,text 5 | Mode: thumbs 6 | Refresh-Rate: 0 7 | -------------------------------------------------------------------------------- /reports/karma: -------------------------------------------------------------------------------- 1 | Query: select day,posts,images,sage from %%BOARD%%_daily where day > floor((%%NOW%%-31536000)/86400)*86400 group by day order by day; 2 | Title: Karma 3 | Mode: graph 4 | Refresh-Rate: 3600 -------------------------------------------------------------------------------- /reports/new-users: -------------------------------------------------------------------------------- 1 | Query: select name, trip, firstseen, postcount FROM %%BOARD%%_users where postcount > 30 order by firstseen desc; 2 | Title: Users by date of first post 3 | Rows: Name,First seen,Total posts 4 | Row-Types: username,timestamp,text 5 | Mode: table 6 | Refresh-Rate: 0 7 | -------------------------------------------------------------------------------- /reports/population: -------------------------------------------------------------------------------- 1 | Query: select day, trips, names, anons from %%BOARD%%_daily where day > floor((%%NOW%%-31536000)/86400)*86400 group by day order by day 2 | Title: Population 3 | Mode: graph 4 | Refresh-Rate: 3600 5 | -------------------------------------------------------------------------------- /reports/post-count: -------------------------------------------------------------------------------- 1 | Query: select name, trip, postcount from %%BOARD%%_users order by postcount desc limit 512 2 | Title: Post counts 3 | Rows: Name,Total posts 4 | Row-Types: username,text 5 | Mode: table 6 | Refresh-Rate: 0 7 | -------------------------------------------------------------------------------- /reports/post-rate: -------------------------------------------------------------------------------- 1 | Query: select count(*),count(*)/60 from %%BOARD%% where timestamp>%%NOW%%-3600; 2 | Title: Post rate 3 | Rows: Posts in last hour,Posts per minute 4 | Row-Types: text,text 5 | Mode: table 6 | Refresh-Rate: 0 7 | -------------------------------------------------------------------------------- /reports/post-rate-archive: -------------------------------------------------------------------------------- 1 | Query: select count(*),count(*)/60 from %%BOARD%% where timestamp>%%NOW%%-3600 and subnum != 0; 2 | Title: Post rate (Archive) 3 | Rows: Posts in last hour,Posts per minute 4 | Row-Types: text,text 5 | Mode: table 6 | Refresh-Rate: 0 7 | -------------------------------------------------------------------------------- /reports/users-online: -------------------------------------------------------------------------------- 1 | Query: select name,trip,max(timestamp),num,subnum from %%BOARD%% where timestamp>%%NOW%%-1800 group by name,trip order by max(timestamp) desc 2 | Title: Users online 3 | Rows: Name,Last seen,Last post 4 | Row-Types: username,timediff,postno 5 | Mode: table 6 | Refresh-Rate: 0 7 | -------------------------------------------------------------------------------- /reports/users-online-internal: -------------------------------------------------------------------------------- 1 | Query: select group_concat(distinct concat(name) separator ', '),max(timestamp),num,subnum from %%BOARD%% where id!=0 and timestamp>%%NOW%%-3600 group by id order by max(timestamp) desc 2 | Title: Users posting in archive 3 | Rows: Posted as,Last seen,Last post 4 | Row-Types: text,timediff,postno 5 | Mode: table 6 | Refresh-Rate: 0 7 | -------------------------------------------------------------------------------- /sql/r0070.sql: -------------------------------------------------------------------------------- 1 | -- Run when upgrading to revision r70 2 | 3 | CREATE TABLE IF NOT EXISTS `index_counters` ( 4 | `id` varchar(50) NOT NULL, 5 | `val` int(10) NOT NULL, 6 | PRIMARY KEY (`id`) 7 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 8 | 9 | -- For the following three lines: 10 | -- Replace `a` with `board`, where board is the name of the board you are archiving 11 | -- Copy and paste for every other board you have 12 | 13 | alter table `a` drop primary key; 14 | alter table `a` add doc_id int(10) unsigned not null auto_increment primary key first; 15 | alter ignore table `a` add unique num_subnum_index (num, subnum); 16 | -------------------------------------------------------------------------------- /sql/r0114.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings; 5 | use utf8; 6 | 7 | # Use this to update your fuuka DB, when updating to SVN revision r114 or later. 8 | # 9 | # Recommended usage is: 10 | # perl r0114.pl > r0114.mine.sql 11 | # less r0114.mine.sql (to analyze the file) 12 | # mysql -u -p < r0114.mine.sql 13 | # 14 | # Hardcore usage: 15 | # perl r0114.pl | mysql -u -p 16 | # 17 | # There's also a static version in r0114.sql, if you don't want automatic 18 | # generation for some reason. 19 | 20 | BEGIN{require "../board-config.pl"} 21 | 22 | my $boards = BOARD_SETTINGS; 23 | my @boards = sort keys %$boards; 24 | my $charset = DB_CHARSET; 25 | 26 | print <<'HERE'; 27 | -- 28 | -- Run when upgrading to revision r98 or later 29 | -- 30 | 31 | HERE 32 | 33 | print 'USE ' . DB_DATABSE_NAME . ';'; 34 | 35 | for(@boards) { 36 | print <<"HERE"; 37 | 38 | 39 | -- 40 | -- Updating table `$_` 41 | -- 42 | 43 | -- Update with keys on: there's few NULL values to insert, the index will help 44 | -- us only update those. 45 | UPDATE `$_` SET name = NULL WHERE name = ''; 46 | UPDATE `$_` SET comment = NULL WHERE comment = ''; 47 | 48 | -- Update with keys off: a huge portion of all entries need to get updated, 49 | -- cost of a table scan + reenabling indexes beats the overhead of updating 50 | -- the indexes during updates. 51 | ALTER TABLE `$_` DISABLE KEYS; 52 | 53 | UPDATE `$_` SET media_hash = NULL WHERE media_hash = ''; 54 | UPDATE `$_` SET email = NULL WHERE email = ''; 55 | UPDATE `$_` SET trip = NULL WHERE trip = ''; 56 | 57 | -- Irrelevant: column doesn't have keys. 58 | UPDATE `$_` SET preview = NULL WHERE preview = ''; 59 | UPDATE `$_` SET media = NULL WHERE media = ''; 60 | UPDATE `$_` SET media_filename = NULL WHERE media_filename = ''; 61 | UPDATE `$_` SET title = NULL WHERE title = ''; 62 | HERE 63 | print <<"HERE" if($_ eq 'a' or $_ eq 'jp'); 64 | 65 | -- Irrelevant: media_filename doesn't have keys and it's going to do a table 66 | -- scan regardless. 67 | -- 68 | -- This query fixes an inconsistency in data left in very old, legacy 69 | -- legacy versions o fuuka. Execute this query ONLY if you're running with /a/ 70 | -- or /jp/ tables acquired from Easymodo. Otherwise you're just wasting time. 71 | UPDATE `$_` SET media_filename = CONCAT(SUBSTRING_INDEX(preview,'s.',1), 72 | SUBSTRING(media from -4)) WHERE (media_filename is NULL OR 73 | media_filename = '') AND preview != '' AND media != ''; 74 | HERE 75 | print <<"HERE"; 76 | 77 | 78 | -- Perform DB schema changes 79 | -- 80 | -- Adjust fields and table default charset to the to new one (utf8mb4). 81 | -- Adjust sizes and types as well. 82 | -- Change the ID field to support IPv6. 83 | -- Make numeric fields not null. 84 | -- Add sticky field. 85 | ALTER TABLE `$_` COLLATE $charset\_general_ci, 86 | CHANGE preview preview VARCHAR(20) COLLATE $charset\_general_ci NULL DEFAULT NULL, 87 | CHANGE media media TEXT COLLATE $charset\_general_ci NULL DEFAULT NULL, 88 | CHANGE media_hash media_hash VARCHAR(25) COLLATE $charset\_general_ci NULL DEFAULT NULL, 89 | CHANGE media_filename media_filename VARCHAR(20) COLLATE $charset\_general_ci NULL DEFAULT NULL, 90 | CHANGE email email VARCHAR(100) COLLATE $charset\_general_ci NULL DEFAULT NULL, 91 | CHANGE name name VARCHAR(100) COLLATE $charset\_general_ci NULL DEFAULT NULL, 92 | CHANGE trip trip VARCHAR(25) COLLATE $charset\_general_ci NULL DEFAULT NULL, 93 | CHANGE title title VARCHAR(100) COLLATE $charset\_general_ci NULL DEFAULT NULL, 94 | CHANGE comment comment TEXT COLLATE $charset\_general_ci NULL DEFAULT NULL, 95 | CHANGE delpass delpass TINYTEXT COLLATE $charset\_general_ci NULL DEFAULT NULL, 96 | CHANGE id id DECIMAL(39, 0) UNSIGNED NOT NULL DEFAULT '0', 97 | CHANGE preview_w preview_w SMALLINT UNSIGNED NOT NULL DEFAULT '0', 98 | CHANGE preview_h preview_h SMALLINT UNSIGNED NOT NULL DEFAULT '0', 99 | CHANGE media_w media_w SMALLINT UNSIGNED NOT NULL DEFAULT '0', 100 | CHANGE media_h media_h SMALLINT UNSIGNED NOT NULL DEFAULT '0', 101 | CHANGE media_size media_size INT UNSIGNED NOT NULL DEFAULT '0', 102 | CHANGE spoiler spoiler BOOL NOT NULL DEFAULT '0', 103 | CHANGE deleted deleted BOOL NOT NULL DEFAULT '0', 104 | ADD sticky BOOL NOT NULL DEFAULT '0'; 105 | 106 | -- Re-enable keys. 107 | ALTER TABLE `$_` ENABLE KEYS; 108 | 109 | -- We can get rid of the _local tables, too. 110 | DROP TABLE IF EXISTS `$_\_local`; 111 | HERE 112 | } 113 | -------------------------------------------------------------------------------- /sql/r0114.sql: -------------------------------------------------------------------------------- 1 | -- Run when upgrading to revision r114 or later 2 | -- 3 | -- Replace `a` with `board`, where board is the name of the board you are 4 | -- archiving. Copy and paste for every other board you have. 5 | -- 6 | 7 | -- Update with keys on: there's few NULL values to insert, the index will help 8 | -- us only update those. 9 | UPDATE `a` SET name = NULL WHERE name = ''; 10 | UPDATE `a` SET comment = NULL WHERE comment = ''; 11 | 12 | -- Update with keys off: a huge portion of all entries need to get updated, 13 | -- cost of a table scan + reenabling indexes beats the overhead of updating 14 | -- the indexes during updates. 15 | ALTER TABLE `a` DISABLE KEYS; 16 | 17 | UPDATE `a` SET media_hash = NULL WHERE media_hash = ''; 18 | UPDATE `a` SET email = NULL WHERE email = ''; 19 | UPDATE `a` SET trip = NULL WHERE trip = ''; 20 | 21 | -- Irrelevant: column doesn't have keys. 22 | UPDATE `a` SET preview = NULL WHERE preview = ''; 23 | UPDATE `a` SET media = NULL WHERE media = ''; 24 | UPDATE `a` SET media_filename = NULL WHERE media_filename = ''; 25 | UPDATE `a` SET title = NULL WHERE title = ''; 26 | 27 | -- Irrelevant: media_filename doesn't have keys and it's going to do a table 28 | -- scan regardless. 29 | -- 30 | -- This query fixes an inconsistency in data left in very old, legacy 31 | -- legacy versions o fuuka. Execute this query ONLY if you're running with /a/ 32 | -- or /jp/ tables acquired from Easymodo. Otherwise you're just wasting time. 33 | UPDATE `a` SET media_filename = CONCAT(SUBSTRING_INDEX(preview,'s.',1), 34 | SUBSTRING(media from -4)) WHERE (media_filename is NULL OR 35 | media_filename = '') AND preview != '' AND media != ''; 36 | 37 | -- Perform DB schema changes 38 | -- 39 | -- Adjust fields to new charset (utf8mb4) and new sizes 40 | -- utf8mb4 requires MySQL 5.5 or later. Upgrade or change it to utf8mb4_general_ci. 41 | -- See DB_CHARSET in board-config.pl for more info. 42 | -- Perform DB schema changes 43 | -- 44 | -- Adjust fields and table default charset to the to new one (utf8mb4). 45 | -- Adjust sizes and types as well. 46 | -- Change the ID field to support IPv6. 47 | -- Make numeric fields not null. 48 | -- Add sticky field. 49 | ALTER TABLE `a` COLLATE utf8mb4_general_ci, 50 | CHANGE preview preview VARCHAR(20) COLLATE utf8mb4_general_ci NULL DEFAULT NULL, 51 | CHANGE media media TEXT COLLATE utf8mb4_general_ci NULL DEFAULT NULL, 52 | CHANGE media_hash media_hash VARCHAR(25) COLLATE utf8mb4_general_ci NULL DEFAULT NULL, 53 | CHANGE media_filename media_filename VARCHAR(20) COLLATE utf8mb4_general_ci NULL DEFAULT NULL, 54 | CHANGE email email VARCHAR(100) COLLATE utf8mb4_general_ci NULL DEFAULT NULL, 55 | CHANGE name name VARCHAR(100) COLLATE utf8mb4_general_ci NULL DEFAULT NULL, 56 | CHANGE trip trip VARCHAR(25) COLLATE utf8mb4_general_ci NULL DEFAULT NULL, 57 | CHANGE title title VARCHAR(100) COLLATE utf8mb4_general_ci NULL DEFAULT NULL, 58 | CHANGE comment comment TEXT COLLATE utf8mb4_general_ci NULL DEFAULT NULL, 59 | CHANGE delpass delpass TINYTEXT COLLATE utf8mb4_general_ci NULL DEFAULT NULL, 60 | CHANGE id id DECIMAL(39, 0) UNSIGNED NOT NULL DEFAULT '0', 61 | CHANGE preview_w preview_w SMALLINT UNSIGNED NOT NULL DEFAULT '0', 62 | CHANGE preview_h preview_h SMALLINT UNSIGNED NOT NULL DEFAULT '0', 63 | CHANGE media_w media_w SMALLINT UNSIGNED NOT NULL DEFAULT '0', 64 | CHANGE media_h media_h SMALLINT UNSIGNED NOT NULL DEFAULT '0', 65 | CHANGE media_size media_size INT UNSIGNED NOT NULL DEFAULT '0', 66 | CHANGE spoiler spoiler BOOL NOT NULL DEFAULT '0', 67 | CHANGE deleted deleted BOOL NOT NULL DEFAULT '0', 68 | ADD sticky BOOL NOT NULL DEFAULT '0'; 69 | 70 | -- Re-enable keys. 71 | ALTER TABLE `a` ENABLE KEYS; 72 | 73 | -- We can get rid of the _local tables, too. 74 | DROP TABLE IF EXISTS `a_local`; 75 | -------------------------------------------------------------------------------- /sql/r0142.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings; 5 | use utf8; 6 | 7 | # Use this to update your fuuka DB, when updating to SVN revision r142 or later. 8 | # 9 | # Recommended usage is: 10 | # perl r0142.pl > r0142.mine.sql 11 | # less r0142.mine.sql (to analyze the file) 12 | # mysql -u -p < r0142.mine.sql 13 | # 14 | # Hardcore usage: 15 | # perl r0142.pl | mysql -u -p 16 | # 17 | # There's also a static version in r0142.sql, if you don't want automatic 18 | # generation for some reason. 19 | 20 | BEGIN{-e "../board-config-local.pl" ? 21 | require "../board-config-local.pl" : require "../board-config.pl"} 22 | 23 | my $boards = BOARD_SETTINGS; 24 | my @boards = sort keys %$boards; 25 | my $charset = DB_CHARSET; 26 | 27 | print <<'HERE'; 28 | -- 29 | -- Run when upgrading to revision r142 or later 30 | -- 31 | 32 | HERE 33 | 34 | print 'USE ' . DB_DATABSE_NAME . ';'; 35 | 36 | for(@boards) { 37 | print <<"HERE"; 38 | 39 | 40 | -- 41 | -- Processing table `$_` 42 | -- 43 | 44 | DROP TABLE IF EXISTS `$_\_threads`; 45 | 46 | -- Creating threads table 47 | CREATE TABLE IF NOT EXISTS `$_\_threads` ( 48 | `doc_id_p` int unsigned NOT NULL, 49 | `parent` int unsigned NOT NULL, 50 | `time_op` int unsigned NOT NULL, 51 | `time_last` int unsigned NOT NULL, 52 | `time_bump` int unsigned NOT NULL, 53 | `time_ghost` int unsigned DEFAULT NULL, 54 | `time_ghost_bump` int unsigned DEFAULT NULL, 55 | `nreplies` int unsigned NOT NULL DEFAULT '0', 56 | `nimages` int unsigned NOT NULL DEFAULT '0', 57 | PRIMARY KEY (`doc_id_p`) 58 | ) ENGINE=InnoDB COLLATE utf8_general_ci; 59 | 60 | -- Populating threads table with threads 61 | INSERT INTO `$_\_threads` ( 62 | SELECT 63 | op.doc_id_p, op.p, op.timestamp, 0, 0, NULL, NULL, 0, 0 64 | FROM 65 | (SELECT doc_id AS doc_id_p, num AS p, timestamp FROM `$_` WHERE parent = 0) 66 | AS op 67 | ); 68 | 69 | -- Updating threads with reply information 70 | UPDATE 71 | `$_\_threads` op 72 | SET 73 | op.time_last = ( 74 | COALESCE(GREATEST( 75 | op.time_op, 76 | (SELECT MAX(timestamp) FROM `$_` re FORCE INDEX(parent_index) WHERE 77 | re.parent = op.parent GROUP BY parent) 78 | ), op.time_op) 79 | ), 80 | op.time_bump = ( 81 | COALESCE(GREATEST( 82 | op.time_op, 83 | (SELECT MAX(timestamp) FROM `$_` re FORCE INDEX(parent_index) WHERE 84 | re.parent = op.parent AND (re.email <> 'sage' OR re.email IS NULL) 85 | GROUP BY parent) 86 | ), op.time_op) 87 | ), 88 | op.time_ghost = ( 89 | SELECT MAX(timestamp) FROM `$_` re FORCE INDEX(parent_index) WHERE 90 | re.parent = op.parent AND re.subnum <> 0 GROUP BY parent 91 | ), 92 | op.time_ghost_bump = ( 93 | SELECT MAX(timestamp) FROM `$_` re FORCE INDEX(parent_index) WHERE 94 | re.parent = op.parent AND re.subnum <> 0 AND (re.email <> 'sage' 95 | OR re.email IS NULL) GROUP BY parent 96 | ), 97 | op.nreplies = ( 98 | SELECT COUNT(*) FROM `$_` re FORCE INDEX(parent_index) WHERE 99 | re.parent = op.parent 100 | ), 101 | op.nimages = ( 102 | SELECT COUNT(media_hash) FROM `$_` re FORCE INDEX(parent_index) WHERE 103 | re.parent = op.parent 104 | ); 105 | 106 | -- Creating triggers and stored procedures 107 | DELIMITER // 108 | 109 | DROP PROCEDURE IF EXISTS `update_thread_$_`// 110 | 111 | CREATE PROCEDURE `update_thread_$_` (tnum INT) 112 | BEGIN 113 | UPDATE 114 | `$_\_threads` op 115 | SET 116 | op.time_last = ( 117 | COALESCE(GREATEST( 118 | op.time_op, 119 | (SELECT MAX(timestamp) FROM `$_` re FORCE INDEX(parent_index) WHERE 120 | re.parent = tnum AND re.subnum = 0) 121 | ), op.time_op) 122 | ), 123 | op.time_bump = ( 124 | COALESCE(GREATEST( 125 | op.time_op, 126 | (SELECT MAX(timestamp) FROM `$_` re FORCE INDEX(parent_index) WHERE 127 | re.parent = tnum AND (re.email <> 'sage' OR re.email IS NULL) 128 | AND re.subnum = 0) 129 | ), op.time_op) 130 | ), 131 | op.time_ghost = ( 132 | SELECT MAX(timestamp) FROM `$_` re FORCE INDEX(parent_index) WHERE 133 | re.parent = tnum AND re.subnum <> 0 134 | ), 135 | op.time_ghost_bump = ( 136 | SELECT MAX(timestamp) FROM `$_` re FORCE INDEX(parent_index) WHERE 137 | re.parent = tnum AND re.subnum <> 0 AND (re.email <> 'sage' OR 138 | re.email IS NULL) 139 | ), 140 | op.nreplies = ( 141 | SELECT COUNT(*) FROM `$_` re FORCE INDEX(parent_index) WHERE 142 | re.parent = tnum 143 | ), 144 | op.nimages = ( 145 | SELECT COUNT(media_hash) FROM `$_` re FORCE INDEX(parent_index) WHERE 146 | re.parent = tnum 147 | ) 148 | WHERE op.parent = tnum; 149 | END// 150 | 151 | DROP PROCEDURE IF EXISTS `create_thread_$_`// 152 | 153 | CREATE PROCEDURE `create_thread_$_` (doc_id INT, num INT, timestamp INT) 154 | BEGIN 155 | INSERT IGNORE INTO `$_\_threads` VALUES (doc_id, num, timestamp, timestamp, 156 | timestamp, NULL, NULL, 0, 0); 157 | END// 158 | 159 | DROP PROCEDURE IF EXISTS `delete_thread_$_`// 160 | 161 | CREATE PROCEDURE `delete_thread_$_` (tnum INT) 162 | BEGIN 163 | DELETE FROM `$_\_threads` WHERE parent = tnum; 164 | END// 165 | 166 | DROP TRIGGER IF EXISTS `after_ins_$_`// 167 | 168 | CREATE TRIGGER `after_ins_$_` AFTER INSERT ON `$_` 169 | FOR EACH ROW 170 | BEGIN 171 | IF NEW.parent = 0 THEN 172 | CALL create_thread_$_(NEW.doc_id, NEW.num, NEW.timestamp); 173 | END IF; 174 | CALL update_thread_$_(NEW.parent); 175 | END; 176 | // 177 | 178 | DROP TRIGGER IF EXISTS `after_del_$_`// 179 | 180 | CREATE TRIGGER `after_del_$_` AFTER DELETE ON `$_` 181 | FOR EACH ROW 182 | BEGIN 183 | CALL update_thread_$_(OLD.parent); 184 | IF OLD.parent = 0 THEN 185 | CALL delete_thread_$_(OLD.num); 186 | END IF; 187 | END; 188 | // 189 | 190 | DELIMITER ; 191 | 192 | -- Add the remaining indexes 193 | CREATE UNIQUE INDEX parent_index ON `$_\_threads` (parent); 194 | CREATE INDEX time_ghost_bump_index ON `$_\_threads` (time_ghost_bump); 195 | HERE 196 | } 197 | -------------------------------------------------------------------------------- /sql/r0142.sql: -------------------------------------------------------------------------------- 1 | -- Run when upgrading to revision r142 or later 2 | -- 3 | -- Replace `a` with `board`, where board is the name of the board you are 4 | -- archiving. Same for a_threads, update_thread_a, etc. 5 | -- 6 | -- Copy and paste for every other board you have. 7 | -- 8 | -- (There's too much to replace, so you might want to use the Perl script 9 | -- instead, which will generate an .sql file for you) 10 | -- 11 | 12 | USE archive; 13 | 14 | -- 15 | -- Processing table `a` 16 | -- 17 | 18 | DROP TABLE IF EXISTS `a_threads`; 19 | 20 | -- Creating threads table 21 | CREATE TABLE IF NOT EXISTS `a_threads` ( 22 | `doc_id_p` int unsigned NOT NULL, 23 | `parent` int unsigned NOT NULL, 24 | `time_op` int unsigned NOT NULL, 25 | `time_last` int unsigned NOT NULL, 26 | `time_bump` int unsigned NOT NULL, 27 | `time_ghost` int unsigned DEFAULT NULL, 28 | `time_ghost_bump` int unsigned DEFAULT NULL, 29 | `nreplies` int unsigned NOT NULL DEFAULT '0', 30 | `nimages` int unsigned NOT NULL DEFAULT '0', 31 | PRIMARY KEY (`doc_id_p`) 32 | ) ENGINE=InnoDB COLLATE utf8_general_ci; 33 | 34 | -- Populating threads table with threads 35 | INSERT INTO `a_threads` ( 36 | SELECT 37 | op.doc_id_p, op.p, op.timestamp, 0, 0, NULL, NULL, 0, 0 38 | FROM 39 | (SELECT doc_id AS doc_id_p, num AS p, timestamp FROM `a` WHERE parent = 0) 40 | AS op 41 | ); 42 | 43 | -- Updating threads with reply information 44 | UPDATE 45 | `a_threads` op 46 | SET 47 | op.time_last = ( 48 | COALESCE(GREATEST( 49 | op.time_op, 50 | (SELECT MAX(timestamp) FROM `a` re FORCE INDEX(parent_index) WHERE 51 | re.parent = op.parent GROUP BY parent) 52 | ), op.time_op) 53 | ), 54 | op.time_bump = ( 55 | COALESCE(GREATEST( 56 | op.time_op, 57 | (SELECT MAX(timestamp) FROM `a` re FORCE INDEX(parent_index) WHERE 58 | re.parent = op.parent AND (re.email <> 'sage' OR re.email IS NULL) 59 | GROUP BY parent) 60 | ), op.time_op) 61 | ), 62 | op.time_ghost = ( 63 | SELECT MAX(timestamp) FROM `a` re FORCE INDEX(parent_index) WHERE 64 | re.parent = op.parent AND re.subnum <> 0 GROUP BY parent 65 | ), 66 | op.time_ghost_bump = ( 67 | SELECT MAX(timestamp) FROM `a` re FORCE INDEX(parent_index) WHERE 68 | re.parent = op.parent AND re.subnum <> 0 AND (re.email <> 'sage' 69 | OR re.email IS NULL) GROUP BY parent 70 | ), 71 | op.nreplies = ( 72 | SELECT COUNT(*) FROM `a` re FORCE INDEX(parent_index) WHERE 73 | re.parent = op.parent 74 | ), 75 | op.nimages = ( 76 | SELECT COUNT(media_hash) FROM `a` re FORCE INDEX(parent_index) WHERE 77 | re.parent = op.parent 78 | ); 79 | 80 | -- Creating triggers and stored procedures 81 | DELIMITER // 82 | 83 | DROP PROCEDURE IF EXISTS `update_thread_a`// 84 | 85 | CREATE PROCEDURE `update_thread_a` (tnum INT) 86 | BEGIN 87 | UPDATE 88 | `a_threads` op 89 | SET 90 | op.time_last = ( 91 | COALESCE(GREATEST( 92 | op.time_op, 93 | (SELECT MAX(timestamp) FROM `a` re FORCE INDEX(parent_index) WHERE 94 | re.parent = tnum AND re.subnum = 0) 95 | ), op.time_op) 96 | ), 97 | op.time_bump = ( 98 | COALESCE(GREATEST( 99 | op.time_op, 100 | (SELECT MAX(timestamp) FROM `a` re FORCE INDEX(parent_index) WHERE 101 | re.parent = tnum AND (re.email <> 'sage' OR re.email IS NULL) 102 | AND re.subnum = 0) 103 | ), op.time_op) 104 | ), 105 | op.time_ghost = ( 106 | SELECT MAX(timestamp) FROM `a` re FORCE INDEX(parent_index) WHERE 107 | re.parent = tnum AND re.subnum <> 0 108 | ), 109 | op.time_ghost_bump = ( 110 | SELECT MAX(timestamp) FROM `a` re FORCE INDEX(parent_index) WHERE 111 | re.parent = tnum AND re.subnum <> 0 AND (re.email <> 'sage' OR 112 | re.email IS NULL) 113 | ), 114 | op.nreplies = ( 115 | SELECT COUNT(*) FROM `a` re FORCE INDEX(parent_index) WHERE 116 | re.parent = tnum 117 | ), 118 | op.nimages = ( 119 | SELECT COUNT(media_hash) FROM `a` re FORCE INDEX(parent_index) WHERE 120 | re.parent = tnum 121 | ) 122 | WHERE op.parent = tnum; 123 | END// 124 | 125 | DROP PROCEDURE IF EXISTS `create_thread_a`// 126 | 127 | CREATE PROCEDURE `create_thread_a` (doc_id INT, num INT, timestamp INT) 128 | BEGIN 129 | INSERT IGNORE INTO `a_threads` VALUES (doc_id, num, timestamp, timestamp, 130 | timestamp, NULL, NULL, 0, 0); 131 | END// 132 | 133 | DROP PROCEDURE IF EXISTS `delete_thread_a`// 134 | 135 | CREATE PROCEDURE `delete_thread_a` (tnum INT) 136 | BEGIN 137 | DELETE FROM `a_threads` WHERE parent = tnum; 138 | END// 139 | 140 | DROP TRIGGER IF EXISTS `after_ins_a`// 141 | 142 | CREATE TRIGGER `after_ins_a` AFTER INSERT ON `a` 143 | FOR EACH ROW 144 | BEGIN 145 | IF NEW.parent = 0 THEN 146 | CALL create_thread_a(NEW.doc_id, NEW.num, NEW.timestamp); 147 | END IF; 148 | CALL update_thread_a(NEW.parent); 149 | END; 150 | // 151 | 152 | DROP TRIGGER IF EXISTS `after_del_a`// 153 | 154 | CREATE TRIGGER `after_del_a` AFTER DELETE ON `a` 155 | FOR EACH ROW 156 | BEGIN 157 | CALL update_thread_a(OLD.parent); 158 | IF OLD.parent = 0 THEN 159 | CALL delete_thread_a(OLD.num); 160 | END IF; 161 | END; 162 | // 163 | 164 | DELIMITER ; 165 | 166 | -- Add the remaining indexes 167 | CREATE UNIQUE INDEX parent_index ON `a_threads` (parent); 168 | CREATE INDEX time_ghost_bump_index ON `a_threads` (time_ghost_bump); 169 | -------------------------------------------------------------------------------- /sql/r0142_2.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings; 5 | use utf8; 6 | 7 | # Use this to update your fuuka DB, when updating to SVN revision r142 or later. 8 | # 9 | # Recommended usage is: 10 | # perl r0142_2.pl > r0142_2.mine.sql 11 | # less r0142_2.mine.sql (to analyze the file) 12 | # mysql -u -p < r0142_2.mine.sql 13 | # 14 | # Hardcore usage: 15 | # perl r0142_2.pl | mysql -u -p 16 | # 17 | # There's also a static version in r0142_2.sql, if you don't want automatic 18 | # generation for some reason. 19 | 20 | BEGIN{-e "../board-config-local.pl" ? 21 | require "../board-config-local.pl" : require "../board-config.pl"} 22 | 23 | my $boards = BOARD_SETTINGS; 24 | my @boards = sort keys %$boards; 25 | my $charset = DB_CHARSET; 26 | 27 | print <<'HERE'; 28 | -- 29 | -- Run when upgrading to revision r142 or later 30 | -- 31 | 32 | HERE 33 | 34 | print 'USE ' . DB_DATABSE_NAME . ';'; 35 | 36 | for(@boards) { 37 | print <<"HERE"; 38 | 39 | 40 | -- 41 | -- Processing table `$_` 42 | -- 43 | 44 | DROP TABLE IF EXISTS `$_\_images`; 45 | 46 | -- Creating images report table 47 | CREATE TABLE `$_\_images` ( 48 | `media_hash` varchar(25) NOT NULL, 49 | `num` int(10) unsigned NOT NULL, 50 | `subnum` int(10) unsigned NOT NULL, 51 | `parent` int(10) unsigned NOT NULL, 52 | `preview` varchar(20) NOT NULL, 53 | `total` int(10) unsigned NOT NULL DEFAULT '0', 54 | PRIMARY KEY (`media_hash`) 55 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 56 | 57 | DROP TABLE IF EXISTS `$_\_daily`; 58 | 59 | CREATE TABLE IF NOT EXISTS `$_\_daily` ( 60 | `day` int(10) unsigned NOT NULL, 61 | `posts` int(10) unsigned NOT NULL, 62 | `images` int(10) unsigned NOT NULL, 63 | `sage` int(10) unsigned NOT NULL, 64 | `anons` int(10) unsigned NOT NULL, 65 | `trips` int(10) unsigned NOT NULL, 66 | `names` int(10) unsigned NOT NULL, 67 | PRIMARY KEY (`day`) 68 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 69 | 70 | DROP TABLE IF EXISTS `$_\_users`; 71 | 72 | CREATE TABLE IF NOT EXISTS `$_\_users` ( 73 | `name` varchar(100) NOT NULL DEFAULT '', 74 | `trip` varchar(25) NOT NULL DEFAULT '', 75 | `firstseen` int(11) NOT NULL, 76 | `postcount` int(11) NOT NULL, 77 | PRIMARY KEY (`name`, `trip`) 78 | ) ENGINE=InnoDB DEFAULT CHARSET=$charset; 79 | 80 | 81 | -- Creating stored procedures and updating triggers 82 | DELIMITER // 83 | 84 | DROP PROCEDURE IF EXISTS `insert_image_$_`// 85 | 86 | CREATE PROCEDURE `insert_image_$_` (n_media_hash VARCHAR(25), n_num INT, 87 | n_subnum INT, n_parent INT, n_preview VARCHAR(20)) 88 | BEGIN 89 | DECLARE o_parent INT; 90 | 91 | -- This should be a transaction, but MySquirrel doesn't support transactions 92 | -- inside triggers or stored procedures (stay classy, MySQL) 93 | SELECT parent INTO o_parent FROM `$_\_images` WHERE media_hash = n_media_hash; 94 | IF o_parent IS NULL THEN 95 | INSERT INTO `$_\_images` VALUES (n_media_hash, n_num, n_subnum, n_parent, 96 | n_preview, 1); 97 | ELSEIF o_parent <> 0 AND n_parent = 0 THEN 98 | UPDATE `$_\_images` SET num = n_num, subnum = n_subnum, parent = n_parent, 99 | preview = n_preview, total = (total + 1) 100 | WHERE media_hash = n_media_hash; 101 | ELSE 102 | UPDATE `$_\_images` SET total = (total + 1) WHERE 103 | media_hash = n_media_hash; 104 | END IF; 105 | END// 106 | 107 | DROP PROCEDURE IF EXISTS `delete_image_$_`// 108 | 109 | CREATE PROCEDURE `delete_image_$_` (n_media_hash VARCHAR(25)) 110 | BEGIN 111 | UPDATE `$_\_images` SET total = (total - 1) WHERE media_hash = n_media_hash; 112 | END// 113 | 114 | DROP PROCEDURE IF EXISTS `insert_post_$_`// 115 | 116 | CREATE PROCEDURE `insert_post_$_` (p_timestamp INT, p_media_hash VARCHAR(25), 117 | p_email VARCHAR(100), p_name VARCHAR(100), p_trip VARCHAR(25)) 118 | BEGIN 119 | DECLARE d_day INT; 120 | DECLARE d_image INT; 121 | DECLARE d_sage INT; 122 | DECLARE d_anon INT; 123 | DECLARE d_trip INT; 124 | DECLARE d_name INT; 125 | 126 | SET d_day = FLOOR(p_timestamp/86400)*86400; 127 | SET d_image = p_media_hash IS NOT NULL; 128 | SET d_sage = COALESCE(p_email = 'sage', 0); 129 | SET d_anon = COALESCE(p_name = 'Anonymous' AND p_trip IS NULL, 0); 130 | SET d_trip = p_trip IS NOT NULL; 131 | SET d_name = COALESCE(p_name <> 'Anonymous' AND p_trip IS NULL, 1); 132 | 133 | INSERT INTO $_\_daily VALUES(d_day, 1, d_image, d_sage, d_anon, d_trip, 134 | d_name) 135 | ON DUPLICATE KEY UPDATE posts=posts+1, images=images+d_image, 136 | sage=sage+d_sage, anons=anons+d_anon, trips=trips+d_trip, 137 | names=names+d_name; 138 | 139 | -- Also should be a transaction. Lol MySQL. 140 | IF (SELECT trip FROM $_\_users WHERE trip = p_trip) IS NOT NULL THEN 141 | UPDATE $_\_users SET postcount=postcount+1, 142 | firstseen = LEAST(p_timestamp, firstseen) 143 | WHERE trip = p_trip; 144 | ELSE 145 | INSERT INTO $_\_users VALUES(COALESCE(p_name,''), COALESCE(p_trip,''), p_timestamp, 1) 146 | ON DUPLICATE KEY UPDATE postcount=postcount+1, 147 | firstseen = LEAST(VALUES(firstseen), firstseen); 148 | END IF; 149 | END// 150 | 151 | DROP PROCEDURE IF EXISTS `delete_post_$_`// 152 | 153 | CREATE PROCEDURE `delete_post_$_` (p_timestamp INT, p_media_hash VARCHAR(25), p_email VARCHAR(100), p_name VARCHAR(100), p_trip VARCHAR(25)) 154 | BEGIN 155 | DECLARE d_day INT; 156 | DECLARE d_image INT; 157 | DECLARE d_sage INT; 158 | DECLARE d_anon INT; 159 | DECLARE d_trip INT; 160 | DECLARE d_name INT; 161 | 162 | SET d_day = FLOOR(p_timestamp/86400)*86400; 163 | SET d_image = p_media_hash IS NOT NULL; 164 | SET d_sage = COALESCE(p_email = 'sage', 0); 165 | SET d_anon = COALESCE(p_name = 'Anonymous' AND p_trip IS NULL, 0); 166 | SET d_trip = p_trip IS NOT NULL; 167 | SET d_name = COALESCE(p_name <> 'Anonymous' AND p_trip IS NULL, 1); 168 | 169 | UPDATE $_\_daily SET posts=posts-1, images=images-d_image, 170 | sage=sage-d_sage, anons=anons-d_anon, trips=trips-d_trip, 171 | names=names-d_name WHERE day = d_day; 172 | 173 | -- Also should be a transaction. Lol MySQL. 174 | IF (SELECT trip FROM $_\_users WHERE trip = p_trip) IS NOT NULL THEN 175 | UPDATE $_\_users SET postcount = postcount-1 WHERE trip = p_trip; 176 | ELSE 177 | UPDATE $_\_users SET postcount = postcount-1 WHERE 178 | name = COALESCE(p_name, '') AND trip = COALESCE(p_trip, ''); 179 | END IF; 180 | END// 181 | 182 | DROP TRIGGER IF EXISTS `after_ins_$_`// 183 | 184 | CREATE TRIGGER `after_ins_$_` AFTER INSERT ON `$_` 185 | FOR EACH ROW 186 | BEGIN 187 | IF NEW.parent = 0 THEN 188 | CALL create_thread_$_(NEW.doc_id, NEW.num, NEW.timestamp); 189 | END IF; 190 | CALL update_thread_$_(NEW.parent); 191 | CALL insert_post_$_(NEW.timestamp, NEW.media_hash, NEW.email, NEW.name, 192 | NEW.trip); 193 | IF NEW.media_hash IS NOT NULL THEN 194 | CALL insert_image_$_(NEW.media_hash, NEW.num, NEW.subnum, NEW.parent, 195 | NEW.preview); 196 | END IF; 197 | END// 198 | 199 | DROP TRIGGER IF EXISTS `after_del_$_`// 200 | 201 | CREATE TRIGGER `after_del_$_` AFTER DELETE ON `$_` 202 | FOR EACH ROW 203 | BEGIN 204 | CALL update_thread_$_(OLD.parent); 205 | IF OLD.parent = 0 THEN 206 | CALL delete_thread_$_(OLD.num); 207 | END IF; 208 | CALL delete_post_$_(OLD.timestamp, OLD.media_hash, OLD.email, OLD.name, 209 | OLD.trip); 210 | IF OLD.media_hash IS NOT NULL THEN 211 | CALL delete_image_$_(OLD.media_hash); 212 | END IF; 213 | END// 214 | 215 | DELIMITER ; 216 | 217 | -- 218 | -- Populating images table with image info 219 | -- 220 | -- (About 7 minutes on /a/ with Easymodo data) 221 | INSERT INTO `$_\_images` ( 222 | SELECT media_hash, num, subnum, parent, preview, COUNT(*) 223 | FROM `$_` WHERE parent = 0 AND media_hash IS NOT NULL AND preview IS NOT NULL 224 | GROUP BY media_hash 225 | ); 226 | 227 | -- (About 14 minutes on /a/ with Easymodo data) 228 | INSERT INTO `$_\_images` ( 229 | SELECT media_hash, num, subnum, parent, preview, count(*) AS replyt 230 | FROM `$_` WHERE parent <> 0 AND 231 | media_hash IS NOT NULL AND preview IS NOT NULL GROUP BY media_hash 232 | ) 233 | ON DUPLICATE KEY UPDATE total = total + VALUES(total); 234 | 235 | -- 236 | -- Populating daily report table with info 237 | -- 238 | -- (About 6 minutes on /a/ with Easymodo data) 239 | INSERT INTO `$_\_daily` ( 240 | SELECT FLOOR(timestamp/86400)*86400 AS days, COUNT(*), 241 | SUM(media_hash IS NOT NULL), SUM(COALESCE(email = 'sage', 0)), 242 | SUM(COALESCE(name = 'Anonymous' AND trip IS NULL, 0)), 243 | SUM(trip IS NOT NULL), 244 | SUM(COALESCE(name <> 'Anonymous' AND trip IS NULL, 1)) 245 | FROM $_ GROUP BY days 246 | ); 247 | 248 | -- 249 | -- Populating users table with users 250 | -- 251 | -- (About 8 minutes on /a/ with Easymodo data) 252 | INSERT INTO `$_\_users` ( 253 | SELECT COALESCE(name, ''), '', MIN(timestamp), COUNT(*) from `$_` 254 | WHERE trip IS NULL GROUP BY name 255 | ); 256 | 257 | -- (About 6 minutes on /a/ with Easymodo data) 258 | INSERT INTO `$_\_users` ( 259 | SELECT COALESCE(name, ''), trip, MIN(timestamp), COUNT(*) from `$_` 260 | WHERE trip IS NOT NULL GROUP BY trip 261 | ); 262 | 263 | -- Add the remaining indexes 264 | CREATE INDEX total_index ON `$_\_images` (total); 265 | CREATE INDEX firstseen_index ON `$_\_users` (firstseen); 266 | CREATE INDEX postcount_index ON `$_\_users` (postcount); 267 | HERE 268 | } 269 | -------------------------------------------------------------------------------- /sql/r0142_2.sql: -------------------------------------------------------------------------------- 1 | -- Run when upgrading to revision r142 or later 2 | -- 3 | -- Replace `a` with `board`, where board is the name of the board you are 4 | -- archiving. Same for a_threads, update_thread_a, etc. 5 | -- 6 | -- Copy and paste for every other board you have. 7 | -- 8 | -- (There's too much to replace, so you might want to use the Perl script 9 | -- instead, which will generate an .sql file for you) 10 | -- 11 | 12 | USE archive; 13 | 14 | -- 15 | -- Processing table `a` 16 | -- 17 | 18 | DROP TABLE IF EXISTS `a_images`; 19 | 20 | -- Creating images report table 21 | CREATE TABLE `a_images` ( 22 | `media_hash` varchar(25) NOT NULL, 23 | `num` int(10) unsigned NOT NULL, 24 | `subnum` int(10) unsigned NOT NULL, 25 | `parent` int(10) unsigned NOT NULL, 26 | `preview` varchar(20) NOT NULL, 27 | `total` int(10) unsigned NOT NULL DEFAULT '0', 28 | PRIMARY KEY (`media_hash`) 29 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 30 | 31 | DROP TABLE IF EXISTS `a_daily`; 32 | 33 | CREATE TABLE IF NOT EXISTS `a_daily` ( 34 | `day` int(10) unsigned NOT NULL, 35 | `posts` int(10) unsigned NOT NULL, 36 | `images` int(10) unsigned NOT NULL, 37 | `sage` int(10) unsigned NOT NULL, 38 | `anons` int(10) unsigned NOT NULL, 39 | `trips` int(10) unsigned NOT NULL, 40 | `names` int(10) unsigned NOT NULL, 41 | PRIMARY KEY (`day`) 42 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 43 | 44 | DROP TABLE IF EXISTS `a_users`; 45 | 46 | CREATE TABLE IF NOT EXISTS `a_users` ( 47 | `name` varchar(100) NOT NULL DEFAULT '', 48 | `trip` varchar(25) NOT NULL DEFAULT '', 49 | `firstseen` int(11) NOT NULL, 50 | `postcount` int(11) NOT NULL, 51 | PRIMARY KEY (`name`, `trip`) 52 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 53 | 54 | 55 | -- Creating stored procedures and updating triggers 56 | DELIMITER // 57 | 58 | DROP PROCEDURE IF EXISTS `insert_image_a`// 59 | 60 | CREATE PROCEDURE `insert_image_a` (n_media_hash VARCHAR(25), n_num INT, 61 | n_subnum INT, n_parent INT, n_preview VARCHAR(20)) 62 | BEGIN 63 | DECLARE o_parent INT; 64 | 65 | -- This should be a transaction, but MySquirrel doesn't support transactions 66 | -- inside triggers or stored procedures (stay classy, MySQL) 67 | SELECT parent INTO o_parent FROM `a_images` WHERE media_hash = n_media_hash; 68 | IF o_parent IS NULL THEN 69 | INSERT INTO `a_images` VALUES (n_media_hash, n_num, n_subnum, n_parent, 70 | n_preview, 1); 71 | ELSEIF o_parent <> 0 AND n_parent = 0 THEN 72 | UPDATE `a_images` SET num = n_num, subnum = n_subnum, parent = n_parent, 73 | preview = n_preview, total = (total + 1) 74 | WHERE media_hash = n_media_hash; 75 | ELSE 76 | UPDATE `a_images` SET total = (total + 1) WHERE 77 | media_hash = n_media_hash; 78 | END IF; 79 | END// 80 | 81 | DROP PROCEDURE IF EXISTS `delete_image_a`// 82 | 83 | CREATE PROCEDURE `delete_image_a` (n_media_hash VARCHAR(25)) 84 | BEGIN 85 | UPDATE `a_images` SET total = (total - 1) WHERE media_hash = n_media_hash; 86 | END// 87 | 88 | DROP PROCEDURE IF EXISTS `insert_post_a`// 89 | 90 | CREATE PROCEDURE `insert_post_a` (p_timestamp INT, p_media_hash VARCHAR(25), 91 | p_email VARCHAR(100), p_name VARCHAR(100), p_trip VARCHAR(25)) 92 | BEGIN 93 | DECLARE d_day INT; 94 | DECLARE d_image INT; 95 | DECLARE d_sage INT; 96 | DECLARE d_anon INT; 97 | DECLARE d_trip INT; 98 | DECLARE d_name INT; 99 | 100 | SET d_day = FLOOR(p_timestamp/86400)*86400; 101 | SET d_image = p_media_hash IS NOT NULL; 102 | SET d_sage = COALESCE(p_email = 'sage', 0); 103 | SET d_anon = COALESCE(p_name = 'Anonymous' AND p_trip IS NULL, 0); 104 | SET d_trip = p_trip IS NOT NULL; 105 | SET d_name = COALESCE(p_name <> 'Anonymous' AND p_trip IS NULL, 1); 106 | 107 | INSERT INTO a_daily VALUES(d_day, 1, d_image, d_sage, d_anon, d_trip, 108 | d_name) 109 | ON DUPLICATE KEY UPDATE posts=posts+1, images=images+d_image, 110 | sage=sage+d_sage, anons=anons+d_anon, trips=trips+d_trip, 111 | names=names+d_name; 112 | 113 | -- Also should be a transaction. Lol MySQL. 114 | IF (SELECT trip FROM a_users WHERE trip = p_trip) IS NOT NULL THEN 115 | UPDATE a_users SET postcount=postcount+1, 116 | firstseen = LEAST(p_timestamp, firstseen) 117 | WHERE trip = p_trip; 118 | ELSE 119 | INSERT INTO a_users VALUES(COALESCE(p_name,''), COALESCE(p_trip,''), p_timestamp, 1) 120 | ON DUPLICATE KEY UPDATE postcount=postcount+1, 121 | firstseen = LEAST(VALUES(firstseen), firstseen); 122 | END IF; 123 | END// 124 | 125 | DROP PROCEDURE IF EXISTS `delete_post_a`// 126 | 127 | CREATE PROCEDURE `delete_post_a` (p_timestamp INT, p_media_hash VARCHAR(25), p_email VARCHAR(100), p_name VARCHAR(100), p_trip VARCHAR(25)) 128 | BEGIN 129 | DECLARE d_day INT; 130 | DECLARE d_image INT; 131 | DECLARE d_sage INT; 132 | DECLARE d_anon INT; 133 | DECLARE d_trip INT; 134 | DECLARE d_name INT; 135 | 136 | SET d_day = FLOOR(p_timestamp/86400)*86400; 137 | SET d_image = p_media_hash IS NOT NULL; 138 | SET d_sage = COALESCE(p_email = 'sage', 0); 139 | SET d_anon = COALESCE(p_name = 'Anonymous' AND p_trip IS NULL, 0); 140 | SET d_trip = p_trip IS NOT NULL; 141 | SET d_name = COALESCE(p_name <> 'Anonymous' AND p_trip IS NULL, 1); 142 | 143 | UPDATE a_daily SET posts=posts-1, images=images-d_image, 144 | sage=sage-d_sage, anons=anons-d_anon, trips=trips-d_trip, 145 | names=names-d_name WHERE day = d_day; 146 | 147 | -- Also should be a transaction. Lol MySQL. 148 | IF (SELECT trip FROM a_users WHERE trip = p_trip) IS NOT NULL THEN 149 | UPDATE a_users SET postcount = postcount-1 WHERE trip = p_trip; 150 | ELSE 151 | UPDATE a_users SET postcount = postcount-1 WHERE 152 | name = COALESCE(p_name, '') AND trip = COALESCE(p_trip, ''); 153 | END IF; 154 | END// 155 | 156 | DROP TRIGGER IF EXISTS `after_ins_a`// 157 | 158 | CREATE TRIGGER `after_ins_a` AFTER INSERT ON `a` 159 | FOR EACH ROW 160 | BEGIN 161 | IF NEW.parent = 0 THEN 162 | CALL create_thread_a(NEW.doc_id, NEW.num, NEW.timestamp); 163 | END IF; 164 | CALL update_thread_a(NEW.parent); 165 | CALL insert_post_a(NEW.timestamp, NEW.media_hash, NEW.email, NEW.name, 166 | NEW.trip); 167 | IF NEW.media_hash IS NOT NULL THEN 168 | CALL insert_image_a(NEW.media_hash, NEW.num, NEW.subnum, NEW.parent, 169 | NEW.preview); 170 | END IF; 171 | END// 172 | 173 | DROP TRIGGER IF EXISTS `after_del_a`// 174 | 175 | CREATE TRIGGER `after_del_a` AFTER DELETE ON `a` 176 | FOR EACH ROW 177 | BEGIN 178 | CALL update_thread_a(OLD.parent); 179 | IF OLD.parent = 0 THEN 180 | CALL delete_thread_a(OLD.num); 181 | END IF; 182 | CALL delete_post_a(OLD.timestamp, OLD.media_hash, OLD.email, OLD.name, 183 | OLD.trip); 184 | IF OLD.media_hash IS NOT NULL THEN 185 | CALL delete_image_a(OLD.media_hash); 186 | END IF; 187 | END// 188 | 189 | DELIMITER ; 190 | 191 | -- 192 | -- Populating images table with image info 193 | -- 194 | -- (About 7 minutes on /a/ with Easymodo data) 195 | INSERT INTO `a_images` ( 196 | SELECT media_hash, num, subnum, parent, preview, COUNT(*) 197 | FROM `a` WHERE parent = 0 AND media_hash IS NOT NULL AND preview IS NOT NULL 198 | GROUP BY media_hash 199 | ); 200 | 201 | -- (About 14 minutes on /a/ with Easymodo data) 202 | INSERT INTO `a_images` ( 203 | SELECT media_hash, num, subnum, parent, preview, count(*) AS replyt 204 | FROM `a` WHERE parent <> 0 AND 205 | media_hash IS NOT NULL AND preview IS NOT NULL GROUP BY media_hash 206 | ) 207 | ON DUPLICATE KEY UPDATE total = total + VALUES(total); 208 | 209 | -- 210 | -- Populating daily report table with info 211 | -- 212 | -- (About 6 minutes on /a/ with Easymodo data) 213 | INSERT INTO `a_daily` ( 214 | SELECT FLOOR(timestamp/86400)*86400 AS days, COUNT(*), 215 | SUM(media_hash IS NOT NULL), SUM(COALESCE(email = 'sage', 0)), 216 | SUM(COALESCE(name = 'Anonymous' AND trip IS NULL, 0)), 217 | SUM(trip IS NOT NULL), 218 | SUM(COALESCE(name <> 'Anonymous' AND trip IS NULL, 1)) 219 | FROM a GROUP BY days 220 | ); 221 | 222 | -- 223 | -- Populating users table with users 224 | -- 225 | -- (About 8 minutes on /a/ with Easymodo data) 226 | INSERT INTO `a_users` ( 227 | SELECT COALESCE(name, ''), '', MIN(timestamp), COUNT(*) from `a` 228 | WHERE trip IS NULL GROUP BY name 229 | ); 230 | 231 | -- (About 6 minutes on /a/ with Easymodo data) 232 | INSERT INTO `a_users` ( 233 | SELECT COALESCE(name, ''), trip, MIN(timestamp), COUNT(*) from `a` 234 | WHERE trip IS NOT NULL GROUP BY trip 235 | ); 236 | 237 | -- Add the remaining indexes 238 | CREATE INDEX total_index ON `a_images` (total); 239 | CREATE INDEX firstseen_index ON `a_users` (firstseen); 240 | CREATE INDEX postcount_index ON `a_users` (postcount); 241 | -------------------------------------------------------------------------------- /sql/r0181.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings; 5 | use utf8; 6 | 7 | # Use this to update your fuuka DB, when updating to SVN revision r181 or later. 8 | # 9 | # Recommended usage is: 10 | # perl r0181.pl > r0181.mine.sql 11 | # less r0181.mine.sql (to analyze the file) 12 | # mysql -u -p < r0181.mine.sql 13 | # 14 | # Hardcore usage: 15 | # perl r0181.pl | mysql -u -p 16 | # 17 | # There's also a static version in r0181.sql, if you don't want automatic 18 | # generation for some reason. 19 | 20 | BEGIN{-e "../board-config-local.pl" ? 21 | require "../board-config-local.pl" : require "../board-config.pl"} 22 | 23 | my $boards = BOARD_SETTINGS; 24 | my @boards = sort keys %$boards; 25 | my $charset = DB_CHARSET; 26 | 27 | print <<'HERE'; 28 | -- 29 | -- Run when upgrading to revision r181 or later 30 | -- 31 | 32 | HERE 33 | 34 | print 'USE ' . DB_DATABSE_NAME . ';'; 35 | 36 | for(@boards) { 37 | print <<"HERE"; 38 | 39 | 40 | -- 41 | -- Processing table `$_` 42 | -- 43 | 44 | ALTER TABLE `$_` CHANGE `capcode` `capcode` VARCHAR(1) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'N'; 45 | CREATE INDEX media_index ON `$_` (media_filename); 46 | HERE 47 | } 48 | -------------------------------------------------------------------------------- /sql/r0181.sql: -------------------------------------------------------------------------------- 1 | -- Run when upgrading to revision r181 or later 2 | -- 3 | -- 4 | -- Replace `a` with `board`, where board is the name of the board you are 5 | -- archiving. Copy and paste for every other board you have. 6 | -- 7 | 8 | -- 9 | -- Processing table `a` 10 | -- 11 | 12 | ALTER TABLE `a` CHANGE `capcode` `capcode` VARCHAR(1) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'N'; 13 | CREATE INDEX media_index ON `a` (media_filename); 14 | -------------------------------------------------------------------------------- /sql/r0183.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings; 5 | use utf8; 6 | 7 | # Use this to update your fuuka DB, when updating to SVN revision r183 or later. 8 | # 9 | # Recommended usage is: 10 | # perl r0183.pl > r0183.mine.sql 11 | # less r0183.mine.sql (to analyze the file) 12 | # mysql -u -p < r0183.mine.sql 13 | # 14 | # Hardcore usage: 15 | # perl r0183.pl | mysql -u -p 16 | # 17 | # There's also a static version in r0183.sql, if you don't want automatic 18 | # generation for some reason. 19 | 20 | BEGIN{-e "../board-config-local.pl" ? 21 | require "../board-config-local.pl" : require "../board-config.pl"} 22 | 23 | my $boards = BOARD_SETTINGS; 24 | my @boards = sort keys %$boards; 25 | my $charset = DB_CHARSET; 26 | 27 | print <<'HERE'; 28 | -- 29 | -- Run when upgrading to revision r183 or later 30 | -- 31 | 32 | HERE 33 | 34 | print 'USE ' . DB_DATABSE_NAME . ';'; 35 | 36 | for(@boards) { 37 | print <<"HERE"; 38 | 39 | 40 | -- 41 | -- Processing table `$_` 42 | -- 43 | 44 | CREATE INDEX capcode_index ON `$_` (capcode); 45 | HERE 46 | } 47 | -------------------------------------------------------------------------------- /sql/r0183.sql: -------------------------------------------------------------------------------- 1 | -- Run when upgrading to revision r183 or later 2 | -- 3 | -- 4 | -- Replace `a` with `board`, where board is the name of the board you are 5 | -- archiving. Copy and paste for every other board you have. 6 | -- 7 | 8 | -- 9 | -- Processing table `a` 10 | -- 11 | 12 | CREATE INDEX capcode_index ON `a` (capcode); 13 | -------------------------------------------------------------------------------- /templates.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use utf8; 5 | 6 | use constant NORMAL_HEAD_INCLUDE => <<'HERE'; 7 | 8 | 9 | 10 | <if $board_desc>/<var $board_name>/ - <var $board_desc></if><if $title> - <var $title></if> 11 | 12 | 13 | 14 | 15 | 16 | 35 | 36 | 37 | 38 |

// -

39 | HERE 40 | 41 | use constant NORMAL_FOOT_INCLUDE => <<'HERE'; 42 | 43 | 44 | HERE 45 | 46 | use constant CENTER_HEAD_INCLUDE => <<'HERE'; 47 | 48 | 49 | 50 | <var $title> 51 | 52 | 79 | 80 | 81 |
82 |
83 | HERE 84 | 85 | use constant CENTER_FOOT_INCLUDE => <<'HERE'; 86 |
87 | 88 | 89 | HERE 90 | 91 | use constant LATE_REDIRECT_INCLUDE => <<'HERE'; 92 |

93 |
94 | 95 |
96 |

Click here to be forwarded manually

Fuuka | All characters © Darkpa's party

97 | HERE 98 | 99 | use constant INDEX_INCLUDE => <<'HERE'; 100 |

Welcome to the 4chan archiver

101 |

Choose a board:

102 |

103 | 104 | //  105 | 106 |

107 | HERE 108 | 109 | use constant SIDEBAR_ADVANCED_SEARCH => <<'HERE'; 110 | 111 |
112 |
215 | HERE 216 | 217 | use constant SIDEBAR_INCLUDE => <<'HERE'.SIDEBAR_ADVANCED_SEARCH.<<'THERE'; 218 |
219 |
220 | 221 |
222 | 231 |
232 | HERE 233 | 234 |
235 |
236 | 237 | 238 | View post  239 |   240 | 241 |
242 | 243 |
244 |
245 | 246 | View page  247 |   248 |   249 | 250 | [?]In ghost mode, only threads with non-archived posts will be shown 251 | 252 | 253 |
254 | 255 |
256 |
257 | THERE 258 | 259 | use constant POST_PANEL_INCLUDE => <<'HERE'; 260 | 261 | 262 | 263 |
Delete posts
264 |
265 | 266 |
267 |
268 |
269 |
270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 287 | 288 |
Name (leave empty)
Comment (leave empty)
Name
E-mail
Subject
Comment
Password [?]Password used for file deletion.
Action 283 | 284 | 285 | 286 |
289 | HERE 290 | 291 | use constant POSTS_INCLUDE_POST_HEADER => <<'HERE'; 292 | 311 | 312 | 313 | No. 314 | 315 | No. 316 | 317 | 318 | [INTERNAL]  319 | [SPOILER]  320 | [DELETED]  321 | [STICKY]  322 | HERE 323 | 324 | use constant POSTS_INCLUDE_FILE => <<'HERE'; 325 | 326 | File: , x, 327 | 328 | [View same] [iqdb] 329 | 330 |
331 | 332 | 333 | "> 334 | 335 | 336 | <var $num>width="" height="" />
337 | 338 | 339 | 340 | ERROR 341 | 342 | HERE 343 | 344 | use constant POSTS_INCLUDE => q{ 345 |
346 |
347 | 348 | 349 | 350 | 351 |
352 | 353 | 354 | [SPOILER] 355 | [ERROR] 356 | 357 | 358 | }.POSTS_INCLUDE_FILE.qq{ 359 | 360 | }.POSTS_INCLUDE_POST_HEADER.q{ 361 | 362 | [Reply] 363 | [Last 50] 364 | [Original] 365 |

366 | 367 | replies omitted. Click Reply to view. 368 | 369 | 370 |
371 |
372 | 373 | 374 | 375 | 376 | 390 |
>> 377 | 378 | }.POSTS_INCLUDE_POST_HEADER.q{ 379 | 380 | [View] 381 | 382 |
383 | 384 | }.POSTS_INCLUDE_FILE.q{ 385 | 386 |

387 | 388 |

389 |
391 |
392 |
393 | 394 | 395 | 396 | 397 | 400 | 401 |
>> 398 | }.POST_PANEL_INCLUDE.q{ 399 |
402 |
403 |

404 |
405 | 406 |
407 |
408 | }; 409 | 410 | use constant SEARCH_INCLUDE => <<'HERE'.POSTS_INCLUDE.<<'THERE'; 411 | 412 | HERE 413 | 414 | 415 | No posts found 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 |
Navigation
View posts[]
430 | THERE 431 | 432 | use constant REPORT_LIST_INCLUDE => <<'HERE'; 433 | 434 | 435 | 436 | 437 |
438 | HERE 439 | 440 | use constant REPORT_HEADER_INCLUDE => <<'HERE'; 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 |
Query
UpdatedReal-time
Updated. Next update
Actions[Update]
451 | HERE 452 | 453 | use constant REPORT_THUMBS_INCLUDE => <<'HERE'; 454 |
455 | 456 | 457 |
458 | 459 | 460 | 461 | 462 | 463 | 464 | 469 | 470 | 471 | 472 |
<var $file>
465 | 466 | : 467 | 468 |
473 |
474 | 475 |
476 |
477 |
478 | HERE 479 | 480 | use constant REPORT_TABLE_INCLUDE => <<'HERE'; 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 500 | 501 | 502 | 503 | 504 | 505 |
<var $file> 496 | 497 | 498 | 499 |
506 |
507 | HERE 508 | 509 | use constant REPORT_GRAPH_INCLUDE => <<'HERE'; 510 |
" alt="" />
511 |
512 | HERE 513 | 514 | 1; 515 | --------------------------------------------------------------------------------