summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYorhel <git@yorhel.nl>2020-07-01 13:04:48 +0200
committerYorhel <git@yorhel.nl>2020-07-01 13:04:51 +0200
commit8939317270f5facdd127c1b7640842a02a76e93c (patch)
treed7d9777c0edc5d481f6aba62e87577504201965d
parentd5b13e58abee0b2edbe16705451d498e39235d77 (diff)
Char::Edit: Abstract & revamp image voting UI
The image voting is now handled separately from form submission and image IDs are validated when changed. This abstraction is hopefully also usable for the VN form.
-rw-r--r--elm/CharEdit.elm71
-rw-r--r--elm/Lib/Api.elm1
-rw-r--r--elm/Lib/Image.elm162
-rw-r--r--lib/VNWeb/Chars/Edit.pm19
-rw-r--r--lib/VNWeb/Elm.pm18
-rw-r--r--lib/VNWeb/Images/Lib.pm65
-rw-r--r--lib/VNWeb/Images/Upload.pm (renamed from lib/VNWeb/Misc/ImageUpload.pm)13
-rw-r--r--lib/VNWeb/Images/Vote.pm74
-rw-r--r--lib/VNWeb/Validation.pm5
9 files changed, 288 insertions, 140 deletions
diff --git a/elm/CharEdit.elm b/elm/CharEdit.elm
index 0f75a357..75362ef0 100644
--- a/elm/CharEdit.elm
+++ b/elm/CharEdit.elm
@@ -17,6 +17,7 @@ import Lib.Autocomplete as A
import Lib.Api as Api
import Lib.Editsum as Editsum
import Lib.RDate as RDate
+import Lib.Image as Img
import Gen.Release as GR
import Gen.CharEdit as GCE
import Gen.Types as GT
@@ -65,11 +66,7 @@ type alias Model =
, mainName : String
, mainSearch : A.Model GApi.ApiCharResult
, mainSpoil : Int
- , image : Maybe String
- , imageState : Api.State
- , imageNew : Set.Set String
- , imageSex : Maybe Int
- , imageVio : Maybe Int
+ , image : Img.Image
, traits : List GCE.RecvTraits
, traitSearch : A.Model GApi.ApiTraitResult
, traitSelId : Int
@@ -108,11 +105,7 @@ init d =
, mainName = d.main_name
, mainSearch = A.init ""
, mainSpoil = d.main_spoil
- , image = d.image
- , imageState = Api.Normal
- , imageNew = Set.empty
- , imageSex = d.image_sex
- , imageVio = d.image_vio
+ , image = Img.info d.image_info
, traits = d.traits
, traitSearch = A.init ""
, traitSelId = 0
@@ -148,9 +141,7 @@ encode model =
, cup_size = model.cupSize
, main = if model.mainHas then model.main else Nothing
, main_spoil = model.mainSpoil
- , image = model.image
- , image_sex = model.imageSex
- , image_vio = model.imageVio
+ , image = model.image.id
, traits = List.map (\t -> { tid = t.tid, spoil = t.spoil }) model.traits
, vns = List.map (\v -> { vid = v.vid, rid = v.rid, spoil = v.spoil, role = v.role }) model.vns
}
@@ -188,12 +179,10 @@ type Msg
| MainHas Bool
| MainSearch (A.Msg GApi.ApiCharResult)
| MainSpoil Int
- | ImageSet String
+ | ImageSet String Bool
| ImageSelect
| ImageSelected File
- | ImageLoaded GApi.Response
- | ImageSex Int Bool
- | ImageVio Int Bool
+ | ImageMsg Img.Msg
| TraitDel Int
| TraitSel Int Int
| TraitSpoil Int Int
@@ -240,13 +229,10 @@ update msg model =
Nothing -> ({ model | mainSearch = A.clear nm "", main = Just m1.id, mainName = m1.name }, c)
MainSpoil n -> ({ model | mainSpoil = n }, Cmd.none)
- ImageSet s -> ({ model | image = if s == "" then Nothing else Just s}, Cmd.none)
+ ImageSet s b -> let (nm, nc) = Img.new b s in ({ model | image = nm }, Cmd.map ImageMsg nc)
ImageSelect -> (model, FSel.file ["image/png", "image/jpg"] ImageSelected)
- ImageSelected f -> ({ model | imageState = Api.Loading }, Api.postImage Api.Ch f ImageLoaded)
- ImageLoaded (GApi.Image i _ _) -> ({ model | image = Just i, imageNew = Set.insert i model.imageNew, imageState = Api.Normal }, Cmd.none)
- ImageLoaded e -> ({ model | imageState = Api.Error e }, Cmd.none)
- ImageSex i _ -> ({ model | imageSex = Just i }, Cmd.none)
- ImageVio i _ -> ({ model | imageVio = Just i }, Cmd.none)
+ ImageSelected f -> let (nm, nc) = Img.upload Api.Ch f in ({ model | image = nm }, Cmd.map ImageMsg nc)
+ ImageMsg m -> let (nm, nc) = Img.update m model.image in ({ model | image = nm }, Cmd.map ImageMsg nc)
TraitDel idx -> ({ model | traits = delidx idx model.traits }, Cmd.none)
TraitSel id spl -> ({ model | traitSelId = id, traitSelSpl = spl }, Cmd.none)
@@ -288,6 +274,7 @@ isValid : Model -> Bool
isValid model = not
( (model.name /= "" && model.name == model.original)
|| hasDuplicates (List.map (\v -> (v.vid, Maybe.withDefault 0 v.rid)) model.vns)
+ || not (Img.isValid model.image)
)
@@ -380,45 +367,25 @@ view model =
image =
div [ class "formimage" ]
- [ div [] [
- case model.image of
- Nothing -> text "No image."
- Just id -> img [ src (imageUrl id) ] []
- ]
+ [ div [] [ Img.viewImg model.image ]
, div []
[ h2 [] [ text "Image ID" ]
- , inputText "" (Maybe.withDefault "" model.image) ImageSet GCE.valImage
- , Maybe.withDefault (text "") <| Maybe.map (\i -> a [ href <| "/img/"++i ] [ text " (flagging)" ]) model.image
+ , input ([ type_ "text", class "text", tabindex 10, value (Maybe.withDefault "" model.image.id), onInputValidation ImageSet ] ++ GCE.valImage) []
, br [] []
, text "Use an image that already exists on the server or empty to remove the current image."
, br_ 2
, h2 [] [ text "Upload new image" ]
, inputButton "Browse image" ImageSelect []
- , case model.imageState of
- Api.Normal -> text ""
- Api.Loading -> span [ class "spinner" ] []
- Api.Error e -> b [ class "standout" ] [ text <| Api.showResponse e ]
, br [] []
, text "Image must be in JPEG or PNG format and at most 10 MiB. Images larger than 256x300 will automatically be resized."
- , if not (Set.member (Maybe.withDefault "" model.image) model.imageNew) then text "" else div []
- [ br [] []
- , text "Please flag this image: (see the ", a [ href "/d19" ] [ text "image flagging guidelines" ], text " for guidance)"
- , table []
- [ thead [] [ tr [] [ td [] [ text "Sexual" ], td [] [ text "Violence" ] ] ]
- , tr []
- [ td []
- [ label [] [ inputRadio "" (model.imageSex == Just 0) (ImageSex 0), text " Safe" ], br [] []
- , label [] [ inputRadio "" (model.imageSex == Just 1) (ImageSex 1), text " Suggestive" ], br [] []
- , label [] [ inputRadio "" (model.imageSex == Just 2) (ImageSex 2), text " Explicit" ]
- ]
- , td []
- [ label [] [ inputRadio "" (model.imageVio == Just 0) (ImageVio 0), text " Tame" ], br [] []
- , label [] [ inputRadio "" (model.imageVio == Just 1) (ImageVio 1), text " Violent" ], br [] []
- , label [] [ inputRadio "" (model.imageVio == Just 2) (ImageVio 2), text " Brutal" ]
- ]
+ , case Img.viewVote model.image of
+ Nothing -> text ""
+ Just v ->
+ div []
+ [ br [] []
+ , text "Please flag this image: (see the ", a [ href "/d19" ] [ text "image flagging guidelines" ], text " for guidance)"
+ , Html.map ImageMsg v
]
- ]
- ]
]
]
diff --git a/elm/Lib/Api.elm b/elm/Lib/Api.elm
index 3ad3f3aa..e09f1199 100644
--- a/elm/Lib/Api.elm
+++ b/elm/Lib/Api.elm
@@ -45,7 +45,6 @@ showResponse res =
BadCurPass -> "Current password is invalid."
MailChange -> unexp
ImgFormat -> "Unrecognized image format, only JPEG and PNG are accepted."
- Image _ _ _ -> unexp
Releases _ -> unexp
BoardResult _ -> unexp
TagResult _ -> unexp
diff --git a/elm/Lib/Image.elm b/elm/Lib/Image.elm
new file mode 100644
index 00000000..44ce8240
--- /dev/null
+++ b/elm/Lib/Image.elm
@@ -0,0 +1,162 @@
+module Lib.Image exposing (..)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Process
+import Task
+import File exposing (File)
+import Lib.Html exposing (..)
+import Lib.Api as Api
+import Lib.Util exposing (imageUrl)
+import Gen.Api as GApi
+import Gen.Image as GI
+import Gen.ImageVote as GIV
+
+
+type State
+ = Normal
+ | Invalid
+ | NotFound
+ | Loading
+ | Error GApi.Response
+
+type alias Image =
+ { id : Maybe String
+ , img : Maybe GApi.ApiImageResult
+ , imgState : State
+ , saveState : Api.State
+ , saveTimer : Bool
+ }
+
+
+info : Maybe GApi.ApiImageResult -> Image
+info img =
+ { id = Maybe.map (\i -> i.id) img
+ , img = img
+ , imgState = Normal
+ , saveState = Api.Normal
+ , saveTimer = False
+ }
+
+
+-- Fetch image info from the ID
+new : Bool -> String -> (Image, Cmd Msg)
+new valid id =
+ ( { id = if id == "" then Nothing else Just id
+ , img = Nothing
+ , imgState = if id == "" then Normal else if valid then Loading else Invalid
+ , saveState = Api.Normal
+ , saveTimer = False
+ }
+ , if valid && id /= "" then GI.send { id = id } Loaded else Cmd.none
+ )
+
+
+-- Upload a new image from a form
+upload : Api.ImageType -> File -> (Image, Cmd Msg)
+upload t f =
+ ( { id = Nothing
+ , img = Nothing
+ , imgState = Loading
+ , saveState = Api.Normal
+ , saveTimer = False
+ }
+ , Api.postImage t f Loaded)
+
+
+type Msg
+ = Loaded GApi.Response
+ | MySex Int Bool
+ | MyVio Int Bool
+ | Save
+ | Saved GApi.Response
+
+
+update : Msg -> Image -> (Image, Cmd Msg)
+update msg model =
+ let
+ save m =
+ if m.saveTimer || Maybe.withDefault True (Maybe.map (\i -> i.token == Nothing || i.my_sexual == Nothing || i.my_violence == Nothing) m.img)
+ then (m, Cmd.none)
+ else ({ m | saveTimer = True }, Task.perform (always Save) (Process.sleep 1000))
+ in
+ case msg of
+ Loaded (GApi.ImageResult [i]) -> ({ model | id = Just i.id, img = Just i, imgState = Normal}, Cmd.none)
+ Loaded (GApi.ImageResult []) -> ({ model | imgState = NotFound}, Cmd.none)
+ Loaded e -> ({ model | imgState = Error e }, Cmd.none)
+
+ MySex v _ -> save { model | img = Maybe.map (\i -> { i | my_sexual = Just v }) model.img }
+ MyVio v _ -> save { model | img = Maybe.map (\i -> { i | my_violence = Just v }) model.img }
+
+ Save ->
+ case Maybe.map (\i -> (i.token, i.my_sexual, i.my_violence)) model.img of
+ Just (Just token, Just sex, Just vio) ->
+ ( { model | saveTimer = False, saveState = Api.Loading }
+ , GIV.send { votes = [{ id = Maybe.withDefault "" model.id, token = token, sexual = sex, violence = vio, overrule = False }] } Saved)
+ _ -> (model, Cmd.none)
+ Saved (GApi.Success) -> ({ model | saveState = Api.Normal}, Cmd.none)
+ Saved e -> ({ model | saveState = Api.Error e }, Cmd.none)
+
+
+
+isValid : Image -> Bool
+isValid img = img.imgState == Normal
+
+
+viewImg : Image -> Html m
+viewImg image =
+ case (image.imgState, image.img) of
+ (Loading, _) -> div [ class "spinner" ] []
+ (NotFound, _) -> b [ class "standout" ] [ text "Image not found." ]
+ (Invalid, _) -> b [ class "standout" ] [ text "Invalid image ID." ]
+ (Error e, _) -> b [ class "standout" ] [ text <| Api.showResponse e ]
+ (_, Nothing) -> text "No image."
+ (_, Just i) ->
+ label [ class "imghover", style "width" (String.fromInt i.width++"px"), style "height" (String.fromInt i.height++"px") ]
+ [ div [ class "imghover--visible" ]
+ [ img [ src (imageUrl i.id) ] []
+ , a [ href <| "/img/"++i.id ] <|
+ case (i.sexual_avg, i.violence_avg) of
+ (Just sex, Just vio) ->
+ -- XXX: These thresholds are subject to change, maybe just show the numbers here?
+ [ text <| if sex > 1.3 then "Explicit" else if sex > 0.4 then "Suggestive" else "Tame"
+ , text " / "
+ , text <| if vio > 1.3 then "Brutal" else if vio > 0.4 then "Violent" else "Safe"
+ , text <| " (" ++ String.fromInt i.votecount ++ ")"
+ ]
+ _ -> [ text "Not flagged" ]
+ ]
+ ]
+
+
+viewVote : Image -> Maybe (Html Msg)
+viewVote model =
+ let
+ vote i = table []
+ [ thead [] [ tr []
+ [ td [] [ text "Sexual ", if model.saveState == Api.Loading then span [ class "spinner" ] [] else text "" ]
+ , td [] [ text "Violence" ]
+ ] ]
+ , tfoot [] <|
+ case model.saveState of
+ Api.Error e -> [ tr [] [ td [ colspan 2 ] [ b [ class "standout" ] [ text (Api.showResponse e) ] ] ] ]
+ _ -> []
+ , tr []
+ [ td []
+ [ label [] [ inputRadio "" (i.my_sexual == Just 0) (MySex 0), text " Safe" ], br [] []
+ , label [] [ inputRadio "" (i.my_sexual == Just 1) (MySex 1), text " Suggestive" ], br [] []
+ , label [] [ inputRadio "" (i.my_sexual == Just 2) (MySex 2), text " Explicit" ]
+ ]
+ , td []
+ [ label [] [ inputRadio "" (i.my_violence == Just 0) (MyVio 0), text " Tame" ], br [] []
+ , label [] [ inputRadio "" (i.my_violence == Just 1) (MyVio 1), text " Violent" ], br [] []
+ , label [] [ inputRadio "" (i.my_violence == Just 2) (MyVio 2), text " Brutal" ]
+ ]
+ ]
+ ]
+ in case model.img of
+ Nothing -> Nothing
+ Just i ->
+ if i.token == Nothing then Nothing
+ else Just (vote i)
diff --git a/lib/VNWeb/Chars/Edit.pm b/lib/VNWeb/Chars/Edit.pm
index f5ccca38..355fcb4e 100644
--- a/lib/VNWeb/Chars/Edit.pm
+++ b/lib/VNWeb/Chars/Edit.pm
@@ -1,6 +1,7 @@
package VNWeb::Chars::Edit;
use VNWeb::Prelude;
+use VNWeb::Images::Lib 'enrich_image';
my $FORM = {
@@ -25,9 +26,8 @@ my $FORM = {
main_spoil => { uint => 1, range => [0,2] },
main_ref => { _when => 'out', anybool => 1 },
main_name => { _when => 'out', default => '' },
- image => { required => 0, regex => qr/ch[1-9][0-9]{0,6}/ },
- image_sex => { _when => 'in out', required => 0, uint => 1, range => [0,2] },
- image_vio => { _when => 'in out', required => 0, uint => 1, range => [0,2] },
+ image => { required => 0, vndbid => 'ch' },
+ image_info => { _when => 'out', required => 0, type => 'hash', keys => $VNWeb::Elm::apis{ImageResult}[0]{aoh} },
traits => { sort_keys => 'id', aoh => {
tid => { id => 1 },
spoil => { uint => 1, range => [0,2] },
@@ -91,7 +91,13 @@ TUWF::get qr{/$RE{crev}/(?<action>edit|copy)} => sub {
$e->{vns} = [ sort { $a->{title} cmp $b->{title} || $a->{vid} <=> $b->{vid} || ($a->{rid}||0) <=> ($b->{rid}||0) } $e->{vns}->@* ];
enrich_releases $e;
- $e->{image_sex} = $e->{image_vio} = undef;
+ if($e->{image}) {
+ $e->{image_info} = { id => $e->{image} };
+ enrich_image 0, [$e->{image_info}];
+ } else {
+ $e->{image_info} = undef;
+ }
+
$e->{authmod} = auth->permDbmod;
$e->{editsum} = $copy ? "Copied from c$e->{id}.$e->{chrev}" : $e->{chrev} == $e->{maxrev} ? '' : "Reverted to revision c$e->{id}.$e->{chrev}";
@@ -154,11 +160,6 @@ elm_api CharEdit => $FORM_OUT, $FORM_IN, sub {
die "Bad release for v$_->{vid}: r$_->{rid}\n" if defined $_->{rid} && !tuwf->dbVali('SELECT 1 FROM releases_vn WHERE id =', \$_->{rid}, 'AND vid =', \$_->{vid});
}
- tuwf->dbExeci(
- 'INSERT INTO image_votes', { id => $data->{image}, uid => auth->uid, sexual => $data->{image_sex}, violence => $data->{image_vio} },
- ' ON CONFLICT (id, uid) DO NOTHING'
- ) if $data->{image} && defined $data->{image_sex} && defined $data->{image_vio} && tuwf->dbVali('SELECT 1 FROM images WHERE c_votecount = 0 AND id =', \$data->{image});
-
return elm_Unchanged if !$new && !form_changed $FORM_CMP, $data, $e;
my($id,undef,$rev) = db_edit c => $e->{id}, $data;
elm_Redirect "/c$id.$rev";
diff --git a/lib/VNWeb/Elm.pm b/lib/VNWeb/Elm.pm
index 32951a24..b0f4b0f0 100644
--- a/lib/VNWeb/Elm.pm
+++ b/lib/VNWeb/Elm.pm
@@ -51,7 +51,6 @@ our %apis = (
BadCurPass => [], # Current password is incorrect when changing password
MailChange => [], # A confirmation mail has been sent to change a user's email address
ImgFormat => [], # Unrecognized image format
- Image => [ {}, { uint => 1 }, { uint => 1 } ], # Uploaded image id, width, height
Releases => [ { aoh => { # Response to 'Release'
id => { id => 1 },
title => {},
@@ -136,15 +135,16 @@ our %apis = (
);
-# Generate the elm_Response() functions
+# Compile %apis into a %schema and generate the elm_Response() functions
+my %schemas;
for my $name (keys %apis) {
no strict 'refs';
- $apis{$name} = [ map tuwf->compile($_), $apis{$name}->@* ];
+ $schemas{$name} = [ map tuwf->compile($_), $apis{$name}->@* ];
*{'elm_'.$name} = sub {
my @args = map {
- $apis{$name}[$_]->validate($_[$_])->data if tuwf->debug;
- $apis{$name}[$_]->analyze->coerce_for_json($_[$_], unknown => 'reject')
- } 0..$#{$apis{$name}};
+ $schemas{$name}[$_]->validate($_[$_])->data if tuwf->debug;
+ $schemas{$name}[$_]->analyze->coerce_for_json($_[$_], unknown => 'reject')
+ } 0..$#{$schemas{$name}};
tuwf->resJSON({$name, \@args})
};
push @EXPORT, 'elm_'.$name;
@@ -347,9 +347,9 @@ sub write_api {
# of the Elm code, similar to def_type().
my(@union, @decode);
my $data = '';
- my $len = max map length, keys %apis;
- for (sort keys %apis) {
- my($name, $schema) = ($_, $apis{$_});
+ my $len = max map length, keys %schemas;
+ for (sort keys %schemas) {
+ my($name, $schema) = ($_, $schemas{$_});
my $def = $name;
my $dec = sprintf 'JD.field "%s"%s <| %s', $name,
' 'x($len-(length $name)),
diff --git a/lib/VNWeb/Images/Lib.pm b/lib/VNWeb/Images/Lib.pm
new file mode 100644
index 00000000..6437281e
--- /dev/null
+++ b/lib/VNWeb/Images/Lib.pm
@@ -0,0 +1,65 @@
+package VNWeb::Images::Lib;
+
+use VNWeb::Prelude;
+use Exporter 'import';
+
+our @EXPORT = qw/enrich_image validate_token/;
+
+
+# Enrich images so that they match the format expected by the 'ImageResult' Elm
+# API response.
+#
+# Also adds signed tokens to the image list - indicating that the current user
+# is permitted to vote on these images. These tokens ensure that non-moderators
+# can only vote on images that they have been randomly assigned, thus
+# preventing possible abuse when a single person uses multiple accounts to
+# influence the rating of a single image.
+sub enrich_image {
+ my($canvote, $l) = @_;
+ enrich_merge id => sub { sql q{
+ SELECT i.id, i.width, i.height, i.c_votecount AS votecount
+ , i.c_sexual_avg AS sexual_avg, i.c_sexual_stddev AS sexual_stddev
+ , i.c_violence_avg AS violence_avg, i.c_violence_stddev AS violence_stddev
+ , iv.sexual AS my_sexual, iv.violence AS my_violence
+ , COALESCE(EXISTS(SELECT 1 FROM image_votes iv0 WHERE iv0.id = i.id AND iv0.ignore) AND NOT iv.ignore, FALSE) AS my_overrule
+ , COALESCE('v'||v.id, 'c'||c.id, 'v'||vsv.id) AS entry_id
+ , COALESCE(v.title, c.name, vsv.title) AS entry_title
+ FROM images i
+ LEFT JOIN image_votes iv ON iv.id = i.id AND iv.uid =}, \auth->uid, q{
+ LEFT JOIN vn v ON i.id BETWEEN 'cv1' AND vndbid_max('cv') AND v.image = i.id
+ LEFT JOIN chars c ON i.id BETWEEN 'ch1' AND vndbid_max('ch') AND c.image = i.id
+ LEFT JOIN vn_screenshots vs ON i.id BETWEEN 'sf1' AND vndbid_max('sf') AND vs.scr = i.id
+ LEFT JOIN vn vsv ON i.id BETWEEN 'sf1' AND vndbid_max('sf') AND vsv.id = vs.id
+ WHERE i.id IN}, $_
+ }, $l;
+
+ enrich votes => id => id => sub { sql '
+ SELECT iv.id, iv.uid, iv.sexual, iv.violence, iv.ignore OR (u.id IS NOT NULL AND NOT u.perm_imgvote) AS ignore, ', sql_user(), '
+ FROM image_votes iv
+ LEFT JOIN users u ON u.id = iv.uid
+ WHERE iv.id IN', $_,
+ auth ? ('AND (iv.uid IS NULL OR iv.uid <> ', \auth->uid, ')') : (), '
+ ORDER BY u.username'
+ }, $l;
+
+ for(@$l) {
+ $_->{token} = $canvote || ($_->{votecount} == 0 && auth->permImgvote) ? auth->csrftoken(0, "imgvote-$_->{id}") : undef;
+ $_->{entry} = $_->{entry_id} ? { id => $_->{entry_id}, title => $_->{entry_title} } : undef;
+ delete $_->{entry_id};
+ delete $_->{entry_title};
+ for my $v ($_->{votes}->@*) {
+ $v->{user} = xml_string sub { user_ $v }; # Easier than duplicating user_() in Elm
+ delete $v->{$_} for grep /^user_/, keys %$v;
+ }
+ }
+}
+
+# Validates the token generated by enrich_image;
+sub validate_token {
+ my($l) = @_;
+ my $ok = 1;
+ $ok &&= $_->{token} && auth->csrfcheck($_->{token}, "imgvote-$_->{id}") for @$l;
+ $ok;
+}
+
+1;
diff --git a/lib/VNWeb/Misc/ImageUpload.pm b/lib/VNWeb/Images/Upload.pm
index 4fd1ed0c..4fbc6d64 100644
--- a/lib/VNWeb/Misc/ImageUpload.pm
+++ b/lib/VNWeb/Images/Upload.pm
@@ -1,6 +1,7 @@
package VNWeb::Misc::ImageUpload;
use VNWeb::Prelude;
+use VNWeb::Images::Lib;
use Image::Magick;
sub save_img {
@@ -54,7 +55,17 @@ TUWF::post qr{/elm/ImageUpload.json}, sub {
save_img $im, $id, 0, $ow, $oh, $nw, $nh;
save_img $im, $id, 1, $nw, $nh, tuwf->{scr_size}->@* if $type eq 'sf';
- elm_Image $id, $ow, $oh;
+ my $l = [{id => $id}];
+ enrich_image 1, $l;
+ elm_ImageResult $l;
+};
+
+
+elm_api Image => undef, { id => { vndbid => [qw/ch cv sf/] } }, sub {
+ my($data) = @_;
+ my $l = tuwf->dbAlli('SELECT id FROM images WHERE id =', \$data->{id});
+ enrich_image 0, $l;
+ elm_ImageResult $l;
};
1;
diff --git a/lib/VNWeb/Images/Vote.pm b/lib/VNWeb/Images/Vote.pm
index a1aa4570..18b4bb2a 100644
--- a/lib/VNWeb/Images/Vote.pm
+++ b/lib/VNWeb/Images/Vote.pm
@@ -1,66 +1,7 @@
package VNWeb::Images::Vote;
use VNWeb::Prelude;
-
-
-# Add signed tokens to the image ist - indicating that the current user is
-# permitted to vote on these images. These tokens ensure that non-moderators
-# can only vote on images that they have been randomly assigned, thus
-# preventing possible abuse when a single person uses multiple accounts to
-# influence the rating of a single image.
-sub enrich_token {
- my($canvote, $l) = @_;
- $_->{token} = $canvote || ($_->{votecount} == 0 && auth->permImgvote) ? auth->csrftoken(0, "imgvote-$_->{id}") : undef for @$l;
-}
-
-
-# Does the reverse of enrich_token. Returns true if all tokens validated.
-sub validate_token {
- my($l) = @_;
- my $ok = 1;
- $ok &&= $_->{token} && auth->csrfcheck($_->{token}, "imgvote-$_->{id}") for @$l;
- $ok;
-}
-
-
-sub enrich_image {
- my($l) = @_;
- enrich_merge id => sub { sql q{
- SELECT i.id, i.width, i.height, i.c_votecount AS votecount
- , i.c_sexual_avg AS sexual_avg, i.c_sexual_stddev AS sexual_stddev
- , i.c_violence_avg AS violence_avg, i.c_violence_stddev AS violence_stddev
- , iv.sexual AS my_sexual, iv.violence AS my_violence
- , COALESCE(EXISTS(SELECT 1 FROM image_votes iv0 WHERE iv0.id = i.id AND iv0.ignore) AND NOT iv.ignore, FALSE) AS my_overrule
- , COALESCE('v'||v.id, 'c'||c.id, 'v'||vsv.id) AS entry_id
- , COALESCE(v.title, c.name, vsv.title) AS entry_title
- FROM images i
- LEFT JOIN image_votes iv ON iv.id = i.id AND iv.uid =}, \auth->uid, q{
- LEFT JOIN vn v ON i.id BETWEEN 'cv1' AND vndbid_max('cv') AND v.image = i.id
- LEFT JOIN chars c ON i.id BETWEEN 'ch1' AND vndbid_max('ch') AND c.image = i.id
- LEFT JOIN vn_screenshots vs ON i.id BETWEEN 'sf1' AND vndbid_max('sf') AND vs.scr = i.id
- LEFT JOIN vn vsv ON i.id BETWEEN 'sf1' AND vndbid_max('sf') AND vsv.id = vs.id
- WHERE i.id IN}, $_
- }, $l;
-
- enrich votes => id => id => sub { sql '
- SELECT iv.id, iv.uid, iv.sexual, iv.violence, iv.ignore OR (u.id IS NOT NULL AND NOT u.perm_imgvote) AS ignore, ', sql_user(), '
- FROM image_votes iv
- LEFT JOIN users u ON u.id = iv.uid
- WHERE iv.id IN', $_,
- auth ? ('AND (iv.uid IS NULL OR iv.uid <> ', \auth->uid, ')') : (), '
- ORDER BY u.username'
- }, $l;
-
- for(@$l) {
- $_->{entry} = $_->{entry_id} ? { id => $_->{entry_id}, title => $_->{entry_title} } : undef;
- delete $_->{entry_id};
- delete $_->{entry_title};
- for my $v ($_->{votes}->@*) {
- $v->{user} = xml_string sub { user_ $v }; # Easier than duplicating user_() in Elm
- delete $v->{$_} for grep /^user_/, keys %$v;
- }
- }
-}
+use VNWeb::Images::Lib;
my $SEND = form_compile any => {
@@ -74,6 +15,7 @@ my $SEND = form_compile any => {
nsfw_token => {},
};
+
# Fetch a list of images for the user to vote on.
elm_api Images => $SEND, { excl_voted => { anybool => 1 } }, sub {
my($data) = @_;
@@ -112,15 +54,14 @@ elm_api Images => $SEND, { excl_voted => { anybool => 1 } }, sub {
LIMIT', \30
);
warn sprintf 'Weighted random image sampling query returned %d < 30 rows for u%d with a sample fraction of %f', scalar @$l, auth->uid(), $tablesample if @$l < 30;
- enrich_image $l;
- enrich_token 1, $l;
+ enrich_image 1, $l;
elm_ImageResult $l;
};
elm_api ImageVote => undef, {
votes => { sort_keys => 'id', aoh => {
- id => { regex => qr/^(?:ch|cv|sf)[1-9][0-9]*$/ },
+ id => { vndbid => [qw/ch cv sf/] },
token => {},
sexual => { uint => 1, range => [0,2] },
violence => { uint => 1, range => [0,2] },
@@ -172,8 +113,7 @@ TUWF::get qr{/img/vote}, sub {
return tuwf->resDenied if !auth->permImgvote;
my $recent = tuwf->dbAlli('SELECT id FROM image_votes WHERE uid =', \auth->uid, 'ORDER BY date DESC LIMIT', \30);
- enrich_image $recent;
- enrich_token 1, $recent;
+ enrich_image 1, $recent;
framework_ title => 'Image flagging', sub {
imgflag_ images => [ reverse @$recent ], single => 0, warn => 1;
@@ -185,11 +125,9 @@ TUWF::get qr{/img/$RE{imgid}}, sub {
my $id = tuwf->capture('id');
my $l = [{ id => $id }];
- enrich_image $l;
+ enrich_image defined($l->[0]{my_sexual}) || auth->permImgmod(), $l;
return tuwf->resNotFound if !defined $l->[0]{width};
- enrich_token defined($l->[0]{my_sexual}) || auth->permImgmod(), $l;
-
framework_ title => "Image flagging for $id", sub {
imgflag_ images => $l, single => 1, warn => !viewget->{show_nsfw};
};
diff --git a/lib/VNWeb/Validation.pm b/lib/VNWeb/Validation.pm
index acee0fdf..caf6491d 100644
--- a/lib/VNWeb/Validation.pm
+++ b/lib/VNWeb/Validation.pm
@@ -24,6 +24,11 @@ our @EXPORT = qw/
TUWF::set custom_validations => {
id => { uint => 1, max => (1<<26)-1 },
+ # 'vndbid' SQL type, accepts an arrayref with accepted prefixes.
+ vndbid => sub {
+ my $types = ref $_[0] ? join '|', $_[0]->@* : $_[0];
+ +{ regex => qr/^(?:$types)[1-9][0-9]{0,6}$/ }
+ },
editsum => { required => 1, length => [ 2, 5000 ] },
page => { uint => 1, min => 1, max => 1000, required => 0, default => 1, onerror => 1 },
upage => { uint => 1, min => 1, required => 0, default => 1, onerror => 1 }, # pagination without a maximum