diff options
author | Yorhel <git@yorhel.nl> | 2019-12-09 15:18:34 +0100 |
---|---|---|
committer | Yorhel <git@yorhel.nl> | 2019-12-12 15:48:35 +0100 |
commit | 2c4203b57652e0c3fdc9bd10973754e911f43b36 (patch) | |
tree | f31e23c6375996d1a413200bcf90cd0624019265 /lib | |
parent | 5075f0ef4573fa95252c1a91b62239cc9b6347bb (diff) |
v2rw: Discussion board editing & thread creation
Now with BBCode preview, interactive board search, client-side error
reporting and lots of new bugs.
This took me far too long, turns out it wasn't such a trivial rewrite.
Diffstat (limited to 'lib')
-rw-r--r-- | lib/VNDB/DB/Discussions.pm | 161 | ||||
-rw-r--r-- | lib/VNDB/Handler/Discussions.pm | 239 | ||||
-rw-r--r-- | lib/VNWeb/Discussions/Board.pm | 3 | ||||
-rw-r--r-- | lib/VNWeb/Discussions/Edit.pm | 159 | ||||
-rw-r--r-- | lib/VNWeb/Discussions/JS.pm | 45 | ||||
-rw-r--r-- | lib/VNWeb/Discussions/Lib.pm | 26 | ||||
-rw-r--r-- | lib/VNWeb/Discussions/Thread.pm | 6 | ||||
-rw-r--r-- | lib/VNWeb/Elm.pm | 14 | ||||
-rw-r--r-- | lib/VNWeb/Validation.pm | 2 |
9 files changed, 241 insertions, 414 deletions
diff --git a/lib/VNDB/DB/Discussions.pm b/lib/VNDB/DB/Discussions.pm index 77af72fb..442f8032 100644 --- a/lib/VNDB/DB/Discussions.pm +++ b/lib/VNDB/DB/Discussions.pm @@ -5,7 +5,7 @@ use strict; use warnings; use Exporter 'import'; -our @EXPORT = qw|dbThreadGet dbThreadEdit dbThreadAdd dbPostGet dbPostEdit dbPostAdd dbThreadCount dbPollStats dbPollVote|; +our @EXPORT = qw|dbThreadGet dbPostGet|; # Options: id, type, iid, results, page, what, asuser, notusers, search, sort, reverse @@ -120,93 +120,6 @@ sub dbThreadGet { } -# id, %options->( title locked hidden private boards poll_question poll_max_options poll_preview poll_recast poll_options } -# The poll_{question,options,max_options} fields should not be set when there -# are no changes to the poll info. Either all or none of these fields should be -# set. -sub dbThreadEdit { - my($self, $id, %o) = @_; - - my %set = ( - 'title = ?' => $o{title}, - 'locked = ?' => $o{locked}?1:0, - 'hidden = ?' => $o{hidden}?1:0, - 'private = ?' => $o{private}?1:0, - 'poll_preview = ?' => $o{poll_preview}?1:0, - 'poll_recast = ?' => $o{poll_recast}?1:0, - exists $o{poll_question} ? ( - 'poll_question = ?' => $o{poll_question}||undef, - 'poll_max_options = ?' => $o{poll_max_options}||1, - ) : (), - ); - - $self->dbExec(q| - UPDATE threads - !H - WHERE id = ?|, - \%set, $id); - - if($o{boards}) { - $self->dbExec('DELETE FROM threads_boards WHERE tid = ?', $id); - $self->dbExec(q| - INSERT INTO threads_boards (tid, type, iid) - VALUES (?, ?, ?)|, - $id, $_->[0], $_->[1]||0 - ) for (@{$o{boards}}); - } - - if(exists $o{poll_question}) { - $self->dbExec('DELETE FROM threads_poll_options WHERE tid = ?', $id); - $self->dbExec(q| - INSERT INTO threads_poll_options (tid, option) - VALUES (?, ?)|, - $id, $_ - ) for (@{$o{poll_options}}); - } -} - - -# %options->{ title hidden locked private boards poll_stuff } -sub dbThreadAdd { - my($self, %o) = @_; - - my $id = $self->dbRow(q| - INSERT INTO threads (title, hidden, locked, private, poll_question, poll_max_options, poll_preview, poll_recast) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - RETURNING id|, - $o{title}, $o{hidden}?1:0, $o{locked}?1:0, $o{private}?1:0, $o{poll_question}||undef, $o{poll_max_options}||1, $o{poll_preview}?1:0, $o{poll_recast}?1:0 - )->{id}; - - $self->dbExec(q| - INSERT INTO threads_boards (tid, type, iid) - VALUES (?, ?, ?)|, - $id, $_->[0], $_->[1]||0 - ) for (@{$o{boards}}); - - $self->dbExec(q| - INSERT INTO threads_poll_options (tid, option) - VALUES (?, ?)|, - $id, $_ - ) for ($o{poll_question} ? @{$o{poll_options}} : ()); - - return $id; -} - - -# Returns thread count of a specific item board -# Arguments: type, iid -sub dbThreadCount { - my($self, $type, $iid) = @_; - return $self->dbRow(q| - SELECT COUNT(*) AS cnt - FROM threads_boards tb - JOIN threads t ON t.id = tb.tid - WHERE tb.type = ? AND tb.iid = ? - AND t.hidden = FALSE AND t.private = FALSE|, - $type, $iid)->{cnt}; -} - - # Options: tid, num, what, uid, mindate, hide, search, type, page, results, sort, reverse # what: user thread sub dbPostGet { @@ -260,76 +173,4 @@ sub dbPostGet { return wantarray ? ($r, $np) : $r; } - -# tid, num, %options->{ num msg hidden lastmod } -sub dbPostEdit { - my($self, $tid, $num, %o) = @_; - - my %set = ( - 'msg = ?' => $o{msg}, - 'edited = to_timestamp(?)' => $o{lastmod}, - 'hidden = ?' => $o{hidden}?1:0, - ); - - $self->dbExec(q| - UPDATE threads_posts - !H - WHERE tid = ? - AND num = ?|, - \%set, $tid, $num - ); -} - - -# tid, %options->{ uid msg } -sub dbPostAdd { - my($self, $tid, %o) = @_; - - my $num = $self->dbRow('SELECT num FROM threads_posts WHERE tid = ? ORDER BY num DESC LIMIT 1', $tid)->{num}; - $num = $num ? $num+1 : 1; - $o{uid} ||= $self->authInfo->{id}; - - $self->dbExec(q| - INSERT INTO threads_posts (tid, num, uid, msg) - VALUES(?, ?, ?, ?)|, - $tid, $num, @o{qw| uid msg |} - ); - $self->dbExec(q| - UPDATE threads - SET count = count+1 - WHERE id = ?|, - $tid); - - return $num; -} - - -# Args: tid -# Returns: num_users, poll_stats, user_voted_options -sub dbPollStats { - my($self, $tid) = @_; - my $uid = $self->authInfo->{id}; - - my $num_users = $self->dbRow('SELECT COUNT(DISTINCT uid) AS votes FROM threads_poll_votes WHERE tid = ?', $tid)->{votes} || 0; - - my $stats = !$num_users ? {} : { map +($_->{optid}, $_->{votes}), @{$self->dbAll( - 'SELECT optid, COUNT(optid) AS votes FROM threads_poll_votes WHERE tid = ? GROUP BY optid', $tid - )} }; - - my $user = !$num_users || !$uid ? [] : [ - map $_->{optid}, @{$self->dbAll('SELECT optid FROM threads_poll_votes WHERE tid = ? AND uid = ?', $tid, $uid)} - ]; - - return $num_users, $stats, $user; -} - - -sub dbPollVote { - my($self, $tid, $uid, @opts) = @_; - - $self->dbExec('DELETE FROM threads_poll_votes WHERE tid = ? AND uid = ?', $tid, $uid); - $self->dbExec('INSERT INTO threads_poll_votes (tid, uid, optid) VALUES (?, ?, ?)', - $tid, $uid, $_) for @opts; -} - 1; diff --git a/lib/VNDB/Handler/Discussions.pm b/lib/VNDB/Handler/Discussions.pm deleted file mode 100644 index b10e7aaf..00000000 --- a/lib/VNDB/Handler/Discussions.pm +++ /dev/null @@ -1,239 +0,0 @@ - -package VNDB::Handler::Discussions; - -use strict; -use warnings; -use TUWF ':html', 'xml_escape'; -use POSIX 'ceil'; -use VNDB::Func; -use VNDB::Types; - - -TUWF::register( - qr{t([1-9]\d*)/reply} => \&edit, - qr{t([1-9]\d*)\.([1-9]\d*)/edit} => \&edit, - qr{t/(db|an|ge|[vpu])([1-9]\d*)?/new} => \&edit, -); - - -sub caneditpost { - my($self, $post) = @_; - return $self->authCan('boardmod') || - ($self->authInfo->{id} && $post->{user_id} == $self->authInfo->{id} && !$post->{hidden} && time()-$post->{date} < $self->{board_edit_time}) -} - - -# Arguments, action -# tid reply -# tid, 1 edit thread -# tid, num edit post -# type, (iid) start new thread -sub edit { - my($self, $tid, $num) = @_; - $num ||= 0; - - # in case we start a new thread, parse boards - my $board = ''; - if($tid !~ /^\d+$/) { - return $self->resNotFound if $tid =~ /(db|an|ge)/ && $num || $tid =~ /[vpu]/ && !$num; - $board = $tid.($num||''); - $tid = 0; - $num = 0; - } - - # get thread and post, if any - my $t = $tid && $self->dbThreadGet(id => $tid, what => 'boards poll')->[0]; - return $self->resNotFound if $tid && !$t->{id}; - - my $p = $num && $self->dbPostGet(tid => $tid, num => $num, what => 'user')->[0]; - return $self->resNotFound if $num && !$p->{num}; - - # are we allowed to perform this action? - return $self->htmlDenied if !$self->authCan('board') - || ($tid && ($t->{locked} || $t->{hidden}) && !$self->authCan('boardmod')) - || ($num && !caneditpost($self, $p)); - - # check form etc... - my $frm; - if($self->reqMethod eq 'POST') { - return if !$self->authCheckCode; - my $haspoll = $self->reqPost('poll') && 1; - $frm = $self->formValidate( - !$tid || $num == 1 ? ( - { post => 'title', maxlength => 50 }, - { post => 'boards', maxlength => 200 }, - $haspoll ? ( - { post => 'poll', required => 0 }, - { post => 'poll_question', required => 1, maxlength => 100 }, - { post => 'poll_options', required => 1, maxlength => 100*$self->{poll_options} }, - { post => 'poll_max_options', required => 1, default => 1, template => 'uint', min => 1, max => $self->{poll_options} }, - { post => 'poll_preview', required => 0 }, - { post => 'poll_recast', required => 0 }, - ) : (), - ) : (), - $self->authCan('boardmod') ? ( - { post => 'locked', required => 0 }, - { post => 'hidden', required => 0 }, - { post => 'nolastmod', required => 0 }, - ) : (), - $self->authCan('boardmod') || $self->authCan('dbmod') || $self->authCan('tagmod') ? ( - { post => 'private', required => 0 }, - ) : (), - { post => 'msg', maxlength => 32768 }, - { post => 'fullreply', required => 0 }, - ); - - $frm->{_err} = 1 if $frm->{fullreply}; - - # check for double-posting - push @{$frm->{_err}}, 'Please wait 30 seconds before making another post' if !$num && !$frm->{_err} && $self->dbPostGet( - uid => $self->authInfo->{id}, tid => $tid, mindate => time - 30, results => 1, $tid ? () : (num => 1))->[0]{num}; - - # Don't allow regular users to create more than 5 threads a day - push @{$frm->{_err}}, 'You can only create 5 threads every 24 hours' if - !$tid && !$self->authCan('boardmod') && - @{$self->dbPostGet(uid => $self->authInfo->{id}, mindate => time - 24*3600, num => 1)} >= 5; - - # parse and validate the boards - my @boards; - if(!$frm->{_err} && $frm->{boards}) { - for (split /[ ,]/, $frm->{boards}) { - my($ty, $id) = /^([a-z]{1,2})([0-9]*)$/ ? ($1, $2) : ($_, ''); - push @boards, [ $ty, $id ] if !grep $_->[0].$_->[1] eq $ty.$id, @boards; - my $bt = $BOARD_TYPE{$ty}; - push @{$frm->{_err}}, "Wrong board: $_" if - !$ty || !$bt - || !$self->authCan($bt->{post_perm}) - || !$bt->{dbitem} && $id || $bt->{dbitem} && !$id - || $ty eq 'v' && !$self->dbVNGet(id => $id)->[0]{id} - || $ty eq 'p' && !$self->dbProducerGet(id => $id)->[0]{id} - || $ty eq 'u' && !$self->dbUserGet(uid => $id)->[0]{id}; - } - } - - # validate poll options - my @poll_options; - if(!$frm->{_err} && $haspoll) { - @poll_options = split /\s*\n\s*/, $frm->{poll_options}; - push @{$frm->{_err}}, [ 'poll_options', 'mincount', 2 ] if @poll_options < 2; - push @{$frm->{_err}}, [ 'poll_options', 'maxcount', $frm->{poll_max_options} ] if @poll_options > $self->{poll_options}; - push @{$frm->{_err}}, [ 'poll_max_options', 'template', 'uint' ] if @poll_options > 1 && @poll_options < $frm->{poll_max_options}; - } - - if(!$frm->{_err}) { - my($ntid, $nnum) = ($tid, $num); - - # create/edit thread - if(!$tid || $num == 1) { - my $pollchange = $haspoll && (!$t - || ($t->{poll_question}||'') ne $frm->{poll_question} - || $t->{poll_max_options} != $frm->{poll_max_options} - || join("\n", map $_->[1], @{$t->{poll_options}}) ne join("\n", @poll_options) - ); - my %thread = ( - title => $frm->{title}, - boards => \@boards, - hidden => $frm->{hidden}, - locked => $frm->{locked}, - private => $frm->{private}, - poll_preview => $frm->{poll_preview}||0, - poll_recast => $frm->{poll_recast}||0, - !$haspoll ? ( - poll_question => undef # Make sure any existing poll gets deleted - ) : $pollchange ? ( - poll_question => $frm->{poll_question}, - poll_max_options => $frm->{poll_max_options}, - poll_options => \@poll_options - ) : (), - ); - $self->dbThreadEdit($tid, %thread) if $tid; - $ntid = $self->dbThreadAdd(%thread) if !$tid; - } - - # create/edit post - my %post = ( - msg => $self->bbSubstLinks($frm->{msg}), - hidden => $num != 1 && $frm->{hidden}, - lastmod => !$num || $frm->{nolastmod} ? 0 : time, - ); - $self->dbPostEdit($tid, $num, %post) if $num; - $nnum = $self->dbPostAdd($ntid, %post) if !$num; - - return $self->resRedirect(VNWeb::Discussions::Lib::post_url($ntid, $nnum, 'last'), 'post'); - } - } - - # fill out form if we have some data - if($p) { - $frm->{msg} ||= $p->{msg}; - $frm->{hidden} = $p->{hidden} if $num != 1 && !exists $frm->{hidden}; - if($num == 1) { - $frm->{boards} ||= join ' ', sort map $_->[1]?$_->[0].$_->[1]:$_->[0], @{$t->{boards}}; - $frm->{title} ||= $t->{title}; - $frm->{locked} //= $t->{locked}; - $frm->{hidden} //= $t->{hidden}; - $frm->{private} //= $t->{private}; - if($t->{haspoll}) { - $frm->{poll} //= 1; - $frm->{poll_question} ||= $t->{poll_question}; - $frm->{poll_max_options} ||= $t->{poll_max_options}; - $frm->{poll_preview} //= $t->{poll_preview}; - $frm->{poll_recast} //= $t->{poll_recast}; - $frm->{poll_options} ||= join "\n", map $_->[1], @{$t->{poll_options}}; - } - } - } - delete $frm->{_err} unless ref $frm->{_err}; - $frm->{boards} ||= $board.($board =~ /^u/ ? ' u'.$self->authInfo->{id} : ''); - $frm->{title} ||= $self->reqGet('title'); - $frm->{poll_preview} //= 1; - $frm->{poll_max_options} ||= 1; - - # generate html - my $url = !$tid ? "/t/$board/new" : !$num ? "/t$tid/reply" : "/t$tid.$num/edit"; - my $title = !$tid ? 'Start new thread' : - !$num ? "Reply to $t->{title}" : - 'Edit post'; - $self->htmlHeader(title => $title, noindex => 1); - $self->htmlForm({ frm => $frm, action => $url }, 'postedit' => [$title, - [ static => label => 'Username', content => sub { VNWeb::HTML::user_($p || VNWeb::Auth::auth->user); '' } ], - !$tid || $num == 1 ? ( - [ input => short => 'title', name => 'Thread title' ], - [ input => short => 'boards', name => 'Board(s)' ], - [ static => content => 'Read <a href="/d9#2">d9#2</a> for information about how to specify boards.' ], - $self->authCan('boardmod') ? ( - [ check => name => 'Locked', short => 'locked' ], - ) : (), - $self->authCan('boardmod') || $self->authCan('dbmod') || $self->authCan('tagmod') ? ( - [ check => name => 'Private (only visible to users mentioned in the boards)', short => 'private' ], - ) : (), - ) : ( - [ static => label => 'Topic', content => qq|<a href="/t$tid">|.xml_escape($t->{title}).'</a>' ], - ), - $self->authCan('boardmod') ? ( - [ check => name => 'Hidden', short => 'hidden' ], - $num ? ( - [ check => name => 'Don\'t update last modified field', short => 'nolastmod' ], - ) : (), - ) : (), - [ text => name => 'Message<br /><b class="standout">English please!</b>', short => 'msg', rows => 25, cols => 75 ], - [ static => content => 'See <a href="/d9#3">d9#3</a> for the allowed formatting codes' ], - (!$tid || $num == 1) ? ( - [ static => content => '<br />' ], - [ check => short => 'poll', name => 'Add poll' ], - $num && $frm->{poll_question} ? ( - [ static => content => '<b class="standout">All votes will be reset if any changes to the poll fields are made!</b>' ] - ) : (), - [ input => short => 'poll_question', name => 'Poll question', width => 250 ], - [ text => short => 'poll_options', name => "Poll options<br /><i>one per line,<br />$self->{poll_options} max</i>", rows => 8, cols => 35 ], - [ input => short => 'poll_max_options',width => 16, post => ' Number of options voter is allowed to choose' ], - [ hidden => short => 'poll_preview' ], - [ hidden => short => 'poll_recast' ], - ) : (), - ]); - $self->htmlFooter; -} - - -1; - diff --git a/lib/VNWeb/Discussions/Board.pm b/lib/VNWeb/Discussions/Board.pm index 4f32931d..0af7f2cc 100644 --- a/lib/VNWeb/Discussions/Board.pm +++ b/lib/VNWeb/Discussions/Board.pm @@ -3,9 +3,8 @@ package VNWeb::Discussions::Board; use VNWeb::Prelude; use VNWeb::Discussions::Lib; -my $board_regex = join '|', map $_.($BOARD_TYPE{$_}{dbitem}?'(?:[1-9][0-9]{0,5})?':''), keys %BOARD_TYPE; -TUWF::get qr{/t/(all|$board_regex)}, sub { +TUWF::get qr{/t/(all|$BOARD_RE)}, sub { my($type, $id) = tuwf->capture(1) =~ /^([^0-9]+)([0-9]*)$/; my $page = eval { tuwf->validate(get => p => { upage => 1 })->data } || 1; diff --git a/lib/VNWeb/Discussions/Edit.pm b/lib/VNWeb/Discussions/Edit.pm new file mode 100644 index 00000000..ab1783a5 --- /dev/null +++ b/lib/VNWeb/Discussions/Edit.pm @@ -0,0 +1,159 @@ +package VNWeb::Discussions::Edit; + +use VNWeb::Prelude; +use VNWeb::Discussions::Lib; + + +my $FORM = { + tid => { required => 0, id => 1 }, # Thread ID, only when editing a post + num => { required => 0, id => 1 }, # Post number, only when editing + + # Only when num = 1 || tid = undef + title => { required => 0, maxlength => 50 }, + boards => { required => 0, sort_keys => [ 'boardtype', 'iid' ], aoh => { + btype => { enum => \%BOARD_TYPE }, + iid => { required => 0, default => 0, id => 1 }, # + title => { required => 0 }, + } }, + poll => { required => 0, type => 'hash', keys => { + question => { maxlength => 100 }, + max_options => { uint => 1, min => 1, max => 20 }, # + options => { type => 'array', values => { maxlength => 100 }, minlength => 2, maxlength => 20 }, + } }, + + can_mod => { anybool => 1, _when => 'out' }, + can_private => { anybool => 1, _when => 'out' }, + locked => { anybool => 1 }, # When can_mod && (num = 1 || tid = undef) + hidden => { anybool => 1 }, # When can_mod + private => { anybool => 1 }, # When can_private && (num = 1 || tid = undef) + nolastmod => { anybool => 1, _when => 'in' }, # When can_mod + + msg => { maxlength => 32768 }, +}; + +my $FORM_OUT = form_compile out => $FORM; +my $FORM_IN = form_compile in => $FORM; + +elm_form DiscussionsEdit => $FORM_OUT, $FORM_IN; + + +TUWF::get qr{(?:/t/(?<board>$BOARD_RE)/new|/$RE{postid}/edit)}, sub { + my($board_type, $board_id) = (tuwf->capture('board')||'') =~ /^([^0-9]+)([0-9]*)$/; + my($tid, $num) = (tuwf->capture('id'), tuwf->capture('num')); + + $board_type = 'ge' if $board_type && $board_type eq 'an' && !auth->permBoardmod; + + my $t = !$tid ? {} : tuwf->dbRowi(' + SELECT t.id, tp.tid, tp.num, t.title, t.locked, t.private, t.poll_question, t.poll_max_options, tp.hidden, tp.msg, tp.uid AS user_id,', sql_totime('tp.date'), 'AS date + FROM threads t + JOIN threads_posts tp ON tp.tid = t.id AND tp.num =', \$num, + 'WHERE t.id =', \$tid, + 'AND', sql_visible_threads()); + return tuwf->resNotFound if $tid && !$t->{id}; + return tuwf->resDenied if !can_edit t => $t; + + $t->{poll}{options} = $t->{poll_question} && [ map $_->{option}, tuwf->dbAlli('SELECT option FROM threads_poll_options WHERE tid =', \$t->{id}, 'ORDER BY id')->@* ]; + $t->{poll}{question} = delete $t->{poll_question}; + $t->{poll}{max_options} = delete $t->{poll_max_options}; + $t->{poll} = undef if !$t->{poll}{question}; + + if($tid) { + enrich_boards undef, $t; + } else { + $t->{boards} = [ { + btype => $board_type, + iid => $board_id||0, + title => !$board_id ? undef : + tuwf->dbVali('SELECT title FROM', sql_boards(), 'x WHERE btype =', \$board_type, 'AND iid =', \$board_id) + } ]; + return tuwf->resNotFound if $board_id && !length $t->{boards}[0]{title}; + push $t->{boards}->@*, { btype => 'u', iid => auth->uid, title => auth->user->{user_name} } + if $board_type eq 'u' && $board_id != auth->uid; + } + + $t->{can_mod} = auth->permBoardmod; + $t->{can_private} = auth->permBoardmod || auth->permDbmod || auth->permUsermod; + + $t->{msg} //= ''; + $t->{title} //= tuwf->reqGet('title'); + $t->{tid} //= undef; + $t->{num} //= undef; + $t->{private} //= 0; + $t->{hidden} //= 0; + $t->{locked} //= 0; + + framework_ title => $tid ? 'Edit post' : 'Create new thread', sub { + elm_ 'Discussions.Edit' => $FORM_OUT, $t; + }; +}; + + +json_api qr{/t/edit\.json}, $FORM_IN, sub { + my($data) = @_; + my $tid = $data->{tid}; + my $num = $data->{num} || 1; + + my $t = !$tid ? {} : tuwf->dbRowi(' + SELECT t.id, tp.num, t.poll_question, t.poll_max_options, tp.hidden, tp.uid AS user_id,', sql_totime('tp.date'), 'AS date + FROM threads t + JOIN threads_posts tp ON tp.tid = t.id AND tp.num =', \$num, + 'WHERE t.id =', \$tid, + 'AND', sql_visible_threads()); + return tuwf->resNotFound if $tid && !$t->{id}; + return elm_Unauth if !can_edit t => $t; + + my $pollchanged = !$data->{tid} && $data->{poll}; + if($num == 1) { + die "Invalid title" if !length $data->{title}; + die "Invalid boards" if !$data->{boards} || grep +(!$BOARD_TYPE{$_->{btype}}{dbitem})^(!$_->{iid}), $data->{boards}->@*; + + validate_dbid 'SELECT id FROM vn WHERE id IN', map $_->{btype} eq 'v' ? $_->{iid} : (), $data->{boards}->@*; + validate_dbid 'SELECT id FROM producers WHERE id IN', map $_->{btype} eq 'p' ? $_->{iid} : (), $data->{boards}->@*; + # Do not validate user boards here, it's possible to have threads assigned to deleted users. + + die "Invalid max_options" if $data->{poll} && $data->{poll}{max_options} > $data->{poll}{options}->@*; + $pollchanged = 1 if $tid && $data->{poll} && ( + $data->{poll}{question} ne ($t->{poll_question}||'') + || $data->{poll}{max_options} != $t->{poll_max_options} + || join("\n", $data->{poll}{options}->@*) ne + join("\n", map $_->{option}, tuwf->dbAlli('SELECT option FROM threads_poll_options WHERE tid =', \$tid, 'ORDER BY id')->@*) + ) + } + + $tid = tuwf->dbVali('INSERT INTO threads (count) VALUES (1) RETURNING id') if !$tid; + tuwf->dbExeci('UPDATE threads SET', { + title => $data->{title}, + poll_question => $data->{poll} ? $data->{poll}{question} : undef, + poll_max_options => $data->{poll} ? $data->{poll}{max_options} : 1, + auth->permBoardmod ? ( + hidden => $data->{hidden}, + locked => $data->{locked}, + ) : (), + auth->permBoardmod || auth->permDbmod || auth->permUsermod ? ( + private => $data->{private} + ) : (), + }, 'WHERE id =', \$tid + ) if $num == 1; + + if($num == 1) { + tuwf->dbExeci('DELETE FROM threads_boards WHERE tid =', \$tid); + tuwf->dbExeci('INSERT INTO threads_boards (tid, type, iid) VALUES (', sql_comma(\$tid, \$_->{btype}, \($_->{iid}//0)), ')') for $data->{boards}->@*; + } + + if($pollchanged) { + tuwf->dbExeci('DELETE FROM threads_poll_options WHERE tid =', \$tid); + tuwf->dbExeci('INSERT INTO threads_poll_options (tid, option) VALUES (', sql_comma(\$tid, \"$_"), ')') for $data->{poll}{options}->@*; + } + + tuwf->dbExeci('INSERT INTO threads_posts (tid, num, uid) VALUES (', sql_comma(\$tid, 1, \auth->uid), ')') if !$data->{tid}; + tuwf->dbExeci('UPDATE threads_posts SET', sql_comma( + sql('msg =', \bb_subst_links $data->{msg}), + auth->permBoardmod ? sql('hidden =', \$data->{hidden}) : (), + auth->permBoardmod && $data->{nolastmod} ? () : 'edited = NOW()', + ), 'WHERE tid =', \$tid, 'AND num =', \$num + ); + + elm_Redirect post_url $tid, $num, $num; +}; + +1; diff --git a/lib/VNWeb/Discussions/JS.pm b/lib/VNWeb/Discussions/JS.pm new file mode 100644 index 00000000..4c097830 --- /dev/null +++ b/lib/VNWeb/Discussions/JS.pm @@ -0,0 +1,45 @@ +package VNWeb::Discussions::JS; + +use VNWeb::Prelude; +use VNWeb::Discussions::Lib; + +# Autocompletion search results for boards +json_api qr{/t/boards.json}, { + search => {}, +}, sub { + return elm_Unauth if !auth->permBoard; + my $q = shift->{search}; + my $qs = $q =~ s/[%_]//gr; + + my sub subq { + my($prio, $where) = @_; + sql 'SELECT', $prio, ' AS prio, btype, iid, CASE WHEN iid = 0 THEN NULL ELSE title END AS title + FROM (', + sql_join('UNION ALL', + sql('SELECT btype, iid, title, original FROM', sql_boards(), 'a'), + map sql('SELECT', \$_, '::board_type, 0,', \$BOARD_TYPE{$_}{txt}, q{, ''}), + grep !$BOARD_TYPE{$_}{dbitem} && ($BOARD_TYPE{$_}{post_perm} eq 'board' || auth->permBoardmod), + keys %BOARD_TYPE + ), + ') x WHERE', $where + } + + # This query is SLOW :( + elm_BoardResult tuwf->dbPagei({ results => 10, page => 1 }, + 'SELECT btype, iid, title + FROM (', + sql_join('UNION ALL', + # ID match + $q =~ /^($BOARD_RE)$/ && $q =~ /^([a-z]+)([0-9]*)$/ + ? subq(0, sql_and sql('btype =', \"$1"), $2 ? sql('iid =', \"$2") : ()) : (), + subq( + sql('1+LEAST(substr_score(lower(title),', \$qs, '), substr_score(lower(original),', \$qs, '))'), + sql('title ILIKE', \"%$qs%", ' OR original ILIKE', \"%$qs%") + ) + ), ') x + GROUP BY btype, iid, title + ORDER BY MIN(prio), btype, iid' + ) +}; + +1; diff --git a/lib/VNWeb/Discussions/Lib.pm b/lib/VNWeb/Discussions/Lib.pm index f2d6c98d..9f77397e 100644 --- a/lib/VNWeb/Discussions/Lib.pm +++ b/lib/VNWeb/Discussions/Lib.pm @@ -3,7 +3,10 @@ package VNWeb::Discussions::Lib; use VNWeb::Prelude; use Exporter 'import'; -our @EXPORT = qw/post_url sql_visible_threads enrich_boards threadlist_ boardsearch_ boardtypes_/; +our @EXPORT = qw/$BOARD_RE post_url sql_visible_threads sql_boards enrich_boards threadlist_ boardsearch_ boardtypes_/; + + +our $BOARD_RE = join '|', map $_.($BOARD_TYPE{$_}{dbitem}?'(?:[1-9][0-9]{0,5})?':''), keys %BOARD_TYPE; # Returns the URL to the thread page holding the given post (with optional location.hash) @@ -22,15 +25,22 @@ sub sql_visible_threads { } +# Returns a SELECT subquery with all board IDs +sub sql_boards { + sql q{( SELECT 'v'::board_type AS btype, id AS iid, title, original FROM vn + UNION ALL SELECT 'p'::board_type AS btype, id AS iid, name, original FROM producers + UNION ALL SELECT 'u'::board_type AS btype, id AS iid, username, NULL FROM users + )} +} + + # Adds a 'boards' array to threads. sub enrich_boards { my($filt, @lst) = @_; enrich boards => id => tid => sub { sql q{ - SELECT tb.tid, tb.type, tb.iid, COALESCE(u.username, v.title, p.name) AS title, COALESCE(u.username, v.original, p.original) AS original + SELECT tb.tid, tb.type AS btype, tb.iid, b.title, b.original FROM threads_boards tb - LEFT JOIN vn v ON tb.type = 'v' AND v.id = tb.iid - LEFT JOIN producers p ON tb.type = 'p' AND p.id = tb.iid - LEFT JOIN users u ON tb.type = 'u' AND u.id = tb.iid + LEFT JOIN }, sql_boards(), q{b ON b.btype = tb.type AND b.iid = tb.iid WHERE }, sql_and(sql('tb.tid IN', $_[0]), $filt||()), q{ ORDER BY tb.type, tb.iid }}, @lst; @@ -90,9 +100,9 @@ sub threadlist_ { }; b_ class => 'boards', sub { join_ ', ', sub { - a_ href => "/t/$_->{type}".($_->{iid}||''), - title => $_->{original}||$BOARD_TYPE{$_->{type}}{txt}, - shorten $_->{title}||$BOARD_TYPE{$_->{type}}{txt}, 30; + a_ href => "/t/$_->{btype}".($_->{iid}||''), + title => $_->{original}||$BOARD_TYPE{$_->{btype}}{txt}, + shorten $_->{title}||$BOARD_TYPE{$_->{btype}}{txt}, 30; }, $l->{boards}->@[0 .. min 4, $#{$l->{boards}}]; txt_ ', ...' if $l->{boards}->@* > 4; }; diff --git a/lib/VNWeb/Discussions/Thread.pm b/lib/VNWeb/Discussions/Thread.pm index d783a4bd..4d4bdeec 100644 --- a/lib/VNWeb/Discussions/Thread.pm +++ b/lib/VNWeb/Discussions/Thread.pm @@ -48,13 +48,13 @@ sub metabox_ { h2_ 'Posted in'; ul_ sub { li_ sub { - a_ href => "/t/$_->{type}", $BOARD_TYPE{$_->{type}}{txt}; + a_ href => "/t/$_->{btype}", $BOARD_TYPE{$_->{btype}}{txt}; if($_->{iid}) { txt_ ' > '; - a_ style => 'font-weight: bold', href => "/t/$_->{type}$_->{iid}", "$_->{type}$_->{iid}"; + a_ style => 'font-weight: bold', href => "/t/$_->{btype}$_->{iid}", "$_->{btype}$_->{iid}"; txt_ ':'; if($_->{title}) { - a_ href => "/$_->{type}$_->{iid}", title => $_->{original}, $_->{title}; + a_ href => "/$_->{btype}$_->{iid}", title => $_->{original}||$_->{title}, $_->{title}; } else { b_ '[deleted]'; } diff --git a/lib/VNWeb/Elm.pm b/lib/VNWeb/Elm.pm index d8e9a73a..9920adbb 100644 --- a/lib/VNWeb/Elm.pm +++ b/lib/VNWeb/Elm.pm @@ -54,6 +54,11 @@ my %apis = ( rtype => {}, lang => { type => 'array', values => {} }, } } ], + BoardResult => [ { aoh => { # Response to /t/boards.json + btype => {}, + iid => { required => 0, default => 0, id => 1 }, + title => { required => 0 }, + } } ], ); @@ -95,7 +100,13 @@ sub def_type { $data .= def_type($name . to_camel($_), $obj->{keys}{$_}{values} || $obj->{keys}{$_}) for @keys; $data .= sprintf "\ntype alias %s = %s\n\n", $name, $obj->elm_type( - keys => +{ map +($_, ($obj->{keys}{$_}{values} ? 'List ' : '') . $name . to_camel($_)), @keys } + keys => +{ map { + my $t = $obj->{keys}{$_}; + my $n = $name . to_camel($_); + $n = "List $n" if $t->{values}; + $n = "Maybe ($n)" if $t->{values} && !$t->{required} && !defined $t->{default}; + ($_, $n) + } @keys } ); $data } @@ -247,6 +258,7 @@ sub write_types { sort { $LANGUAGE{$a} cmp $LANGUAGE{$b} } keys %LANGUAGE; $data .= def releaseTypes => 'List (String, String)' => list map tuple(string $_, string $RELEASE_TYPE{$_}), keys %RELEASE_TYPE; $data .= def rlistStatus => 'List (Int, String)' => list map tuple($_, string $RLIST_STATUS{$_}), keys %RLIST_STATUS; + $data .= def boardTypes => 'List (String, String)' => list map tuple(string $_, string $BOARD_TYPE{$_}{txt}), keys %BOARD_TYPE; write_module Types => $data; } diff --git a/lib/VNWeb/Validation.pm b/lib/VNWeb/Validation.pm index e3254866..3cec3385 100644 --- a/lib/VNWeb/Validation.pm +++ b/lib/VNWeb/Validation.pm @@ -157,7 +157,7 @@ sub can_edit { return 1 if auth->permBoardmod; if(!$entry->{id}) { # Allow at most 5 new threads per day per user. - return auth && tuwf->dbVali('SELECT count(*) < 5 FROM threads_posts WHERE num = 1 AND date > NOW()-\'1 day\'::interval AND uid =', \auth->uid); + return auth && tuwf->dbVali('SELECT count(*) < ', \5, 'FROM threads_posts WHERE num = 1 AND date > NOW()-\'1 day\'::interval AND uid =', \auth->uid); } elsif(!$entry->{num}) { die "Can't do authorization test when 'locked' field isn't present" if !exists $entry->{locked}; return !$entry->{locked}; |