File Coverage

blib/lib/Slash/OurNet.pm
Criterion Covered Total %
statement 63 66 95.4
branch 1 2 50.0
condition 1 6 16.6
subroutine 22 23 95.6
pod n/a
total 87 97 89.6


line stmt bran cond sub pod time code
1             package Slash::OurNet;
2 1     1   7032 use 5.006;
  1         4  
  1         58  
3              
4             our $VERSION = '1.41';
5              
6 1     1   5 use strict;
  1         2  
  1         38  
7 1     1   6 use warnings;
  1         5  
  1         49  
8 1     1   5 no warnings qw(once redefine);
  1         1  
  1         35  
9              
10 1     1   934 use Date::Parse;
  1         9975  
  1         166  
11 1     1   1059 use Date::Format;
  1         3435  
  1         76  
12 1     1   10 use File::Spec;
  1         1  
  1         21  
13 1     1   9 use File::Basename;
  1         2  
  1         124  
14 1     1   1112 use Lingua::ZH::Wrap;
  1         22854  
  1         205  
15              
16 1   33 1   11 use constant PATH => File::Spec->rel2abs(dirname($ENV{SCRIPT_FILENAME} or $0));
  1         2  
  1         191  
17 1     1   7 use constant SLASH => $ENV{SLASH_USER};
  1         2  
  1         62  
18              
19 1     1   1052 use lib PATH . '/lib';
  1         4100  
  1         9  
20 1     1   167 use base 'Locale::Maketext';
  1         2  
  1         1973  
21             use Locale::Maketext::Lexicon {
22 1         14 en => [ Gettext => PATH . '/po/en.po' ],
23             zh_tw => [ Gettext => PATH . '/po/zh_tw.po' ],
24             zh_cn => [ Gettext => PATH . '/po/zh_cn.po' ],
25 1     1   24660 };
  1         6193  
26              
27 1     1   10768 use if SLASH, 'Slash';
  1         22  
  1         14  
28 1     1   66 use if SLASH, 'Slash::DB';
  1         3  
  1         4  
29 1     1   39 use if SLASH, 'Slash::Display';
  1         2  
  1         4  
30 1     1   43 use if SLASH, 'base' => qw(Slash::DB::Utility Slash::DB::MySQL);
  1         2  
  1         4  
31 1     1   39 use if !SLASH, 'Slash::OurNet::Standalone';
  1         2  
  1         5  
32             Slash::OurNet::Standalone->import unless SLASH;
33              
34             $Lingua::ZH::Wrap::columns = 75;
35             $OurNet::BBS::Client::NoCache = 1; # avoids bloat
36              
37             our %Lexicon = ( _AUTO => 1 );
38             our ($TopClass, $MailBox, $Organization, @Connection, $SecretSigils,
39             $BoardPrefixLength, $GroupPrefixLength, $Strip_ANSI, $Use_RealEmail,
40             $Thread_Prev, $Date_Prev, $Thread_Next, $Date_Next, $Language, $Colors,
41             $DefaultUser, %CachedTop, $LanguageHandle, $SourceEncoding, $Theme,
42             $TrappedExcept, $ALLBBS);
43              
44             sub loc {
45 1     1   399 use Encode;
  1         2  
  1         172  
46 0   0 0     return decode_utf8(($LanguageHandle ||= __PACKAGE__->get_handle($Language))->maketext(@_));
47             }
48              
49 1 50   1   45 BEGIN { do(PATH . '/ournet.conf'); die $@ if $@ }
  1         36  
50              
51 1     1   1946 use OurNet::BBS $SourceEncoding;
  0            
  0            
52              
53             sub new {
54             return unless @_; # to satisfy pudge's automation scripts
55             my ($class, $name) = splice(@_, 0, 2);
56              
57             no warnings 'once';
58             my $self = {
59             bbs => OurNet::BBS->new(@_),
60             virtual_user => $name,
61             };
62              
63             return bless($self, $class);
64             }
65              
66             sub article_save {
67             my ($self, $group, $board, $child, $artid, $reply,
68             $title, $body, $state, $name, $nick) = @_;
69              
70             $child ||= 'articles';
71             my $artgrp = $self->{bbs}{boards}{$board};
72              
73             $body = ($body);
74              
75             # honor 75-column tradition of legacy BBS systems
76             $body = wrap('','', $body) if $body and length($body) > 75;
77              
78             no warnings 'uninitialized';
79             my $offset = sprintf("%+0.4d", getCurrentUser('off_set') / 36);
80             $offset =~ s/([1-9][0-9]|[0-9][1-9])$/$1 * 0.6/e;
81              
82             if ($Use_RealEmail and SLASH) {
83             $name = getCurrentUser('realemail');
84             $nick = getCurrentUser('nickname');
85             }
86              
87             # we could ignore the $reply until a Reply-To header is supported
88             my $article = {
89             header => {
90             From => "$name ($nick)",
91             Subject => $title || '',
92             Board => $board,
93             Date => timeCalc(
94             scalar localtime, "%a %b %e %H:%M:%S $offset %Y"
95             ),
96             },
97             body => $body || '',
98             };
99              
100             my $error; # error message
101              
102             $error .= loc('Please enter a subject.
')
103             unless (length($article->{header}{Subject}));
104             $error = ' ' unless $state;
105              
106             $PerlIO::via::trap::PASS = 1 if $TrappedExcept eq 'post';
107             $artgrp->{articles}{$artid || ''} = $article unless $error;
108             $PerlIO::via::trap::PASS = 0 if $TrappedExcept eq 'post';
109              
110             return ($article, $error);
111             }
112              
113             sub article {
114             my ($self, $group, $board, $child, $artid, $reply) = @_;
115             my (@related, $artgrp, $is_reply);
116              
117             # put $reply to $name and set flag for further processing
118             $is_reply++ if !defined($artid) and defined($artid = $reply);
119             return unless defined $artid; # happens when a new article's made
120              
121             $child ||= 'articles';
122             $artgrp = $self->{bbs}{($child eq 'mailbox') ? 'users' : 'boards'}{$board};
123              
124             foreach my $chunk (split('/', $child)) {
125             $artgrp = ($chunk =~ /^\d+$/ ? $artgrp->[$chunk] : $artgrp->{$chunk});
126             }
127              
128             # number OR name
129             my $article = ($artid =~ /^\d+$/ ? $artgrp->[$artid] : $artgrp->{$artid});
130              
131             my $related = ($is_reply || $child =~ m/^archives/) ? [] : $self->related_articles(
132             [ group => $group, board => $board, child => $child ],
133             $artgrp, $article,
134             ); # do not calculate related article during reply
135              
136             return ($self->mapArticle(
137             $group, $board, $child, $artid, $article, $is_reply
138             ), $related);
139             }
140              
141             sub related_articles {
142             my ($self, $params, $artgrp, $article) = @_;
143             return unless $article;
144              
145             my $header = $article->{header};
146             my $recno = $article->recno;
147             my $size = $#{$artgrp};
148             my $title = $header->{Subject};
149             my $related = [];
150              
151             $title = "Re: $title" unless substr($title, 0, 4) eq 'Re: ';
152              
153             my %cache;
154              
155             # grepping for thread_prev
156             if ($Thread_Prev) { foreach my $i (reverse(($recno - 5) .. ($recno - 1))) {
157             next if $i < 0;
158             my $art = $artgrp->[$i];
159             my $title2 = $art->{header}->{Subject};
160             next unless $title eq $title2 or $title eq "Re: $title2";
161             pushy(\%cache, $related, $params, $Thread_Prev, $art);
162             last;
163             } }
164              
165             pushy(\%cache, $related, $params, $Date_Prev, $artgrp->[$recno - 1])
166             if $Date_Prev and $recno;
167              
168             if ($Thread_Next) { foreach my $i (($recno + 1) .. ($recno + 5)) {
169             next if $i > $size - 1;
170             my $art = $artgrp->[$i];
171             my $title2 = $art->{header}{Subject};
172             next unless $title eq $title2 or $title eq "Re: $title2";
173             pushy(\%cache, $related, $params, $Thread_Next, $art);
174             last;
175             } }
176              
177             pushy(\%cache, $related, $params, $Date_Next, $artgrp->[$recno + 1])
178             if $Date_Next and $recno < $size - 1;
179              
180             return $related;
181             }
182              
183             sub pushy {
184             my ($cache, $self, $params, $relation, $art) = @_;
185             return unless defined $art;
186              
187             my $name = $art->name;
188             return if $cache->{$name}++;
189             my $header = $art->{header};
190             my $author = $art->{author};
191             $author =~ s/(?:\.\.?bbs)?\@.+/\./;
192              
193             push @{$self}, {
194             @{$params},
195             relation => $relation, name => $name, header => $header,
196             author => $author
197             } unless $params->[5] ne 'articles' and $art->REF =~ /Group/;
198             }
199              
200             sub board {
201             my ($self, $group, $board, $child, $begin) = @_;
202             my ($artgrp, $bm, $title, $etc);
203              
204             my $PageSize = 20;
205             if ($child eq 'mailbox') {
206             $artgrp = $self->{bbs}{users}{$board};
207             $bm = $board;
208             $title = $MailBox;
209             $etc = '';
210             }
211             else {
212             $artgrp = $self->{bbs}{boards}{$board};
213             $bm = $artgrp->{bm};
214             $title = $artgrp->{title};
215             if ($etc = $artgrp->{etc_brief}) {
216             $etc = (split(/\n\n+/, $etc, 2))[1];
217             $etc =~ s/\x1b\[[\d\;]*m//g;
218             $etc =~ s/\n+/
/g;
219             }
220             }
221            
222             die "no such board" unless $artgrp;
223             return unless $artgrp;
224              
225             die "permission denied"
226             if $child ne 'mailbox' and $SecretSigils and
227             index(substr($artgrp->{title}, 0, index($artgrp->{title}, ' ')), $SecretSigils) > -1;
228              
229             foreach my $chunk (split('/', $child)) {
230             $artgrp = $artgrp->{$chunk};
231             }
232              
233             die "permission denied"
234             if $artgrp->can('readlevel') and $artgrp->readlevel and $artgrp->readlevel ne 4294967295;
235              
236             my $reversed = ($child eq 'articles' or $child eq 'mailbox');
237             my $size = eval { $#{$artgrp} } || 0;
238              
239             $begin = $reversed ? ($size - $PageSize + 1) : 0
240             unless defined $begin;
241              
242             my @pages;
243              
244             foreach my $page (1..(int($size / $PageSize)+1)) {
245             my $thisbegin = $reversed
246             ? ($size - ($page * $PageSize) + 1)
247             : (($page - 1) * $PageSize + 1);
248             my $iscurpage = ($thisbegin == $begin);
249             push @pages, {
250             number => $page,
251             begin => $thisbegin,
252             iscurpage => $iscurpage,
253             };
254             }
255              
256             $size = $begin + $PageSize - 1 if ($begin + $PageSize - 1 <= $size);
257             $begin = 0 if $begin < 0;
258              
259             my $message = "| $board | ".
260             (($artgrp->name or $child eq 'mailbox')
261             ? $title : substr($title, $BoardPrefixLength)).
262             " | $bm |
";
263             $message .= $etc if defined $etc;
264              
265             my @range = $reversed
266             ? reverse ($begin .. $size) : ($begin.. $size);
267              
268             local $_;
269             return ($message, ($#pages ? \@pages : undef), $self->mapArticles(
270             $group, $board, $child, \@range,
271             map { eval { $artgrp->[$_] } || 0 } @range
272             ));
273             }
274              
275             sub group {
276             my ($self, $group, $board) = @_;
277             my $boards;
278            
279             $self->{bbs}{groups}->toplevel($TopClass);
280              
281             if ($board eq $TopClass) {
282             $boards = $self->{bbs}{groups};
283             }
284             else {
285             # $group =~ s|^\Q$TopClass\E/?||;
286             $boards = ($group =~ m!^\Q$TopClass\E/?$!)
287             ? $self->{bbs}{groups}{$board}
288             : $self->{bbs}{groups}{$group}{$board};
289             }
290              
291             my ($thisgroup, $title, $bm, $etc);
292              
293             if ($board eq $TopClass) {
294             $bm = 'SYSOP';
295             $title = 'All Boards';
296             }
297             elsif ($title = $boards->{title}) {
298             $bm = $boards->{bm}; # XXX!
299             $bm = $boards->{owner};
300             $etc = $self->{bbs}{boards}{$board}{etc_brief}
301             if exists $self->{bbs}{boards}{$board};
302             }
303              
304             local $_;
305             $title ||= '';
306             my $title2 = substr($title, $GroupPrefixLength);
307             $title2 =~ s|^[^/]+/\s+||; # XXX: melix special case
308              
309             my $message = "| $board | ".
310             ( $title2 || $Organization) .
311             ($bm ? " | $bm |
" : ' |
');
312             $message .= $etc if defined $etc;
313              
314             # $boards->refresh;
315              
316             return ($self->mapBoards(
317             "$group/$board",
318             map {
319             $boards->{$_}
320             } sort {
321             uc($a) cmp uc($b)
322             } grep {
323             $_ !~ /^(?:owner|id|title)$/
324             } keys (%{$boards}),
325             ), $message);
326             }
327              
328             sub top {
329             my $self = shift;
330              
331             # XXX kludge!
332             my $top = $self->{bbs}{files}{'@-day'} || $self->{bbs}{files}{day};
333             my $brds = $self->{bbs}{boards};
334             my @ret;
335            
336             if (($self->{top} || '') eq $top) {
337             @ret = $CachedTop{"@Connection"};
338             }
339             else { while (
340             $top =~ s/^.*?32m([^\s]+).*?33m\s*([^\s]+)\n.*?37m\s*([^\x1b]+?)\x20*\x1b//m
341             ) {
342             my ($board, $author, $title) = ($1, $2, $3);
343             my $artgrp = $brds->{$board}{articles};
344              
345             foreach my $art (reverse(0..$#{$artgrp})) {
346             my $article = $artgrp->[$art];
347             next unless ($article->{title} eq $title);
348             push @ret, $article;
349             last;
350             }
351             }
352             $CachedTop{"@Connection"} = \@ret;
353             $self->{top} = $top;
354             }
355              
356             return $self->mapArticles($TopClass, '', 'articles', [], @ret);
357             }
358              
359             sub mapArticles {
360             my ($self, $group, $board, $child, $range) = splice(@_, 0, 5);
361              
362             local $_;
363             return [ map {
364             my $recno = shift(@{$range});
365             my ($type, $title, $date, $author, $board, $artid);
366              
367             if (UNIVERSAL::isa($_, 'UNIVERSAL')) {
368             $type = ($_->REF =~ /Group/) ? 'group' : 'article';
369             $title = $_->{title};
370             $title =~ s/\x1b\[[\d\;]*m//g;
371             $date = $_->{date},
372             $author = $_->{author};
373             $author =~ s/(?:\.bbs)?\@.+//;
374             $board = $board || $_->board;
375             $artid = $_->name;
376             }
377             else { # deleted article
378             $type = 'deleted';
379             $title = loc('<< This article has been deleted >>');
380             $board = $board;
381             $author = ' ';
382             $date = ' ';
383             }
384              
385             {
386             title => $title,
387             child => $child,
388             group => $group,
389             type => $type,
390             date => $date,
391             author => $author,
392             board => $board,
393             name => $artid,
394             recno => $recno,
395             articles_count => $type eq 'group' ? $#{$_} : 1,
396             }
397             } @_ ];
398             }
399              
400             sub mapArticle {
401             my ($self, $group, $board, $child, $artid, $article, $is_reply) = @_;
402             my $header = { %{$article->{header} || {}} };
403             my $title = $header->{Subject} || '(untitled)';
404             $header->{Subject} =~ s/\x1b\[[\d\;]*m//g;
405              
406             return {
407             body => txt2html($article, $is_reply),
408             header => $header,
409             title => $title,
410             board => $board,
411             group => $group,
412             child => $child,
413             name => $artid,
414             };
415             }
416              
417             sub mapBoards {
418             my $self = shift;
419             my $group = shift;
420              
421             my (@group, @board);
422             local $_;
423             no strict 'refs';
424              
425             foreach (@_) {
426             my $type = 'board';
427             my $board;
428             my $etc;
429             my ($title, $date, $bm);
430              
431             if ($_->REF =~ /Group$/) {
432             $title = $_->{title} or next;
433              
434             $board = $1 if $title =~ s|^([^/]+)/\s+||;
435             $bm = $_->{owner};
436             $bm = '' if $bm =~ /\W/; # XXX melix 0.8 bug
437              
438             $type = 'group';
439             }
440             else {
441             $board = $_->board or next;
442             if ($etc = $_->{etc_brief}) {
443             $etc = (split(/\n\n+/, $etc, 2))[1];
444             $etc =~ s/\n+/\n/g;
445             }
446             $bm = $_->{bm},
447             $title = substr($_->{title}, $BoardPrefixLength) or next;
448             }
449              
450             next if $SecretSigils and index(substr($_->{title}, 0, index($_->{title}, ' ')), $SecretSigils) > -1;
451              
452             next if $TopClass and $board eq $TopClass;
453              
454             my $entry = {
455             title => $title,
456             bm => $bm,
457             etc_brief => $etc,
458             group => $group,
459             board => $board,
460             type => $type,
461             # archives_count => (
462             # ($type eq 'group') ? '' : $#{$_->{archives}}
463             # ),
464             # articles_count => (
465             # ($type eq 'group') ? ' ' : $#{$_->{articles}}
466             # ),
467             };
468              
469             if ($type eq 'group') {
470             push @group, $entry;
471             }
472             else {
473             push @board, $entry;
474             }
475             }
476              
477             return [ @group, @board ];
478             }
479              
480             sub txt2html {
481             my ($article, $is_reply) = @_;
482              
483             # reply mode decorations
484             my $body = $article->{body};
485              
486             if ($is_reply) {
487             $body =~ s/^(.+)\n+--+\n.+/$1/sg;
488             $body =~ s/\n+/\n: /g;
489             $body =~ s/\n: : : .*//g;
490             $body =~ s/\n: : ※ .*//g;
491             $body =~ s/: \n+/\n/g;
492             $body = sprintf(loc("*) %s wrote:")."\n: %s", $article->{header}{From}, $body);
493             }
494             elsif ($Strip_ANSI) {
495             require HTML::FromText;
496              
497             $body =~ s/\x1b\[.*?[mJH]//g;
498             $body = HTML::FromText::text2html(
499             $body,
500             metachars => 1, urls => 1,
501             email => 1, underline => 1,
502             lines => 1, spaces => 1,
503             );
504              
505             $body =~ s/
506             $body =~ s/<\/TT>//g;
507              
508             $body = << ".";
509            
510             $body
511            
512             .
513             }
514             else {
515             require HTML::FromANSI;
516             $body = HTML::FromANSI::ansi2html($body);
517             }
518              
519             return $body;
520             }
521              
522             1;
523              
524             __END__