summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorYorhel <git@yorhel.nl>2019-12-09 15:18:34 +0100
committerYorhel <git@yorhel.nl>2019-12-12 15:48:35 +0100
commit2c4203b57652e0c3fdc9bd10973754e911f43b36 (patch)
treef31e23c6375996d1a413200bcf90cd0624019265 /lib
parent5075f0ef4573fa95252c1a91b62239cc9b6347bb (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.pm161
-rw-r--r--lib/VNDB/Handler/Discussions.pm239
-rw-r--r--lib/VNWeb/Discussions/Board.pm3
-rw-r--r--lib/VNWeb/Discussions/Edit.pm159
-rw-r--r--lib/VNWeb/Discussions/JS.pm45
-rw-r--r--lib/VNWeb/Discussions/Lib.pm26
-rw-r--r--lib/VNWeb/Discussions/Thread.pm6
-rw-r--r--lib/VNWeb/Elm.pm14
-rw-r--r--lib/VNWeb/Validation.pm2
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};