From 507fc78a5816110134df2ba6265d25298c2f5be2 Mon Sep 17 00:00:00 2001 From: Grzegorz Adam Hankiewicz Date: Tue, 25 Mar 2014 18:38:43 +0100 Subject: [PATCH 001/451] Adds form to query google search on the forum. --- main.tmpl | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/main.tmpl b/main.tmpl index 108532f..0d9f6c1 100644 --- a/main.tmpl +++ b/main.tmpl @@ -43,6 +43,16 @@ ${c.genActionMenu} +
+
+ + + + +
+
+
$content $c.errorMsg From d2942cc0c339e9ce021cf603073748ab0ed437ba Mon Sep 17 00:00:00 2001 From: Clement Date: Wed, 16 Jul 2014 21:08:53 +0200 Subject: [PATCH 002/451] fix #16 --- forum.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forum.nim b/forum.nim index 9a62841..315cd1d 100644 --- a/forum.nim +++ b/forum.nim @@ -798,7 +798,7 @@ post "/doedit": createTFD() readIDs() if edit(c, c.postId): - redirect(c.genThreadUrl()) + redirect(c.genThreadUrl(pageNum = $(c.getPagesInThread+1)) & "#" & $c.postId) else: body = "" handleError("doedit", "Edit", true) From b242b1fce9e7c530a17046647bd44d85077a8d23 Mon Sep 17 00:00:00 2001 From: Clement Date: Wed, 16 Jul 2014 21:32:33 +0200 Subject: [PATCH 003/451] fix #17 --- forum.nim | 3 +++ 1 file changed, 3 insertions(+) diff --git a/forum.nim b/forum.nim index 315cd1d..75a0d7e 100644 --- a/forum.nim +++ b/forum.nim @@ -670,6 +670,9 @@ get "/postActivity.xml": get "/t/@threadid/?@page?/?": createTFD() parseInt(@"threadid", c.threadId, -1..1000_000) + if c.threadid == unselectedThread: + # Thread has just beed deleted + redirect(uri("/")) if @"page".len > 0: parseInt(@"page", c.pageNum, 0..1000_000) cond (c.pageNum > 0) From a12349427df7dede2469feebb1172132a905cf23 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 14 Feb 2015 14:33:21 +0000 Subject: [PATCH 004/451] Reverted to 42e74d230db908389f41e35389 --- forum.nim | 5 ++++- main.tmpl | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/forum.nim b/forum.nim index 9a62841..75a0d7e 100644 --- a/forum.nim +++ b/forum.nim @@ -670,6 +670,9 @@ get "/postActivity.xml": get "/t/@threadid/?@page?/?": createTFD() parseInt(@"threadid", c.threadId, -1..1000_000) + if c.threadid == unselectedThread: + # Thread has just beed deleted + redirect(uri("/")) if @"page".len > 0: parseInt(@"page", c.pageNum, 0..1000_000) cond (c.pageNum > 0) @@ -798,7 +801,7 @@ post "/doedit": createTFD() readIDs() if edit(c, c.postId): - redirect(c.genThreadUrl()) + redirect(c.genThreadUrl(pageNum = $(c.getPagesInThread+1)) & "#" & $c.postId) else: body = "" handleError("doedit", "Edit", true) diff --git a/main.tmpl b/main.tmpl index e9b1b51..2079ffc 100644 --- a/main.tmpl +++ b/main.tmpl @@ -43,6 +43,16 @@ ${c.genActionMenu}
+
+
+ + + + +
+
+
$content $c.errorMsg From cc5d611f93498294ba56fd9640e32b861cd13a87 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 14 Feb 2015 14:39:32 +0000 Subject: [PATCH 005/451] Babel -> Nimble --- nimforum.babel => nimforum.nimble | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename nimforum.babel => nimforum.nimble (59%) diff --git a/nimforum.babel b/nimforum.nimble similarity index 59% rename from nimforum.babel rename to nimforum.nimble index bcd7c68..bcb60cc 100644 --- a/nimforum.babel +++ b/nimforum.nimble @@ -2,10 +2,10 @@ name = "nimforum" version = "0.1.0" author = "Dominik Picheta" -description = "Nimrod forum" +description = "Nim forum" license = "MIT" bin = "forum" [Deps] -Requires: "nimrod >= 0.9.2, cairo#head, jester#head, bcrypt#head" +Requires: "nimrod >= 0.10.3, cairo#head, jester#head, bcrypt#head" From 5ab18cd95395f01950e53300e237edbb9662530a Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 14 Feb 2015 15:16:24 +0000 Subject: [PATCH 006/451] More Nimrod -> Nim. Readme improvements. --- README.md | 21 ++++++++++++++++----- captchas.nim | 2 +- createdb.nim | 2 +- license.txt | 4 ++-- main.tmpl | 10 +++++----- 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 1ab73c6..3e2e006 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,20 @@ -nimforum -======== +# nimforum -This is Nimrod's forum. The code depends on the RST parser of the Nimrod +This is Nim's forum. Available at http://forum.nim-lang.org. + +## Building + +You can use ``nimble`` (available [here](https://github.com/nim-lang/nimble) +to get all the necessary +[dependencies](https://github.com/nim-lang/nimforum/blob/master/nimforum.nimble#L11). + +Clone this repo and execute ``nimble build`` in this repositories directory. + +## Dependencies + +The code depends on the RST parser of the Nim compiler and on Jester. The code generating captchas for registration uses the -[cairo module](http://nimrod-lang.org/cairo.html), which requires you to have +[cairo module](https://github.com/nim-lang/cairo), which requires you to have the [cairo library](http://cairographics.org) installed when you run the forum, or you will be greeted by a cryptic error message similar to: @@ -19,7 +30,7 @@ Replace ``/opt/local/lib`` with the correct path on your system. # Copyright -Copyright (c) 2012-2013 Andreas Rumpf, Dominik Picheta. +Copyright (c) 2012-2015 Andreas Rumpf, Dominik Picheta. All rights reserved. diff --git a/captchas.nim b/captchas.nim index d95767b..c4f6ac2 100644 --- a/captchas.nim +++ b/captchas.nim @@ -1,6 +1,6 @@ # # -# The Nimrod Forum +# The Nim Forum # (c) Copyright 2012 Andreas Rumpf, Dominik Picheta # Look at license.txt for more info. # All rights reserved. diff --git a/createdb.nim b/createdb.nim index 1566089..815b7a8 100644 --- a/createdb.nim +++ b/createdb.nim @@ -1,6 +1,6 @@ # # -# The Nimrod Forum +# The Nim Forum # (c) Copyright 2012 Andreas Rumpf, Dominik Picheta # Look at license.txt for more info. # All rights reserved. diff --git a/license.txt b/license.txt index a7c088e..cf4ac4a 100644 --- a/license.txt +++ b/license.txt @@ -1,4 +1,4 @@ -Copyright (C) 2013 Andreas Rumpf, Dominik Picheta +Copyright (C) 2015 Andreas Rumpf, Dominik Picheta Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in @@ -15,4 +15,4 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/main.tmpl b/main.tmpl index 235e4aa..89e301c 100644 --- a/main.tmpl +++ b/main.tmpl @@ -1,5 +1,5 @@ #! stdtmpl -#proc genMain(c: var TForumData, content: string, title = "Nimrod Forum", +#proc genMain(c: var TForumData, content: string, title = "Nim Forum", # additional_headers = "", showRssLinks = false): string = # result = "" # var stats: TForumStats @@ -98,7 +98,7 @@
- +
#end if
@@ -156,7 +156,7 @@ @@ -208,7 +208,7 @@ # ORDER BY modified DESC LIMIT 1""") - Nimrod forum thread activity + Nim forum thread activity ${frontQuery} @@ -257,7 +257,7 @@ ${xmlEncode(rstToHtml(%postContent))} # ORDER BY creation DESC LIMIT 1""") - Nimrod forum post activity + Nim forum post activity ${frontQuery} From 34553881978cfa9e6d5c7942e26f3f30d1108b37 Mon Sep 17 00:00:00 2001 From: Hans Raaf Date: Sun, 15 Feb 2015 12:28:43 +0100 Subject: [PATCH 007/451] Added binaries to ignored files for convenience I often add recursive or by wildcard. This way the binaries will not end up in the commits. --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 3b9d320..83421ca 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,8 @@ nimcache/ /createdb /forum /nimforum.db + +# Binaries +forum +createdb +editdb From 77b7812994c7c21b4fe385507c50b30e193a2cfb Mon Sep 17 00:00:00 2001 From: Hans Raaf Date: Sun, 15 Feb 2015 12:58:45 +0100 Subject: [PATCH 008/451] I added some important info to the readme. There was no information about how to create the db. I also added informations about compiling it on OS X (which can be removed after the bcryptnim maintainer has updated his package. There is a PR for that from me). --- README.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/README.md b/README.md index 3e2e006..5da3520 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ to get all the necessary Clone this repo and execute ``nimble build`` in this repositories directory. +_See also: Running the forum for how to create the database_ + ## Dependencies The code depends on the RST parser of the Nim @@ -20,6 +22,9 @@ or you will be greeted by a cryptic error message similar to: $ ./forum could not load: libcairo.so(1.2) +### Mac OS X + +#### cairo If you are using macosx and have installed the ``cairo`` library through [MacPorts](https://www.macports.org) you still need to add the library path to your ``LD_LIBRARY_PATH`` environment variable. Example: @@ -28,6 +33,41 @@ your ``LD_LIBRARY_PATH`` environment variable. Example: Replace ``/opt/local/lib`` with the correct path on your system. +#### bcrypt + +On macosx you also need to make sure to use the bcrypt >= 0.2.1 module if that +is not yet updated you can install it with: + +``` +nimble install https://github.com/oderwat/bcryptnim.git@#fix-osx +``` + +You may also need to change `nimforum.nimble` such that it uses 0.2.1 by +changing the dependencies slightly. + +``` +[Deps] +Requires: "nimrod >= 0.10.3, cairo#head, jester#head, bcrypt >= 0.2.1" +``` + +# Running the forum + +**Important: You need to compile and run `createdb` to generate the initial database +before you can run `forum` the first time**! + +This is as simple as: + +``` +nim c -r createdb +``` + +After that you can just run `forum` and if everything is ok you will get the info which URL you need to open in your browser (http://localhost:5000) to access it. + +_There is an update helper `editdb` which you can safely ignore for now._ + +_The files `captchas.nim`, `cache.nim` are included by `forum.nim` and do +not need to be compiled by you._ + # Copyright Copyright (c) 2012-2015 Andreas Rumpf, Dominik Picheta. From 4fa2d1a75bba071fa72ba584dba945ef9ca60c5b Mon Sep 17 00:00:00 2001 From: Flaviu Tamas Date: Thu, 19 Feb 2015 20:42:57 -0500 Subject: [PATCH 009/451] Improve background quality See flaviut/Nim@e36011a5a170f6666c7ba6f77d9ba6350898bdd9 for details --- public/css/style.css | 2 +- public/images/bg.jpg | Bin 94894 -> 0 bytes public/images/bg.png | Bin 0 -> 149129 bytes 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 public/images/bg.jpg create mode 100644 public/images/bg.png diff --git a/public/css/style.css b/public/css/style.css index d6c2147..b09a2f4 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -7,7 +7,7 @@ body { min-width:1030px; margin:0; font: 13pt Helvetica,Arial,sans-serif; - background:#152534 url("/images/bg.jpg") no-repeat fixed center top; } + background:#152534 url("/images/bg.png") no-repeat fixed center top; } pre { color: #F5F5F5;} pre, pre * { cursor:text; } diff --git a/public/images/bg.jpg b/public/images/bg.jpg deleted file mode 100644 index 4e33a79ce15aadf50fc685d7b0f6e827821d2925..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 94894 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 IKlbGR0F%nD#{d8T diff --git a/public/images/bg.png b/public/images/bg.png new file mode 100644 index 0000000000000000000000000000000000000000..91f335913d7c01b14e7b063354768f1f9b955291 GIT binary patch literal 149129 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
-
-
-
-
- +
@@ -83,7 +76,7 @@ #end if - + #else:
Login
@@ -136,7 +129,7 @@
- +
@@ -171,9 +164,6 @@ ga('send', 'pageview'); - - - #end proc diff --git a/public/css/arrow.js b/public/css/arrow.js deleted file mode 100644 index f3e1832..0000000 --- a/public/css/arrow.js +++ /dev/null @@ -1,12 +0,0 @@ -"use strict"; - -function positionGlowArrow() { - var headLinks = document.getElementById("head-links"); - var activeLink = headLinks.getElementsByClassName("active")[0] - if (activeLink == undefined || activeLink == null) - return; - - var offset = (headLinks.offsetWidth - activeLink.offsetLeft) - (activeLink.offsetWidth / 2) - 133; - var glowArrow = document.getElementById("glow-arrow"); - glowArrow.style.right = offset + "px"; -} \ No newline at end of file diff --git a/public/css/forum.js b/public/css/forum.js deleted file mode 100644 index 7af672b..0000000 --- a/public/css/forum.js +++ /dev/null @@ -1,5 +0,0 @@ -"use strict"; - -window.onload = function() { - positionGlowArrow(); -}; diff --git a/public/css/style.css b/public/css/style.css index ecf6972..9d7d0f2 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -3,11 +3,11 @@ a, a * { cursor:pointer; } html { margin:0; overflow-x:auto; } body { - overflow-x:hidden; - min-width:1030px; - margin:0; - font: 13pt Helvetica,Arial,sans-serif; - background:#152534 url("/images/bg.png") no-repeat fixed center top; } + overflow-x:hidden; + min-width:1030px; + margin:0; + font: 13pt Helvetica,Arial,sans-serif; + background:#152534 url("/images/bg.png") no-repeat fixed center top; } pre { color: #F5F5F5;} pre, pre * { cursor:text; } @@ -34,21 +34,24 @@ pre .EscapeSequence .talk-layout { margin:0 40px; } .wide-layout { margin:0 auto; } -#head { height:100px; background:url("/images/head.png") repeat-x bottom; } +#head { + height:100px; + margin-bottom: 40px; + background:url("/images/head.png") repeat-x bottom; } #head.docs { margin-left:280px; background:rgba(0,0,0,.25) url("/images/head-fade.png") no-repeat right top; } #head > div { position:relative } - - #head-logo { - position:absolute; - left:-390px; - top:0; - width:917px; - height:268px; - pointer-events:none; - background:url("/images/logo.png") no-repeat; } - #head.docs #head-logo { left:-381px; position:fixed; } - #head.forum #head-logo { left:-370px; } - + + #head-logo { + position:absolute; + left:-390px; + top:0; + width:917px; + height:268px; + pointer-events:none; + background:url("/images/logo.png") no-repeat; } + #head.docs #head-logo { left:-381px; position:fixed; } + #head.forum #head-logo { left:-370px; } + #head-logo-link { position:absolute; display:block; @@ -58,116 +61,115 @@ pre .EscapeSequence height:85px; } #head.docs #head-logo-link { left:-260px; } #head.forum #head-logo-link { left:30px; } - - #head-links { position:absolute; right:0; bottom:13px; } - #head.docs #head-links, - #head.forum #head-links { right:20px; } - #head-links > a { - display:block; - float:left; - padding:10px 25px 25px 25px; - color:rgba(255,255,255,.5); - font-size:14pt; - text-decoration:none; - letter-spacing:1px; - background:url("/images/head-link.png") no-repeat center bottom; - transition: - color 0.3s ease-in-out, - text-shadow 0.4s ease-in-out; } - #head-links > a:hover, - #head-links > a.active { - color:#1cb3ec; - text-shadow:0 0 4px rgba(28,179,236,.8); - background-image:url("/images/head-link_hover.png"); } - #head-banner { width:200px; height:100px; background:#000; } + #head-links { position:absolute; right:0; bottom:13px; } + #head.docs #head-links, + #head.forum #head-links { right:20px; } + #head-links > a { + display:block; + float:left; + padding:10px 25px 25px 25px; + color:rgba(255,255,255,.5); + font-size:14pt; + text-decoration:none; + letter-spacing:1px; + background:url("/images/head-link.png") no-repeat center bottom; + transition: + color 0.3s ease-in-out, + text-shadow 0.4s ease-in-out; } + #head-links > a:hover, + #head-links > a.active { + position: relative; + color:#1cb3ec; + text-shadow:0 0 4px rgba(28,179,236,.8); + background-image:url("/images/head-link_hover.png"); } -#neck { z-index:0; height:40px; } -#neck.home { height:370px; } -#neck > div { position:relative } - - #glow-arrow { - position:absolute; - top:-9px; - left:0; - right:-16px; - height:48px; - background:url("/images/glow-arrow.png") no-repeat right; } - glow-arrow.docs { left:280px; } - - #glow-line-vert { - position:fixed; - top:100px; - left:280px; - width:3px; - height:844px; - background:url("/images/glow-line-vert.png") no-repeat; } + #head-links > a.active:after { + display: block; + content: ""; + width: 771px; + background: url("/images/glow-arrow.png") no-repeat left; + height: 41px; + position: absolute; + left: 50%; + bottom: -49px; + transform: translateX(-618px); } + + #head-banner { width:200px; height:100px; background:#000; } + + #glow-line-vert { + position:fixed; + top:100px; + left:280px; + width:3px; + height:844px; + background:url("/images/glow-line-vert.png") no-repeat; } #body { z-index:1; position:relative; background:rgba(220,231,248,.6); } #body.docs { margin:0 40px 20px 320px; } #body.forum { margin:0 40px 20px 40px; min-height: 700px; } - - #body-border { - position:absolute; - top:-25px; - left:0; - right:0; - height:35px; - background:rgba(0,0,0,.25); } - - #body-border-left { - position:absolute; - left:-25px; - top:-25px; - bottom:-25px; - width:35px; - background:rgba(0,0,0,.25); } - - #body-border-right { - position:absolute; - right:-25px; - top:-25px; - bottom:-25px; - width:35px; - background:rgba(0,0,0,.25); } - - #body-border-bottom { - position:absolute; - left:10px; - right:10px; - bottom:-25px; - height:35px; - background:rgba(0,0,0,.25); } - - #body.docs #body-border, - #body.forum #body-border { left:10px; right:10px; } - - #glow-line { - position:absolute; - top:-27px; - left:100px; - right:-25px; - height:3px; - background:url("/images/glow-line.png") no-repeat left; } - #glow-line-bottom { - position:absolute; - bottom:-27px; - left:-25px; - right:100px; - height:3px; - background:url("/images/glow-line2.png") no-repeat right; } - - #content { padding:40px 0; } - #content.page { width:680px; min-height:800px; padding-left:20px; } - #content h1 { font-size:20pt; letter-spacing:1px; color:rgba(0,0,0,.75); } - #content h2 { font-size:16pt; letter-spacing:1px; color:rgba(0,0,0,.7); margin-top:40px; } - #content p { color: #1D1D1D; margin: 5pt 0pt; } - #content a { color:#CEDAE9; text-decoration:none; } - #content a:hover { color:#fff; } - #content ul { padding-left:20px; } - #content li { margin-bottom:10px; text-align:justify; } - + + #body-border { + position:absolute; + top:-25px; + left:0; + right:0; + height:35px; + background:rgba(0,0,0,.25); } + + #body-border-left { + position:absolute; + left:-25px; + top:-25px; + bottom:-25px; + width:35px; + background:rgba(0,0,0,.25); } + + #body-border-right { + position:absolute; + right:-25px; + top:-25px; + bottom:-25px; + width:35px; + background:rgba(0,0,0,.25); } + + #body-border-bottom { + position:absolute; + left:10px; + right:10px; + bottom:-25px; + height:35px; + background:rgba(0,0,0,.25); } + + #body.docs #body-border, + #body.forum #body-border { left:10px; right:10px; } + + #glow-line { + position:absolute; + top:-27px; + left:100px; + right:-25px; + height:3px; + background:url("/images/glow-line.png") no-repeat left; } + #glow-line-bottom { + position:absolute; + bottom:-27px; + left:-25px; + right:100px; + height:3px; + background:url("/images/glow-line2.png") no-repeat right; } + + #content { padding:40px 0; } + #content.page { width:680px; min-height:800px; padding-left:20px; } + #content h1 { font-size:20pt; letter-spacing:1px; color:rgba(0,0,0,.75); } + #content h2 { font-size:16pt; letter-spacing:1px; color:rgba(0,0,0,.7); margin-top:40px; } + #content p { color: #1D1D1D; margin: 5pt 0pt; } + #content a { color:#CEDAE9; text-decoration:none; } + #content a:hover { color:#fff; } + #content ul { padding-left:20px; } + #content li { margin-bottom:10px; text-align:justify; } + #talk-heads { overflow:auto; margin:0 8px 0 8px; } #talk-heads > div { float:left; font-size:120%; font-weight:bold; } #talk-heads > .topic { width:45%; } @@ -177,7 +179,7 @@ pre .EscapeSequence #talk-heads > div > div { margin:0 10px 10px 10px; padding:0 10px 10px 10px; border-bottom:1px dashed rgba(0,0,0,0.4); } #talk-heads > .topic > div { margin-left:0; } #talk-heads > .activity > div { margin-right:0; } - + #talk-thread > div { background-color: rgba(255, 255, 255, 0.5); } @@ -237,7 +239,7 @@ pre .EscapeSequence overflow:hidden; background:rgba(0,0,0,0.8); color: white; - + } #talk-thread > div > .author { width: 15%; @@ -269,7 +271,7 @@ pre .EscapeSequence } #talk-threads > div > .topic > div > a { - font-weight:bold; + font-weight:bold; white-space: nowrap; } #talk-threads > div > .topic > div > a:visited { color: #1a1a1a; } @@ -278,7 +280,7 @@ pre .EscapeSequence #talk-threads > div > .detail > div { width:50%; } #talk-threads > div > .detail > div:first-child > div { background:url("/images/forum-views.png") no-repeat left; cursor: help; } #talk-threads > div > .detail > div:last-child > div { background:url("/images/forum-posts.png") no-repeat left; cursor: help; } - + #talk-thread > div { margin:20px 0; min-height:160px; padding-bottom: 10pt; } #talk-thread > div > .author > div > .avatar { margin-top:20px; } #talk-thread > div > .author > div > .name { } @@ -342,12 +344,12 @@ pre .EscapeSequence #talk-head > .detail > div > div { padding-left:22px; } #talk-head > .detail > div:first-child > div { background:url("/images/forum-views.png") no-repeat left; } #talk-head > .detail > div:last-child > div { background:url("/images/forum-posts.png") no-repeat left; } - + #talk-nav { margin:20px 8px 0 8px; padding-top:10px; border-top:1px dashed rgba(0,0,0,0.4); text-align:center; } #talk-nav > a.active { text-decoration:underline !important; } #talk-nav > a, #talk-nav > span, #talk-info > .info-post > div > a, #talk-info > .info-post > div > span { margin-left: 5pt; } - + .standout { padding:5px 30px; margin-bottom:20px; @@ -369,7 +371,7 @@ pre .EscapeSequence .standout li:first-child { padding-top:0; border-top:none; } .standout li p { margin:0 0 10px 0 !important; line-height:130%; } .standout li > a { font-weight:bold; } - + .forum-user-info, .forum-user-info * { cursor:help } @@ -381,22 +383,22 @@ pre .EscapeSequence #foot.home > div { width:960px; } #foot h4 { font-size:11pt; color:rgba(255,255,255,.4); margin:40px 0 6px 0; } #foot a:hover { color:#fff; } - - #foot-links { float:left; } - #foot-links > div { float:left; padding:0 40px 0 0; line-height:120%; } - #foot-links a { display:block; font-size:10pt; color:rgba(255,255,255,.3); text-decoration:none; } - #foot-legal { float:right; font-size:10pt; color:rgba(255,255,255,.3); line-height:150%; text-align:right; } - #foot-legal a { color:inherit; text-decoration:none; } - #foot-legal > h4 > a { color:inherit; } - - #mascot { + + #foot-links { float:left; } + #foot-links > div { float:left; padding:0 40px 0 0; line-height:120%; } + #foot-links a { display:block; font-size:10pt; color:rgba(255,255,255,.3); text-decoration:none; } + #foot-legal { float:right; font-size:10pt; color:rgba(255,255,255,.3); line-height:150%; text-align:right; } + #foot-legal a { color:inherit; text-decoration:none; } + #foot-legal > h4 > a { color:inherit; } + + #mascot { z-index:2; - position:absolute; - top:-340px; - right:25px; - width:202px; - height:319px; - background:url("/images/mascot.png") no-repeat; } + position:absolute; + top:-340px; + right:25px; + width:202px; + height:319px; + background:url("/images/mascot.png") no-repeat; } article#content { @@ -407,18 +409,18 @@ article#content div#sidebar { background-color: rgba(255, 255, 255, 0.1); - + border-left: 8px solid rgba(0, 0, 0, 0.8); border-right: 8px solid rgba(0, 0, 0, 0.8); border-bottom: 8px solid rgba(0, 0, 0, 0.8); border-radius: 3px; - + width: 15%; margin-top: 40px; - + display: inline-block; float: right; - + color: #FFF; } @@ -434,7 +436,7 @@ div#sidebar .content { padding: 12pt; overflow: auto; - + } div#sidebar .content .button @@ -459,7 +461,7 @@ div#sidebar .content input width: 99%; margin-bottom: 10pt; margin-top: 2pt; - + border: 1px solid #6D6D6D; font-size: 12pt; } @@ -607,7 +609,7 @@ div:target { margin-left: 1em; } -.searchHelp { +.searchHelp { color: #000000 !important; float: right; font-size: 11px; diff --git a/public/js/arrow.js b/public/js/arrow.js deleted file mode 100644 index f3e1832..0000000 --- a/public/js/arrow.js +++ /dev/null @@ -1,12 +0,0 @@ -"use strict"; - -function positionGlowArrow() { - var headLinks = document.getElementById("head-links"); - var activeLink = headLinks.getElementsByClassName("active")[0] - if (activeLink == undefined || activeLink == null) - return; - - var offset = (headLinks.offsetWidth - activeLink.offsetLeft) - (activeLink.offsetWidth / 2) - 133; - var glowArrow = document.getElementById("glow-arrow"); - glowArrow.style.right = offset + "px"; -} \ No newline at end of file diff --git a/public/js/forum.js b/public/js/forum.js deleted file mode 100644 index 7af672b..0000000 --- a/public/js/forum.js +++ /dev/null @@ -1,5 +0,0 @@ -"use strict"; - -window.onload = function() { - positionGlowArrow(); -}; From 3499711b6006060ac340446fcdad2e2e8943f4bb Mon Sep 17 00:00:00 2001 From: Nycto Date: Thu, 23 Apr 2015 20:10:58 -0700 Subject: [PATCH 016/451] Run pngcrush on all images --- public/images/forum-posts.png | Bin 206 -> 174 bytes public/images/forum-reply.png | Bin 490 -> 405 bytes public/images/forum-views.png | Bin 424 -> 387 bytes public/images/glow-arrow.png | Bin 8657 -> 8078 bytes public/images/glow-line2.png | Bin 2297 -> 2295 bytes public/images/head-link.png | Bin 203 -> 180 bytes public/images/head-link_hover.png | Bin 799 -> 620 bytes public/images/head.png | Bin 171 -> 164 bytes public/images/logo.png | Bin 116562 -> 111615 bytes 9 files changed, 0 insertions(+), 0 deletions(-) diff --git a/public/images/forum-posts.png b/public/images/forum-posts.png index a9ef428441a837797a614fd3a93b410c8a3341a0..4c4c63b06f1f472393b8dbde2e4da822dbd3560a 100644 GIT binary patch delta 87 zcmX@dxQ=l`T!6Kwi(`n!`Q(Iz1Pf=jvIGYv=KKRjMjJSp#S~bN985{DC{Va;-F$+f q>yScI&x{QBZMM^Smg`H)urV+!cB`!A(rcAu00K`}KbLh*2~7YBOdK@; delta 119 zcmZ3-c#d>tT#_i(`n!`Q(&@1dGECRvL*w`Tz(VaZp@vgF%dqtt~p`#c!r$10_x` zLj!||NspN%0+JIFRxGKO2*_bLn#f?R(A3y?u-|1bzodkOR$`+D7q3g(Lh%j;Au0B3 Tk46izJ{fbkVZ)B2WoF+VPj`w z3?VYPrwQei^Y+6cY=c` z;3V5$Qe2D&^E%xr0VI0000(4W#xFU;z2~0qoZrbsRdL5v z&R)K;3TC66I}BX9&KH*iO?-72b-70J)E+W9dlBIP>cPznt8DfRsTFwCyq|KFew00000NkvXXu0mjfupqW{ diff --git a/public/images/forum-views.png b/public/images/forum-views.png index 93ef8ed05a24a3b0a4d621d0a7f996159a537932..febbdf626dbfc9d8e686b630afdb6ac97043923a 100644 GIT binary patch delta 286 zcmV+(0pb3r1A_yQZhrttL_t(2&y~_W3qesB2k^5Py(E!YDT9goJ!FtY7Wo(!46;a) zZ(u-CWFTTxDCA|w*SP*i|EmYr;MT8h|L1wmxi3eVrl}-c5vV{P_Hcv&R3ZA+F9v;3 zhbxElKmsznzrYQe5O`Yvmlx)6hn{Q@8w8=to~7Q9xpKgSg?}x$SO`IZLd@K-X77}< zD#<|0voR(zON_Ow$n>ll*)+9m7HF{+6`7t@LlMOPTtLr0zqA$P*ksr&V6c`ES;G2= zV+I@Wi1SdU2s0lam|2bP557bInH%K!iX07*qoM6N<$g5pDg3jhEB delta 323 zcmV-J0lfZ$1E>R#Zhs|7L_t(2k$uunOVm*m#_`V`nO)`}L(zIbos9^-j}}wgNRWIL zB82!ZbQ6LL4RjGMilU%R;JBDYW%~iQ;kwge&JS+Rj}9CzJm-7vzw?xcP)8|ch%KyQ zfCc=*4c6E_Cxj>r7*=#f}54rdpRpVfb38`c#dF-5`C)=TW=49JH#oo74aR diff --git a/public/images/glow-arrow.png b/public/images/glow-arrow.png index 436d32f038c8810cd867757fae10ad60162097d8..6a91c23124c32e2be69e6ed4688bea30b11d4476 100644 GIT binary patch literal 8078 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

SB%ADnC-=EqhoL zxDo1nm6|u&W0?QZ_-pvjkmKDc{$Js`;<0tGv&00pj(&QKKxD?1yVu?=6%&$yn|)gq zw!?3W-ZQ}d11rbY@!>&CfH@c_PMYa%%Fhe`sm1^Dyjat$wUCvFk~@f=u0oPKKJ5Z* z&90~VpN_(815rRl3m7o(vpnCj@Fcs&BT{BrX~W40$-15`#<#2O1%Oo`UK{uJ&fM%t z{(oGNmj(}VZVg#J?nkj4-EKG`d#+?|vPA`ema5nPGAkf=uk{?rGI)M^oXLfO`uwhPj-#%u11pDGqy7*0UGIwk literal 8657 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 a)U0iX&8^H=;tz)}0E%)dvenYCkpBZy4QF%! diff --git a/public/images/glow-line2.png b/public/images/glow-line2.png index aea52fdf82ca7631839ab56aff22a58af120c593..7c4c5f57ff06a5155606d29eeb9a6ed1e007aa13 100644 GIT binary patch delta 2225 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ξ$0qmgZ<+T~ zo&r30uq^5C<82cdn2#P+TTd3jVEezNHFr<|{sUWESUR2`PQ?HK002ovPDHLkV1gvY BaF+l8 diff --git a/public/images/head-link.png b/public/images/head-link.png index d97cba5b81c36e9e6ac3d4faa6a1e652c75ad179..8e55b94b4aa84f9abe5fa4263106aff5b15e58b1 100644 GIT binary patch delta 93 zcmV-j0HXiP0ki>-URFU#L_t(Y$L-Ux4FE6*1Hn*HIQy^rX;P#lE%+o4@ItI*W}^WC zB(NJyw$0m}i9UTs)OL_t(Y$L*9k3cxT71ZRVt|G(D=7E_ WHw_m{Krglc0000i$2J8foZGQpjNklY>8)v=>=a8Pr(pn~XjzISP`X>~X4@TF&33qt;LEMv|t;yXJ6!2f2VR#@lfbLPOxI% z4zw08TQmP%k=G?Ir_eugEO9la6QRN+#NahJr9;K08pVJG{*<%0BzgCxK-k-|SraQJ z^H&oi@Qc3DKXRDtJ&Rn(hn!IYqE=5AsTwWNU>a|>-C!-xyiJ?SEt82IyYBDW+}}%# zz^_#gw$Xo{2Y(yLgIw6f$f-NPFj8UFp(S$E%Mj*UCVMRtF7_=ZtHkwhK@Ir*-gc9t z(64Nuf6MhNgo%A9Vj1~zw3%uD1pwX&t4`?SEVc}(2gk$}AHn*N{eZAiHyx2H{@nZ80wVG(4COV-t00000 MNkvXXt^-0~f?kFD!2kdN delta 702 zcmV;v0zv)k1fK?wZGQrsNkl44a5QV>)z5Kb79Lk6#0YM7Ia9oh3fz;|# z1PDOn0rDV> zGiOZ_E0|#{x5fbD00VgS=cTb_xP19)$z+JQMw}uHBpwOdihsO;0mg5a>A3l2s#2M9 zC;1n%kIvJl0L1ui<3;ejjsScijTWJFm*dH^qxwxr=7azwmI9chCGo+)d%b?Td2`z; z8g)C-Y^A81p>~RxJ8m0K{1G9!H^Xh3vlnya#&jOD+0lz?-@2* zrSB;hhf0fYBd4D%^p%?>#a87V3EUKta-er6oh$QtUUqa}U1u)3fs~D39z;e9}3asB#fQhN4_&I0)!>j6jhkvifSJ!8?a|)BmTVE87f{4oC5lEpxFGtRts1*cre;z`;ipoqG%M8$y?|26#oz1+UptLhHU>oIA^fCdmN2= zQR7~j@jKXzwMYJ-rm^|4R`w6JMNCOLCwtYX7xnJ$ad38qAsH8>gW<5HWb#Y0l#aBwiMst@DYp3KAM z=KQ71Zn!m_NZT~(OenDI#@ru9ow-KM@o3zkJ0G49m{CZ`fGY;MN5A^GxQ(W_rp98F ztZrJgQ;xiaCMSihOpY4U`C>NP?>%3d9Qu-^e>dCP3;abzy1%_CawqwI!z~(1;-96x kUE%R#bB59V{69AT0D9rA%aa3>DF6Tf07*qoM6N<$f{&SDIRF3v diff --git a/public/images/head.png b/public/images/head.png index 009f86728e96fb18c580c73b5b8cfed1462ac8d1..9d7bcd29a01f8c4900a6304a41b3ef78e3301d1c 100644 GIT binary patch delta 77 zcmZ3@xP);+oSUJii(`n#@wcZ8c^MQ0ST;;zopr0FBikIRF3v diff --git a/public/images/logo.png b/public/images/logo.png index 85d3d2e5169217dbff6c9e0b5407b1bd3507a055..2594249cf54a9ab9cbe8c374aee56b4cbdf8269f 100644 GIT binary patch literal 111615 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 g=CV-?)28KoG(uB#dBxbM9Gj9Y% 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 P)63$gD$djf0`1 z_=bz1(rQpnfaYZ!ygDB77BD}b7VzwA~ouSOX5n=SgM zm_C*9zyrLP*(W=AI4EV-;A{BwhJew9zh05E+oEFsDxU_jM(v{$Qo+8Fc;rt55j%$V z0rgc%)W9$0>(Uo}6~oQH^`JSEWRVZ!%ix6=;%TR<7`L`LnQ$n092A^Vbs zL`ss~|56J^g55BRts0~99xm0yb1A69k$+CgVD`GBeGej!KKnV@X_Hi0 zH;TbW%#i(4`wibq5EcM66<3A$R^YK$sl^e~{{H8>FnU}E+|!oo&N>J-_SDSQ?tuci-Q( zltr&fG!@(%G!gMQx5`5WwYPk=*dhgX?QAIs91s`Srkgecz$<7Jd2HWm6rFe&{L#ia zpE-30y~j^wooTb$+COkhip2i&&ilVMdckvgRQdzHs@mPv7H^EY*&EI#%}(fGzl~2{ zoiG=cjKo3QflTdQ?#n|;FQf-CES;L6ZoUy}3(TT>*aaAD%5Iy05H%Gn34^h%x-<@V1E`HvalM_M%g zyMr{Sgx;HVl-LANW8aDy-DaUERVWim={*nNd2%olPqG?sk|O?YkWPjVluejk)`5vh zHK;fs!Q6lF_IP|kSeUJ?)^1s_iZ5nNk;br>QTg819PYzQqh_b2R^L{u7NL_4@(%sn z7w0FhtzeOfbqW+ftu<-=nqgGJuwDpKp~#pwxQ~xfcX`Alqswvx8bSFe5<`JXztFm5 z9Y6X3g(8A~FHsw_;Zj>!4UsCP0jPNc1d6h=f3DlR$JW-_eci1(5i<3TEDh9M0pTHD z?)gpZaOO%1)TA`luM19L`;QvXjQ-KE)%saV6_0M5`6NNwhCJCZxmm^C8A6CGV#wRB zQNYcYH8t31ov4q=In;%x*rQ^(Oyb~m$!n# z1>DMUPL1ab&6@b}KKXI?e!Nmc)cxoU2%s zucp)gt5^tB>a?$)+3i9^T$-IF;#v`KATz}_ZHT3<;?o*L6T{YNMg6kL!yw``JJ&X~ z%Qh`L*Ey8u82nz3s>#oHI(I4>v82i_itW0#pziXw3twe*1$Fn zW@Q4uf-7J_%oVFyU)=HY9@kA+3SX89DqP!7<7bk78g;? za;i=8qt=b)vci_3fqL7(wtK&#%*h1c|JAqa87IH|>jxv}&mVr9orDieQ}e=uPr3NP zuQQ{tg0<~xw;aL&aq*}IO9vy#7Ci>Bm+K@krdO?|a|erJtFzMtO<@JSu5<7y>#6SG-pv|S z(il(MCNd4=76lb^ArvAod6}Y!biP-&YgW|EFWbt9E6g6bK4(U-5;uf!CRWrD^pTFd zo3F+BTzl%A(J=4sQiggus{gQa$3THKk)*V8`9Tb*W+KXg5U>@^ehz!@O4v-$7BJ8y zy5L%lR}LdbESaA()PP~u@Baln!>XbhR^_TzC9=_XYphU6VC(UlINO;qpq zapt=Z(h}d?ckAP4fiY71?ZA%sDBj)Co!W|#3L{gbs-;0EH80+M^{>_<=(kCC4tI#MoIwq?F^1IH ztf1?`jU)TP9^~%ew6Www>wI2H=oGN`?!q525Myt=)OdlpXqiY zJ3HHizU@}PK-6qUM3{MqLDz6)>6X*TJS3PlD2bwK^vFz&;FqyfRS(K6{66%=g5Tt8H$91QY|C~1k# zQ@3~oP3~5LEo-+h;Ak`fG-MZKt$mx$4r1)~I@7v~SF*muMuUMKRC}TdF6HU!gov3a z0knTc|3ETh{F(I~_sp;Qf?w1eWZ-2HrUgJofB9PyWARV^g#k9NT*R`jji+zzeMB{B z%)p@pC{?=uap=g+hfsQ(= zRwX#zReH>a6)%rj(S+79-bH<($Q=8HB);Jhw0@F_FE0ntGO=h(k#ac>+n^6B)HJA8 z#Wz%u!W6zoYW#zmDV{M*M_t=Q8Ai~COQt~hur+EId9O*uYWaX*Fz=rFMS4^i^{kS@ zfa3(|5W;D!-bAU&2y})fiz;b7Jhd3$9A%{>EP~Cw?M^iPs92FmCCpRm5(MEALc%zTHq7v6kR-A4!n$( zO60>emc3YJHN_StuWg%+BkxcOiCejc@#pY6=f_q2j6}BYm(gS!hsQl(PYb{{=V`tw)CZXQlmx-# zy@Dd{^l3w9WBdBG5RXBwqBEdP}E+-W%i;;oe#$7bA9V zliID0zEsO#E@=mLy4~kvvSWUBrfQ6C+YEI^5_7E)V=75!#lR#~m4j0XxJR}ywnJ>uZz2Gq8xxrwC z`utwa$1!qHX{+|9B_bzMR61iX;@b>g1Fn(dFGBqFGz4aJ9PkkDkJ37##*a1yiLGe` zqG--X7+tfg72D4Y_{W1n55hQpvdI73SB?uSxjc$;I++|2qT5%%&aOwIvG20`+$D?S z%2QKEIXjy5B|dk;KpT_vjhkgWQmsEq%lw06wIRii6xh=;}iMVZMb>%Pj55A)AQspM|23CrEOsMr`&GLHtl5zbe!cBJF z##dhEQTT_!gFf{J7RB3s2IX6_?$za3%AGi_;6H&HPav1Sp?gKo&xBx0 zTrSJuXP3Ww|8i6$x3$I1*dew1dpr>`CET{HQW5 z_Pj1sD@0WZ&KWmW;t|d{O?55)L(EkZ>U%OVAn$sfCh&CUpOFMbxh#6u($uXzoWLtlK zG?uOp8iks(*LB$8&&d9%BK!YA)^b$zT%FnP10hGZa%R zVn#{}5>_Oy5jH19&@tL3u&%DTpZ5_}jGTf(5Q_~`kZHiWuerP5hH!G=VKXL(Qxzc1 z0_w4Cf??!hZe4{R>g9zo1)i0M_C|!@6n9P?NKbA=oN&ynhsWJA2P+vhw})Dy>GD~O zd(LB7MPq;9k-s?hhYRhi<8A37KXIxlY#c52UD~>aM#{xKYsEbhCwrdOLj2Tw+`Pgj z+FJG2cZ(0Q7o^1S0NKbt8gJ{Qh2HsFJUlrRKyd~;zC$;`pzq*|a^XY4sG;{ctIt^G zy;npQg5S`DKZn8)mCXfG*K&pN%2PJdQtM91lo{6%Oy4%?rbwt_Ug#+;g=P4VYUG~S z(UuxiY4Wr2eUWsj1(q4d&=TrN5K_m1#Zu=FoGs%_-%4Gn%2YT+ITU=F6v;gzrNAk- zvGK4~<7zRBqQZXnR9VM1$E8!A^`W#W6Z#zBEn-9*T*s9sC2|TGRZXgW2LOM%&m|SO z?-$;=6Po$S!*n1n0@E3ZYu^HWsODrT$KDZ0M0!Kq6OqW1dV_=Ye$q-P*AZFK4f+)Q zgAV2OcDj@F4%!>rg0iJb!R9}Hd5t9yJf}Xp&^_WsTb;mfs~es@_iLrM>+h%cbhhfV zUpcd7BO^`16&NBkE$El7>2Uz>WBIyHMDY{6*T1~D;$%|fM z|7iF$_FW(h6*6U%t+|7Wn{#SSSXta@-Fi#cUkH9sW>2FU9P*(;rph(qu4y|0|Fr%{ zcDA=a_&c&v#7tUUJ)TTIdc#8 z!T0@#s*2Wv=~vq07IdHQ#3n-OS=WO?F?!77p-}| z%|v?*hx9&g7Mr^agOerOMOyqlsIGMiC;6tWF8`O|iV^Cu?iv18q$}uSO?U$#KQxR# zp(W*D@)VIvCj-&4O;F<_L^Sy6KeTX@_u|wcjZA3fcJ}?|AG}NQDbruq)7-;MIn!&^ z)|K6@n}(E9yu)jXhKq&vo!OX_CM%QJA4ej5&E}DsRm(?btIe()_P6H!YCt_LkMoF9 zuObrt=~UiVI$`_@#E&MHu|{1g`iXu2!-whqk>Y<3d;IJsL;uBp5j(tae7x(6wQ0Qp zO--oIcBRE=28=#c~(x{g19zMTBRpLObMnD4f6W$G*^5&?P zHDx54SNC7`ac*_KrjyG4>^I1u1f?RwSHZ12{_upbqBvh(T|QmoW(?VCl+D6Q43CIh ze+$9s?iUQ94uaFc9Gz+md$eVA!svaW$9s1w9F8F=PDJ>u5t^}bwuYBGYClyov`#r- z>4-x7O=WXA9uIebK{&sL?+=#LEWKz6E$%~{#(LW1F;9~({-q8Owa3O!*du(Yzm-PQ zLFfFGb$?U`C1T<=%G!;X^(e5`2aEka@V>?Z;zBhxXQ5Yg_CKEf_}U5o&hJE7M3fx| z4LB~JdRse~pb(imI73ZDw%hht=?NKc)1_4_uqd z?|X*}=%c0cu8lqetd6f{nX{ErZGUDYC^=)R=;$=zO`UaNr}`l&oDV17(y&ehw{;9Z zk|Hv@&hB8W0WNbEdB}u(K>eI2=0IIuu6q+|{?bl{ z?zcQ;#}~vopMN6BW`kjdYF)z|SV+?}TR#ccGB7*V2jy3|AuK6S6$n^xjx_~rA9J}( zH$Mo7&@wvDUDpbUUCgJ+e6sh*0BT`QUyYsPccpK*Ok=qkM(bY|PzmhN)EJ~kwV8iV zITJkR8B(rhq3WAYHvXc8HjPP<=S(Pb}tT(Rm|(*K+5W@WpvvOlBz-%9_p3N6PqT#C74c`&2v z!fW6Xa|M%yw==#f+vkjN+!nzP74Y0;`RJz=QAEJ=jwB6>S+=X5@iY5{gGlVWdA z2IMzaI)U3usR7Pd9g#$Wybwy?B;EPw&_Dor0+n%lf$^P}y-+sEK_hL3oON+XOjeuB zDnf)?gi30Z*j#E%+oD47YR=4Vm@$j0lGEc`H7cH`v9#MHn&%5yO4N2`rJr1>4P<^` zc2#E&C84i8Cg=$!;C8J7GL}go3ru81iE*qHC1X!megEzYrp(eyK57SOlhS}0BN12LSHeIU-?AKUh&C*vzO!6){F^~E#D0tXX zO-Jh`i?sWa$F{gYYpjC85Vo&1=Zr)4x;qZg)3S2ad!gt#@#CCdbusqcM1rRu)W+9m8h>Lcp4d}65LVg zU=L@EELX1A93&(TONP>kQb?-$s~Cy>-1XDp7g2r2TsY{{)XZ?Iq}s*~gjX9KyTjX%RS$AKjD{UrqFQr> z%aFMT*y_>W(D|D&IoCfNUxZgM=9ki{mGaR->s^Ome`tV!vS4|ACaZZ)qDxl*UiX8m z4Smv4Yx`Hsp30c1u-UH`_KA68r$UMA-fn^eOZkNH9XWqa&L=K}facZspL{!gruGHifthMx=@1+V0JSCY$n$_fZ zpWSF0XS&(+0{O!Hls(RO)}&`HM6PlgHszAt9(PFMbX2UN5AxQ?-*OZ&3p#|Ev|Hrg znhQ9;lQS@Kh<4OiLq|B+;u~SJ7mel38qg4Tz{TT=Z`KM{sRaf%gyGH)4DpzAG%{9j z_aQiTbWJa$!Lt0nik992VXq=0dJWlEJN8$&{r}2W9jJYZgtX%`S<#hYMPOpa5NS-R`vQ@_^bs$Yd`{?EvkFD z(D~G_+F?+%HVRswGF*`rOvcNQP#95mEy6S_H0WELX2~90 z*jt?C(w5U5FH|5aXprpH_;Zkn8yUeNjsGh+Op=@RZ3Lpxm-6BYvV7mpPZ*lhHF$4! zP6s^_yJo9+{YOGMdNHB!b>D6s(H|TV`&M4&FJw((mVv}#fj7;49CTlvqxg)Q>0M{| zSF5IkDB9NKzKu2FK;_6_iKaf8X4)WWO!vV0;%gVj_PB0D5_|tvKQb53o;0uYZI=iz z2~FLQTNHExkO-&B(fqDc{EW~Yb60&8K9Bf(9_77RD6-dJfzq8RHp7`+S+I+}ttp92~_*C?Z!L9awcy zWG;qqfX&fW`rP!UuxQA>SmoQ7o;t`i^ElI&Eh<|Fn^7>muM3L7ef}VrSa#TFm9im} zkGD=HOc#HO43R}$SjZGAuhD7LttuKusZce_CNANqyT97PJM^ZFPADB#moYl(Ob{bA zMh-=}3OMAx42N=w51H{awb6aP0=)*$Sw{YY+t+&a+N}V+YNmBLaoHaKm2|NFYZ4YP z8$z!C;PSs*fezaZKZjn!YQd*Ad!_x8#WB?@V}KxLYht}CH{2fJ*G6IQ|5V+(qgbbx zcc2VzRE#A&L~Hzycg!bY_4nNxpJFBQ2f~hWyFn7H(wHmw&tL>lqD!J(A&I!{^0Jlr zvJWT)1A$t3m}@JR?m~Z{kohMJ5s777wRw%3f)YOQyonX7)CkN{DA`dwY{y%QmS|m1 zvikbyRZoTms_oIB!XmzBJwD+S?}`=H=OA^p;>f8ALaM++CUjYg3a?5o0vV#781j@E zeU;?~&6dw{il8N?lW4V6Vy=7*)U)-x{qX#p%TPZ_$ex58o`5tO_ac7hXZ1uoTI>h! zo%=12FkN%eZ?!y7n*+w|e8>1!8`A7_{B-P0+R-x?1gKmu|;z5IfIx@bw^5H?bAZ3CXdOju74&`F5`m8VcI51~I2M{Y!QpM>3|H#j$21s5+K zZ|~T!gsI*1*Cn+T*sa=EEx`+E&ti(S81oBQ#^%dlCcyap9{0r=w2*yOMXPldqegmS zS!cnwEDbYFpiT$OC&(;_xg0T(LoXJSPN7QDNFbyMto{@fO!|*9sE{sZ{-X*1gT1Y? zCzEwQ1<@WPO&TRZ{I}&Uth>c1J3CfwCV|sr)CNPdXxKrL#G-EO8t&0{l6Id1#|ndj zUq|=VZcO1z6B*^DuL@za5e}MK?qb%2K9X~WpP{p`SVZFS!x-=xw|k22%$KTr#VhT+ z7ETgfj}HoH%Xf|4!a;KET6*@iEg(4Hy4BXLylPm{|H8mNwU3G=Lyj|tYZBau!=1EC z4<2kp!Dl4Hu(^5`V4XdfJ=W8RQu>ZXrkTKMAM;}o+`C!M+t6y0|5eb>xF=#mEcWwJVma_w5P7GJ3@@z-ozsYrhL7@B<-ylSmA}ZWt`lU=_mmVw z=KY}-PeX`KCOl;PABa>GGT*H;Hw(^xT^9^|YNn$T%2O~ESE(qlrrESYe>7BVr3zX% z`rW96dmmq{c4+%f;b+)he#mPyhOowm_|ulj_r)s~I(2DP-99%d2#+0I1ocAEFGp`{ z^O#ZB(E#b>`F-BzzA^GK%Y^Y}>~dFqFt(T=J?yhUwc;uolg~1ZfePwdBnNgh=RF*u zG0Q5b*UMIT2Yh#v+ca8bzAJpy7c)7%1-F{De>~jSoN%#S=ev;{UH4yquO)pt>23D6 z`T6>8?lcSBOtW(iJp~(80oRRm^decE!orDy(6ELYj|W;T+8%@$G!_d(WhW#Rt$yjt zI8u9~GtqlNSHslm$qJOic0+kI%_w#}(#6OW{ZKbtO7U@E9yI2zl&uG#Q9se}&34c0 zB+RCNAG^Jf(t#Rfy3?qrt>eljdOH7c(;_(gR`u}8Rsa9u>k}1nP$BEb(pDqxRFns^ z3d66M*Mi2o#G7}uui!FI>w@Dy1~`GAPjF>gs2T$$ zmA_Tt*z-h5MZu!s+H!~0w_$V2e0n;_+rHdsCxOo5&&oI5wYi}AH5McOlKe8AJ%J9u zHk-X}BLkK+j}AL$j`TN`S?9O>n##6gfDtlgmSC<(mKA4ecEj4{9u{EVfEZtYE>90z zdfQzl=e1&}9rpu*Lw^|!VQX5Qc%r%RmNhM88Hc5sT~ekP#w4iWcY%51UIJCx^^@d+ z>rF-!9{PLfB5hTT@oC32lPiGD{LLQ{EEdkILmn9t_xYv;hqpgJiLrB7LceF+PAXK9 z8ht?KCp-~)vaDAH0=oDw1kSs;uAJXlrumQ3te zZ%g|oAHEd&t+n>%h*M@ae%zFsIOT6U9@_;cr-P@(e~z@i`fD+~fM{FwIl7V6>Hi`$ z9%4jiv{mJbySsHs&;og-jV4`Gf8+-%dbGJi;N0-j~0aIvWUmbOM zgmd3(LYgkKGW1V_6`wWyc63RzTsnevZzrZVc1wr>Ac7eH4b|H${&37&DtvHbFx`zj z9sCHY{uKyp%Stumq!oMjufeDOYyYpU9`%)>hUi-@RIE)i-!nDqHVVTfU;DW`jV||1 zXDTe)A(B?Es&CQ#eY)F60t7aK=ry)PQV( zF<748lUUD$Dm;3G=}8_n9XN0LE}4)Z;1)q&(F4IZBcBALBXP2`wNaEfj7N|hD8>e6 z-7;u3Jkl3lW%Hj^u=3$<2oCnMSZ5<|yopvPNonGMBrD!DX%}K_CObl1w-9@cb$g2y z=ZWJ`-oxqOJvb>X@0X-$MPeHq{!r3d=GO|#`KtO;6gyR8rKn=!HAak{oS5h>Z`r(2 z)(E3+Zd5cR{j_XWIDZ=xKv}C1tqRu@Q1Gs7*%MC9@z0lYEoo+ZC?rM$sAevh1ZKpe#S$SdhE{l zTlp4(x!3XrE6wC>JJ}Am{F$kTv+a_t&2LU;t!A8Awd7T5r}J1UIPLFf&XBmfihE-> zzFw4uqV&Vbwd-vf$z7fw3@51{+hCs0UEb@#D-wcl7af}$k0E}gQIrK89|X8CE|iT| z&@5Axm(|PO%^eAevaEm{^Bn4y~#8YJmhE zn;mxv4p<(EL@>IO311{lr=9y}pK;SY%$bMn%0zQOu@>S;a^Zkas*}3n@)TufBFxSH z`nOkr7%2#raSU6QZX^7lEwc&cd|Cg|4L)R^h(d8(ZoK_Xr6p#I-`$a{Vr6Au?ln+E z%#ngOGwt<=C+6*yna$aVX5M+CsQfKlvIbYnY;AnCP}p>b6w@`7$u8J!6)ccZBVMB zX<`J=S~bQ{rv>pWe!bq zst^&2Us4@e`;i**d{>EO98>)P&MWsKsh-}j&V@>Pdfpca|1*MqBow7T7ns~BGicXc z)%f18b4)aeq&Zb1TYcO0b9-UG6!oJChf%a&3X92iHu61Fx05WJ0|34y`E^qY)W3ky z)TUFsH`lZ+3i$BT`Y*1sH+}a9%bE%)ci>e9MLo;g#bL((0MF%^ZtEGD8CyvY977wD zDyRGaTRlaQ-o_?moikL{149e=(Y$YlCUm;XoX&$Eds4+tw2g+X^SJ+*_@x5Vzz>E`90T|0%tH6VEM>b%@HC zPL~_^Fa?$^Wh`yvr0p4b|4YLv-w4%-jEPBBwv^4B@uq0Y%I~6~R2D7UCYUf?jeL(Q z?(&*_Tm@k(?bDX5%>dwePYKNrn|O<-d@^bmVF)Q)OfGtX0%FYX&Y+743;a;&$_v?2 z$l)R9PD*hZk~o7&S)wsE{aHTF}X-f`O?_4&f6o6eR?qSAc znP*<-yEvR!dDHD!_gjzLUf|~aMjAyOCA$!ID_QHA{8?%E%loqk{631cyN4kzP%w7J zU)4dmS?XSc`HBH6w@garv$Z*%^>nZdhSro}vEsRlUg2+EIC~Y}X1Eubx*lSWWG-QO z`()>hdG%%>+8uelJdIaDaSc$7fVi9QQ%jj0o=xPn-{C||zaKRjky{kL<>)Z>X0Yd2 z#ye_T^T&ffqM6Tyy~3?>q`4j0*aa?zOm8$?_X7`bF@dKD?rFHc7K`@JdThMS4O3Qm z^k;nn4=iNo^F`GdPe_h4r0Oy2l- zF*&-&&ayhhrjZl6z$qlIZ(nlyi@H*=5V0t78#Wf1~Z9p5_8 zdE_+OA&cAmbw5a6nmc7K(GO*zHFPdc!%Ne@!O7}Z@5ak&W+`NU-6#FSLl49gsQMF> z$g=r$H0LVOtLnqZ_H;S%>wonslhWsNhnE@lMBj&*Y(<^`%cMSTTAjSVD91G%gxu!K zB5PzY6zOPEBM+;kgj2>77JT*1;0nUyUAAOO)Y9Y8F1^d1iXyfDR9;zR6TPj^3t(?r zn6&DBV$GWocqHLRUwHipz!9Y|IgnPNSitRv1mYC+sD&s zk^1X2G-fAUnpj?*!Dg4Gv&a=Z9$?GB!AyRNzdgs&934V0y7-D3qbHlJ8^z>dhb$-2 z2sGupP9m*F@>fZfk-V|GovkkKeoFR5wl@#q#e2}sK`a8jQ6`~40h%J?Z9JSX(fq)oO z91h*tYUF>g7>JUE-`-C}U_m@@s{;-Pm9@0>KfJ!&{^`&9te6eK$X=*2e7&r|_75Ci zTe4N4V&k1>cuJBHGV391IJUf7rm85c zP~3d1Y^cPV-5leP4)BK=1H~- z*05brNdz(CdH6jASM&V-~+>O$UrQ5KQTd7JoWl z;LBDVZBfa7N1FD1kZ48{FmH(3&N7c;+#$R2;|`y5F=isQ&Re`DT^4F%AlNMwJ`*p1 zZD~lG_(-@?XJN0fx*+NQ{o;MKXzBxIt6R?Wv8Sc1Bk;zCC^fMOz4Kko2EGQ$$AyrO z+dVlK{b@O0Y~mffgWX!BblYmNtYohmMmM%878ligSCwNVHId|RU}&6# zQms@c*hZFXwRL>}@8@#6IN5*9(!<+`FrkFY*Dpq8+=m+!RD}3uL_9= zziLPV@&A>cu1i(;3E}@1;VT;YL_hRp;TpdG+9Uj0gd1*fDPxIM#0$wP`wP{%$94;yayG>$KK%=_~7VfV! z9K`gSjB*)i*_{uwb5LDU&bEZY9u`(X+>T4#OET&M#ah9gh8jUzXCoQ)a$3#~aB?{c z>BS9!X$7;Qh34#s9-Hcm9vhU7F;t|$#%_dO?jfr-Rg)e2g?<@@Zhyt4)KI$71@~4H zo3FVz5hYCHq8W^SxJKV>34QB^(49pYj7p+9R>O*aEogo%tbrTewB&WqKA3+O-=<n>5x`937_%Qo6P_vW$}i=4q+UCQtWqtVfxVQTnOOi7GJGW{QaCs!gZ}3Sd^0 zpIJY~p8-_d7AZA!=AxV-U0zW$VC)nh!`Ktl%>(za8ZfQ9 zgRSxKcD&n|=AmfR!&PsMnGhDVWR+Q-A0pWz zYO5{X$YE&QeVm>X>=nWf4&r4|LDA?C+LFY;gyhkx6S-^+L0bPq0u5!07h{qpw88Bd zWu?EL3alW-gJ?d90?X}jFTYh!X?}O+ymNo+kNVNAVvM2!ouR6Vl{JygR=-lL!C(zk z?1{ro>@O}_okCW_EdXiFlZVb}=D=5Phq|istV91p?vt0nN6pI5W!bFFU874(2Ps~- z3aF+=N8@&(C)x8)a(s)$4qB1OYV9)8 zra^2V1blu6aqePA{0eKgAerEwsTit%9Be;R9^n-FOFj6onCKA;s9&-ubbnsWEnt_r zAssq+i87;YE?hvK-;bur8h=~0%fle)pjSXyNObP&H~ zwQJs_w7OTD1x9zWsgbKZA!}1IUa=bz^dv-7O+djwY3mrT;H$QP0=G!|ND9( z5UkwHI#->qXlE5_8GuA*%I`qqhS}BG0y+o&X&VYa0(Gn=7zWf|G@s;kjJ(zXTk&lzK zM$@BcG*3i8_);jL2m)~yRF4ZjHu+SEuZTNb-HXgV&jvcyR!#631-hg&6?@jm@o6ln zryM4nHt`XY2Js@mE`C>W6!|~zxV_EDnP-l{x@>lj{7@8|{KFWR7r@LP8%^Dj2$F>X z7q7$HB!6#>tm9w+Lk;K!1OYJlsUS^%TI<3+5X~i5)grw=H{|X*up?_;Cwv*veIGGv zK}cn5$AVn;>7achx3!;2U%E%WGIm<(izI&)$V2{w_kWvcEBeg;tW;h}oUK)Nli`2` z^{c`y$0U%1$1}hAdZ@Lexeq0$GmYsK=`Y;x7H6s?b^@Ch_#bIlr(0&y>ve zUULLl=L$5VhAXG~SwO@pFUh|3HS6`xdabmRy5cQUEQNGU`D7&;sDk&`8 z-I6XPjY#Lx-OaLqyM!P}cY^}b-6h>6(j`bKDT{!BaUQ<^-#O>qzT40J%*-{{Tr>CF zLN?+}ZT_!4_1O-*=P911Tt1K2BK$M&HVhgKMlVHX=RpVLVIz%?YdZc#*kt zO>Q#W8e`t=_iC}D*s7&`+OEl$k@D^8t%2{LF2~m|PmX{aIJicgr6TUp`EO$+P=>d% zSj)xIpVGV$Y$i6`gKvG;sO_Y&vR=OP1vxan4>N%=i)&b%+1T{<3#P%T{tGJv zfVSLpcZ}bxEH25Uy4v+9VB4qIZh@KTp#aMC*td zVNSTr0a>)Rz7I&OafJSa_x@&vInlh8FIvYERr4Ty>j2A-ukq&meY2$F3!z7dnUZ5r zWwNU3NHn#&d2R}^VS}-i=|i5d1=nS7TvAr@^zMv_8sMcFr-do!k8<2yOZ6jILbI%P zY^wO}pxn}E(N-=pnSZ>z+TFOqtK3jujWgiy?0-m1%p*w)Na+k$E*4RuX_d;|r!`seScegpq8{;Q1 zYqR(&q|xXfF*j?~)ka^9Rz!GtW^~j%m7)hzZ?~u{vkHYtf)#wYw?8ZdPSQENN2nLo z=4-r9UE<|cE%|!Zyk+pw#=6fXRNy;MK5xo z=sXo=>z9aF*4E(S7LjLTMG-;zZ=`}4V%@(>4q!VjZ$;p3Nk?rMuzJ?;iobmFRh&hN zK5pgND-tU0vC5Spwo9=_uGHS+{aAac$5Rb>wwKWcXDG-&c5-@ukeEdKO0yjBe7Zve zTs|-GEsAb*`MBv6?oLqH)VvQXY&yD?LF(igyCcjo$6Z`&moV&Vb>dNIFQ;05yZ`(v zGFonrw6F`Giys)sZ3bXW+3@X8OE^o@LG$we=T`|gYIip$H{&C0{a2PwBXyKKpL0ut zG(R!l6*nmK#ZYJIxxLd{vf>2Rf5nM}i3w_bXUMemC%bTc%sRyLJPIqvNWCSR)DxT| z!q@~)q1}8hoGQle3M__&p!qF>u3aeINCWIIdku}!(#>pt*p+(Xb!Oc9s{{qNx@wCEQuv+!udgyc zlu}M~BF#uL0%#8%Ro3i$%s|#v7)cY;W)k-OcaktNhO4}_ z;7dX|p$`Kd|E>HjI63)Y3PY_|h=N|FOSq7=n7!I*)#+2x`M-pQ zGWCK@na$1Z6QRS&mqANi!g|6B)(*ZnwfZ#}@U>SWyO4m`~r@U-5O< zhrXD`DWidJTLxrjB^BmuXS?v-$da6`kxwbBaq-#fl2|=C@(JJt(OtYFCRykc#`m%1 z!C2}ybEs;*-^+*AKiToUth#Kc$V5IGO_~N}m_muX31?MaHCgb6O})q#dvZ zmU)q94_GXxa0VR5X94TSS9Ah(lY0dG)WDJ0x|Rhz8Vu@99OUusBoQx)U?YwH>M2W2h;*vo`N(oTA8I=3^hJ40s)`p_|lbUDVBHBdz` z>e2USR2N2gwkV=8tue8{l{c2Dy$zYFf-|OvTAsgV)Ua_%W9fyNtQ?juBI9O0>s@f6 z0<&+S^y5Fm^RQC~epI|;q!pZ+~hl2F(5LL)5*8+nS3?asmqIrRb`}Je2Zg+hk^T*y{U+W%cx=VASq9$19h)L zRYp^=&M~QIE9}-Zn%bnMcT^!$@Z#IHP&1pYRwsd9O&y4T#$5f>9Iw;+GoM52zKMff z>Kuy7De9)ijAWKK;_{h|0bg^~wkPhmYC1jqZN2Of;35 z9p$ihO?PBHhg20?mL+WTY8&e2+c9i?vSiEJZhFyCIxeIpC$gqdm$q0KrR;PC$4}2* zF!?P6S zKY1%MFlMi!FL!cWSLJUs>Dgbkz$|vem6w*jpgXTg@cW#E(IiH%C$p@@;^%8EUg&rG zv#-LPEIHQcdsPvv6ZUgf{haY4{Q_V`iR#aQOV0x!FI6{)nK?#T=5{7nh1={tnT2usH}da3y!Z>c2ZMq3A@wax@yL|7vo80W z=3CX)(AUDmTFe)$-Wjgh-F_14qK1p|nb8 zyxdyWhrC>Gj}~>+OyaWO1el+(zEKDm6DUXruUEC=&k}NcMsU+*7X+Njn(~He*8SF`8~8L7I*@ z$CjK@qt(c-M<6xCKRU2l;NRjU(X}Jl!r|ZE@A9VY2U26 z)zzrL{-+KADJsLY4spv($a6-z-iVWatr8K!S0|!>Qg4}+K;6iUo4Ur3Z1pn5YD$u) z0IGSwBjWuP&Zp z=<&_Q9seI{ljX;aP0LY#X2~=K`tvOd?e~3Gidzh{W2qM(_c6v;XhxZ+O=I%vJ5E)j=NKi-_HwuICaaZ(6YCX37R6P*wFv=)(S_ZfaQlb{g@MQCxY(Y3BQ4DvO1 zc`fuQ=8PwT*3GUV(66BDPGLTYLbCFzJ6ciYVucp^|vhND7Is)__vOb zx%W#v#t#`f*b*P%TLM#d6bKlA|!|f z(%P0L7zEuleb6y-B-w)xsmSTl(bwUUXfr%`ez=4FI#Zu%@f#X<{!jsweUpqUf(N(Z zP=9UqlGr=&ZAiP-sthMh?)9%U8xEdPZRC`W+I!fXy2MYqz6IrqGd9z~76X?MTC>k@ z?eiJmV&qR(tiK*pcK6W~u2wZB$e(!OfT6WJE;ODNo`N>sG|TRBd8p}3IF0IksChuN zV4`*#UqII;ZJa9AEV(txqTpu#94iysjD)#v7&(8+-3(0nbBht;F|}6Te(?ItV5JLL z0X5VIYo;l4ew$Sz1_7_dbK?J^&CVy~NLYN`^}Mj@E*MMuifd0M3>awexJQZqa|q^p z(y~y4B=*I3uVu9n7n>fX+67#V(s z7Tn5JZ+aRBV6WA!a~kI_EPc{DMn9b^g{xndt|mwI_B&@%C5=@Z3MQOWhXUN#HRCw->TH&O2e?l6P7+r z-uVaL>zr{{wf^rJCsLoWCxKw-H!ZtD9$sZMD1%V``cf3DgEh|hK%bmhxSW(o;l+u@ z5Yw~Yr6I4RhoNk{i{ZXvta22}T`#-qOEA)SMIFv#m)GoE+F8=YD84}-W#I@cXokx@ zz^Z6|gq{A}{|h!|JcA#eONQteqAVT3R}y&sp$%g_@nt1j+B7$x@f<=n=;2v>#TjZ; zr{kgAVqI=z9yvUE^!xR#gd$aQb{%VL%dda}{(;1b=XFxoHv!bc-OOUCRDT#++@ePd zyUDf;5YtueKS()0#YyOUmCmGy379bpDX6P41$`;D6596}8}@&rRC1A+gBy3~H;9Nn z%C)Z%C?N_oZe>nGgJ!X*SPXGT>Igerxg5xfp~Xi*!N=rp7!bt z@YbGYNf++DlkonJ$PReKDJ%ig=FFZP!4*lDm%QeJ$qMBoRF5Ob7A(yySUJpmsLM z#mG4qOySh(%A0$enb-PQEJ7y#{FcW)t&pO(UR(HNo1B=%k4G?UYAEf4ZowaN!Q1VM)GM-s zEt2c9gkRBON%bzi!U6mHKWF)3lN4sk9*0aMeOPLEO2;gn+Hl)`t<9HQ{|W~gd3^gDv62d%SM{CyOWOi zJUjBP6xujf5k}5f0%LZDGX=7Iqy|lH8cRVBA88vYlGC27J8tH?#_zhTKP%uA7+pZ} zrOglcIoorYF4v?-RzOYND__NpD%5VIv?ZeEJ!117kCOeQSv0yDF`u7!D_jBFU-B9= z?wemj5nwlh*{;LP_4liuNBI2F0L_2vKNjIFo3n$NUmf65rc&(X>wT|^xBVJM?B~-@ zFIa=&Fc|Ld`Ynd`w5!d{`E|m-M$*zoU79$8pU}&%_2-5J7l+3`zy37|v1)-RK2d}S z0jn&l@4n@WXf}V3n`~F`?kiqassSg6!jAEYDow@yMcD)4^|ef9Jf3~eV$S-R2=rGef=7cT)&kE@-n1}FG0w0-X9e6!zoQFC2~?U6n? z2H;)D0_ctN{s9KpHksQT2zizsS<&OBFZmkv$kxrTWo>08TV03mF)rwQF@-M*h4K|9 za88M2JAW5`MU?S4ff*AIoWeHPAp7idYCiE%vtmK(TQxENz0cLzv<&X#ER(yDxzt@% zqn}_0nQqla64>v{dZk*;+as#X(qm;!W{FBJ-Sv_ps?%Z|t?SXt<>3UEWFdsrn_p>L z9wAqDGiEm{yS5swDvrWJe*cLr`a2Y2W#BD2dc5N(cp1ijYF}uNP7JHltFrBmMsysv zfUVF{{uG1WQr0apjyib#k zQj;_VOk1+1EO7^eQR7R@(@Y3cy)Lr^jr80*Bl^!Km1<%g-nF%AwJde@e1y>CK1ohA zY_fvODIw>`K}o&$$rJU)FZPeQzw^-@l+Nmf&gl?Z_xlh%3h~FaWn4rh=_(Il6U1A!(R%TmgqE8c$5C|)})0^~rI7H}gPWe`3nEtOT< zidYyM09rEv)v~(kbYB2m?^!}%*r%M2%u>cL!)NaSQXLA3qS!Kcy zL3LdYOvf(Zi73wWk`W=gHDz#mhW>=hN!^{ky2Wmrv#QqDW2h~{6DS= zKb@GO{yD`=tH$HgGhvg(MnZ*MB_nPQzhFj~1VhIp6cV9xPosw7j* zdz(-3WJYt+RX*q#C&T~Ad2JQUnjLI~E`0srF>`ksbFJv(HqKa1PU?}`7Z29_RNrqr4fIo+XSR8tO?*+PNqpHOcvaDX3W7@R)avqV z{s2exls==~sJ79jE%@5?YY!VH_;V<^Ge74q%bH7&?|2g8QmT+Oo6EFyg@mLXXHi|W z;X#;JW3#1Vdxd?!w+@TxW(bE{wvKgy#pgNBETe=+zOT97#OgvSSUN&i(O-^JTAq_M z*L|>AULDxp#Qq_$rwLZ~jk~6_R^N)BMN;#gi-t_M(DvIts-1-Gl-Dl_?JVo4kPaWT z_Sr5bR}X)FJkn+SHHCz?uuHC`!y1`Aqt#Ew0gQe#FL!2aT<(C!vo@-I>Y~u1+^I95 zRDZ-Z#yuVT&y4?pFaiXWZTG`_7r;Q@|HsOK=B_f+F>77dy46i!oyY>(55?N`Rbt}k zr0!auH^ra{sjmKZ@10`ii!_N2-s;d|uej|(&yXNz7UqG$-Hmj?e@;6Ag6i{yMSVi) zBpD)Ws`VOvi(i`({mG}8Tu6P|RYQpwTCGG>kcVd|ZH^P~aS6T_LyC+;JNqt@NUGV8 zx@C~PI>kzPO;I?rC(>J0?zmDdJ&}51{N1Ar0-W@n7tY)U3fuM)yBr*jKNTjjr$NSB z$!2{1FXymTOg>{5+!5Cx?evMni8GZy4ps*A`4xJ;@eJG+@w<`!o|knfdd&SW#H75W z%AmZem|r0YD}~1R)oV)JL8Z7!u1>R$L)R+W&^b8?dr_HUoOJw}zp`KL=7{eEaf59h zY=VgN>u=B7Ic&~1m0QTEDO?2np4cUMvh<13I$KK8<5=vzX<6K+3KO^S)tX-Rp3|V6HOnL#95u|b8 z)R6Z&;#bv_Nxc39T277nwUC*Cdet<~YiImNy|(@aDps=2Z!}v}hD)tv%b$t#iC27x zhjU5!Ev>!LiU7cGF1nb_rjFfQ>wZ=LwCJ&3O|iIG~e|2 zU{K&WC+Bs=&AZ2R0(+MvV^6C-{R$Z+Mvs4RE!IhYsrOn)&ln$Jgfw;!#mauFEh2oo z74vSf?;MKzUNDUh*jHKC2KhU{{iTQJ$)Y0#*yoFcZ(E>SV`*+s>Xsl@^Mw#?GM-FUdW1laL{2X(Rf!DQ9L4Y zx7M+y!nvExQ}cMNzJ6O&>R~6Bs*r7_TUj;hk8DPL^Pi$Tf4^HT;*GetKOEh!b<~d| zx_ObNe)Cq)NWKiX|InXOleftBRiM0lGa7?P;c;y(YN0u5?4kVxLw>y^+|?g#L^Z>u z4RZzA?*?+akwi+f28sCI$4a#$GsfR*l2 zv6tHOENsHGPiPB$bB~S_JO0avigX#X)0ey#GeNC(uP2ve=IlcBxv&g;RM~6`jAZ9;B|TM4!x02#JrDI}{5fzW!Al zubOJf++;FVF}OS}%llL6XJL%2Qj1AOMpgP_s9B|pN_j&EHER8{-UoUnKeY$Gee&M~ z`RW`SwisQn9=A)Zj560#@pEPn1g>7cziygnp*s#L?6<{;T|YCZmcC-NBhvBp@_f}0 zGJuhGSMdmGikoL1P)dxsFP|$WjX%k1`~jkdQn@n$tFBmaUSyt2XKQj zEby~k)zs5c%{TJr#meFMrH|dIr4FO$n&EX>ij0PArzG623WFA@FGtPz4Cq_Nf7zIleCNQ;V^GNma znDgp%qveQ+ch2N(RdUQP6|6_F!&x$s>TJC#v3q)US+b@&GpwZ$EwMW~gObQ6H<&_J z(K?#eI{bphnzbC4`%;8<44^_w>*{Ve(HLX7nkRU5~lzur-#Jbd}%mhz^b{H9}# z&$l)4g`o*F5H!&Ij_L1b?yNr_U9vSgMoRL;Gpubt5D>c??LWF4&Qq=bGU`cR8+pQT z%`k?~GBmIK1v3{a^btELM9?Foi6~iLS?LeSl$;_?N@}HOxmS2kK%?tt$44|exW+p*#+*Q~^!M1;rBW&*FeftV1|XVI-5JiD#K*=w z_58RQ(Gzzs49sHk)wRZ1OGmWnu`F|!mP-z{SQ|G>E$R8VIM_y~#_9>5WXn)=Dy4Fj zE;nit{VK|Ru(EDt7LQ7q%huPJ`q`T-sn~t#!O&zSGRE_B^kpkEW#i7QSf{NnUxhue zfR|*wUo8hy*o#~m_usPVP<`Mr|1;e;rtgb0SeE+u&z?&$cd|RW`X)?8?~0(-D$ zbf+zch&3dhK@(E`(QO=Z7H|>jnCrxN?1hp@@{&1k#SsP{E!0~fR~fYJoqR%`XGR>< zcck02zs&36+_+Rasrf=%7##nf(spP7R0}hwymd-+xH@f9GqDtN>yyBgu1y zo!;$y^Ue~#mY3QeKV4aGLR9LQS#QFHr~XEeu6Z!D%9Hw;g(}mbO2?oPlB`vMQ{q!8 zs5?Ev#%z|J-b%B~mn80KZmG$Ka$xV;hX_;bO z9>uK?gwRR$wvo?H)~>*M`ke9UKAV=S?3rc ztA)O5!MFi4;sD`rCFMl@8 z@0sn{G%_J#909;U<#So=I^Zc#)bGxcvscHKH!8qa)Sq~$oKpe3!7~`Yh1U5q3TZRT zY2~5st+bMD9k5%u9Fgoa&c{^qN!nOv<=twKcaaUfL&3U5-t+h#+CilBdaLNNd$9$4 zdWi#Kd8@7ZAY?8ntcG|lRIPDB+#SC|4w(>C61bFt*H5sDh?tg6k_VF~D<5|-{UaSr zU0Sw^A&4IIlOy%I>~EQS2X^sE#Cxl(@`xTIvlAWaY&1PC*A$f7Rqg4lJzYW4fhVA3 zrVG^#^)PK0H3PHwoQUs9GiIq&Vc6C_uD@hYS0YBpsQTwK+q4*v1;`|~aa0dBjFG!e zJ4pvpx7jetE{C(+t$Tq~wbsPfWv;5!_!L~x2Y8f62diDSWp2FP--8#Ej%J>L*)Gm& z`-&l7_%dlLQ`STo6$)J?oPNpaGX}h5_h{@4J%&2nQusc6t-arq&iTd=vdHxMuy(?kdl$>U!<@iC^MYt8hI%# z4$rrdzw2_ZMfD#U+KQ?buM$e>)CS0%hFDR0+5drW8p*NZ>oj3wLpuHZ;{3W7P;_z|tx?s$S%04vqoYh&m zi9y@3-h>p+jTv@ExIjTvAFD=aSGWAEsf}Awy?vZ$FVEsK;bp_Bjn}UtUl~ijXJ3RY zy>Twkj7Q@ML&J>GHI(*!)FV?^0B@UB6cT-C-yH+OCoiW9|dH{@eh#A1Z%Y6 z=izi2Vs)iI$gSklYJzuOw{5cG->NmIw0ck)m?|PG9|e=sBzckMf37AaLwtUA8ChoHeY&GF*SA`PE z-<(NEs%d@jspu?;{WZdJVj!5c;RnfP>8~8q!ewKgb@jjErd+r*R)gW5Y?4nrWt%&u z7@GP}xWJIt_edH;1p;CrW+1DN{?%L)A;oD$6rpo4?lFJP^2P)+xJHNf%r+{d#(2WqbEOg>k?uzKQ6-#N-B)7KORH5= z(7FB|mj0!E7=pmVt6ElVHP#X<;FLs_<7RdRWS4HAYT(3P+EYE-WVoe94 zq^XQ=d)MwZaBv9hH2y{^5EB2aEwbw*j26Gs((BK<|FfY9DV`ju@0rhkMAM}jAe*RH z>cJ8QjEEgxe$H#KBA-|inIgo1`HUzTaa_G)Jl0)d`+7{~BALv&Nl&vDe+y2ONn4)s zUe6E%Sy@#hUY4S4&wd6S(3|l>bPis6hTcR1m`CY0xQy;9 z+IeNo%0jKVt8}TMf3Jo0YP4(5C2Rs?lFk#}OM_7Dt{M?)7}=y3Ahd`F+Y^VXS01}1 z!k^6^Q^#jurtbabMfr%U-_WwtvyOs42EMSB_BIXZ3SJ!Pi#kiS(pL9Msy6JcpOvYt zO!(W=i3#1rWJ_2y?``Wv*JtZ8Au^ZodMrs29_)@rE|4!G z*e%rZChPpK1@fPq)a=Q^1hqG<141yz4IiX=Sl$rkHbwwsU%4V3lo6oKyWPT2Ama%y z4%>D2#FxShnZriV@wFcC{gGu`u7=@Q>Qrc~I3y6xOAuvN96x0I4&mBo9j&@vk=w_F z`rzl_qI|F=ciM}0q8n}DYIpOhhIW8bB#f`27&AV-wm9|b0`28m5W=shwPbU8 z;1_s2q5t%@;o?7Y6rndQ@qeJVWjrk5)I0^ug9ng`Jv=_df;T9`G<_Mc0elk;-_mja z8X;*Bl zmVWQGC|%i;`{3I8D?OoP@S*+PSZxo(nR2nbAoGkb@-g3|4L^wKf7+&19nnS<9|ud4 zU-6ea7ZC9eB)ZE|U9aomTNoB~q($v4{0JmrP$p@osA|Mcx)JuKSKNH}ytHyNrrFO$ ztDzQL?);UE1(OYv#Y2bH%E@cNhFidiV#yFYsMG7@X!O$s} z$S>8&7d-}rzm0-8Ea>!PX?T|OCdBI=*9EICG_($E>NW?TGtjqZg;uC0FYDT$JmrTTPz1Vj=4Z(HVyXmS z9AfhOcNR~b8(xpqxjfjR5V#uOVD#()L9Q`&VcbU$);TNS8IKN&xL{b~q(J-9W{`)z zPJ;O69k;-n5~4l7K3j7=|QpGXi; z;hq-axEX1jUrT^=qFMVTeDu`T+Ayzvi4UPTMT^>p=<6o<;kusUPOEQ87?59Md^3tv z44x8BMf7<-i~@$vq+6v>=VVWb>I3i+E;kLfz1X2JJ;v*0V@l^;uV^#cs1SiOjIqtt zAOe+|6;o1oeOAzwiP?4kZhrn)R9|s!#Syo>f4?pEc!wZA<=;Y z^*&nz_A}D{6wm_FOFgeEYT+`!F(pBdeF=p&={z!JnE3!s2&a%_A>oU5EXQ zdhnvYBIO8rGnHHLTMvcShweQwf)S_l7nx|SXx&cY*JH!`d z{;lm&y#AF3-dGuT^Xf~#h{`hV9QcKOwnsO_YKyKv&krLH*>h;c_LBSyQOcS%^JT#iT+ zCl9KmH>D=k`1Dng-4sjR0`1?jA~2n5aKZr^1bue|$xOBAmKHAu@48oV_nvg8TMmUs11L)3&!#U2^o5^p_XO9*qfJH`48oMApw zGGK>Gj;N~lDp*@w*1RMeZp@&^w1nvhW^f(795oALyV?qHn59Jx2rgHND9cctAgA5Z z&cY7iB+nE6e3RKusom?Yd=;o^-i7tnWwx#KDUOO6e!>Rh<|76~*=qKib<8AY4w-k) zd$ba}LJfre#rN&v9k=k*-1yZa$K+;qnJwoEM#2TMJ_c;8?>=4dOORX{*d^fzFu6}H zJ^T0A{2NDpFUR?Y3fTT)(m{8Ht*0I}fop1gJ!vXSVAc;Zt&&}a@DaRaX^X&jFX)`V zTUU$ge%{?>q?t@X&Wt#Q^qR@DvEh9D7;}R++Qsfz)4w0n0LOaTTKGt;_kt`K_Ij2z z5%PL&>77aZYqr+V&=Ccmx~CY!dotAuJ1|dL&IIPbE^(+VrQ&Asb1pnWT~p#PTL}Zm zzs>aSOB(&T(^q`j!FOFO4=o#9iD*@Pcm-Bg%;@n`x+USYdP~fQ#eKW#Whw85A-e1Z zk2t7c=WhhUuBWK|+PbO#aXl;wP^p@anCrpXWSACkk-$BWCAz=}IPizP@ z>EvftyJ54rttvAnglFU9i@|+;QEt9&bV~Fv4+)nov()CN+a( zwFWnPj=g0fuNknznN&8m;Ro%DUM;(Tga;O=1dgmBtr)s)4GFI&rBm9q6~yN!E1?%e zb?x}Ml!X^$fv~Wd2Zcwzs`_iI3Xc37%BK?2>qUm%ty^EDqjIjr!9WT%!yG~M?`UF3 zAV{b^NTHrR6%>~M zLa7%Z2DZfaQT65cKec5+`$iD8rw*vFMzZqFSZ$$;Xgo6tZJA6mm+MBmH<2$ud?`Wj zsr5Zlmtz`BqOeEFkK zG3O5$G&h4D2JhJ{U;4Nd2I5mC$lLrg`SD`wlaDX9Gz5E;nQK>ufou!yx~u8Dn8)>LLDglUXAY$ay56J@vd?t5)TY|OS}=U4t%`Ljhj@eRiCC-bivct8}x zBt^U)vqICd3nNZp$cz0c3_C#z10DSUKOth2dw%pi1KjC3I-kM#aT7?zy&~Cj{j16J zQv8oRQjPc^Hf4c!2YYV&*Y~+R4vcmp!3(mw91K8NAnRh0eiArN`C_ zb*J}F;o5Z?&0 z92&qVD8~QLu)1D(K|QBiL)d+4C(wx?z=4dq1X+$qmXg`DMbi!)O3Dy37mW*+Gy{Ajp0Zj(502O?&5X1)K6DKS{-n8&5ovHnw=*1z&!c{D{JJK$HNq}) zhIxCgC!i^2_oIat`N|)OQFvr=>UvxWQqaF`*?$}_m*RKAovMofY*=6SSj(%^Pf$5;&6Q0Ywm;u zoYD6r#wS(R$Wb>@A&7^}Rk~h}9njy;gZG=&8hG;4+tQPz2^G>q8HS2NA0<2^A-sISJP1OU6-f7Tfa005zG}0L{=MgiEKkJSpxvd@pG3&*8~m8rk-MOB zz2e$b*)YSMW}a=Qmkj`YlcSQssl3znOT**$+K^G4#iVrVp_&(dJtQ;R_N$uQleRqG zlLRtxCPTtNpy+=O320JhTd1xn#WX4*>EHXa&)d2sbe8au zqHPk{PL#3clQB~Q^KZ>Nu1~<8T(B{U_-zKtoOzIS>Cb0ue+J4nt!pp2H&N{DO2`da z;Hgc22uaYR)E@M+^01FJ*z*ClzY93rB6{Ko#0&zraOt*gRs#xId6;Q z>Gx2ff!$R>=?`34A3tKJWTtH!f>Wq#7A?Jtv^JgG(e2eW^s$mRw9p&Q-d%Q3+BMe1 zfi~~Kye6D~!JF7@Q}{lLV8R|ZbDr&;F%kG%j{$ZZQ!`BGnU&x$L33wzXong?1sPME zQ10`eVP{6UHApdBH0WPZ+4ALAl8<&#I>HEY;C*V;>;B*a3g2O`IkCxEJUV{NZ+>9b z4W^$mcbA7`B(Q%^aSGe;W1Zf}{@SDvaSvziO?SIA3G5pPzMom4=?a!OxLpytD-&+w#%YHtgL{uNwY zcA1}0N5aP-2wqDVIU1;#=h@cybNNi|S{WOWx5)6Cm%<4swq8>G3q04W*frkB3yL3A zBbs-GZ>|`aLxVz?=pMT>Ijj+ExLr8ZfQFf4c1Q!)-^G9T25svtgZ6Dd;S*2# zgHLfXJb7jvurgYHe0e>Q(Krm#;paC2H=aZ&?Bj{F&QER> zu6+7jQGxHNvSNlg{eISX<~sC;G`MId_V};a(!vo$vd)j)o=zCXIo>)Np2y3Fj6dYbj_l%kF3H&_F^uoC~AxPx{Z<#sWC zT|tX;6TlIBT8l@|V7P!m*QL0#wol527OcDvx@2PF%h-t2Y~A>eGh@bOXV#M`rQ+d><;Hn@*?bHc@{f@(}OzNiXqPuA88Yx+7^AgO|-ORDaP!HNL#$tVvuUlL9ciN+d$hVfefc}CT z3@zHX+pQ2Nq`fJ;;D?o(VR^r;hH2%JkmDmr8_sJ615Yq}0u)hs3*F9JZ|VtWnLJ4H z%mSUL%-FS00cZ-kSZ!Cw9PJ2vRs*B>wowj~rL?p!1UeKMVk^q>``O9(o_bT#53ElswB4+d?hQum#`Mu)i!=xZ((|g2`&~OW315&tiZMW#aE}uSGyr~($70G*q z!^R$lF8$Q#5dn&ku90;h0_mOnHo)UR-HiKR#f6im+zWUka5^CV5*P}YJ)JCXa57ri zhwXdK8i#RsAiDgZQ;&ph8Ud>TCPYzb@#ajPo<`d=*V={|YO;E(;4iH#g$O-nj!UmZ z$L!0{d?&(P|9*$tnPy7EE>fCW(1P-x6$0c^*ftvr@Pu~mKwakcgQpieT=Qm{b0Uzhlq(?O=VP)6H&vrpFxEBGCTAwnhy1WoOt+=J z_-0N5qmz0|zz7#s3Id6C^G5^suy8UGj_=u3tFI|gPcdJln|exU#e^6G_qIQi@CjSO zqyhjdZ5{%J6=KE412h>-R14OSkx&NdG72v5KOZ%7#VEFE9BFgQ3VG}Z!_84fT~PVn z!l?#ms<`tJTRsop(M4*xwBAkmEc5i${k0(=x$b|5Qe2ZRMfqK0h@4`NxZEEoi?P9w z5dqi>_hJ(Y@{U$#;VC#94pyN7tA;R*qeG@z`Gj;XUyKJ2IfT{IPC>MFf;ZT0Snq zrezTz!qgN%@}{lWg=Ma1XYgRGO{oKw@F(G$q<&k}5P^UM&^b0TZ$*0ZYXH5dCwix^ zCq(Z-11PNi&o+S8GBBauTLPVLJ3w=oNq#$tnDFj=5|Tmv)&1pvE*Ij5gli9ViQBqQ z* zt0~12K;|RQj>Ey zKENh2p2o=2!h7lQ-awsC7FhT}nbbo33#q#Vb0k8odXpeqZx2?Phgp~FKNDBN_xVIG zFaU*56Xv~Tx4e!q-N7jUo@Pxm$IQ>z@*zf^Mt$;o_dB!JPQB7)?Gll(<;l+hix}GI zHA)CE_lk(!YNLci)dhg=j|O3)i)ulub0N1=sIb zV2=Inl2JsE_*8I%YP) z^8ab-%fq4WzxQXx41=+ZE$bLFNFigFt+B);Eh_uIJVS{{*{iXy!_dNz2x+mU$rj3z z?aA7PED@ndL`7PDuQxrP@6UB{^+(sd&+DA~+~>Z}YnaO%&Q~I?^b(tyt|1WxtK!Q_ z`>JE4R|*umLKX{(y~~95A}-V#Pq|YpKVJ<-E&40c6$fB|kC^c9jUBMTJtMyCGLJ4c`nNhL6XqGQka926qGPD{AiJ!%Wtvh&z8XY# z$jf2`z;>W$5|)pR-U~Ohr&oiGB^9#uJW8%yfDyh0Fwb1#b3#aRf9O+ z`CBQKFK3r;FcT5E%(vKf-r(0n74hbBI8Sr_-i2m+N3%dCQ@k_=rR& zRF@2Fc67wuTWZ{ly;~7~mNV0Dd2A`J>(k5E5wscNV)=$>{_(5e7n3fmW)ibgrnT&uwc@+tiwTzF4TCr_gn(`eJx%Q}dzgUiEgUVLV`urMCe3aL%r z#Kp>n^y9=zw(jxQuk~m^E#qH>W?jXL4Cy%7MysBM74Mm~QPmk)n3~E#Pn6 z;p#N*gMEg1YorUn-<`DprV0((I?q-IWx<9V!6I{_(B&d9mwakxtW`^`zhN27sQ@IY zp9fkil{eSN_Fb3^<1|!(&+p;?Tt^!=D}a;cWIC!dxd$3a)9ZEvjVh;?5PebucU|{kws{T*A2c}{ zo~k#L{s%Tdkkly&{x!8~Gh#G6m}H*t}sLsZyii)4Zn*XIO${O>VCV7;a8LF7o{@RdJ{ zY|uK1TYqROXdkU2W?pN*z}O%0_SF6WdflHuZ5_$ytnc9+MWamHijWA}ih_oJ^z@`m z#fZ(BxXuXB{~3-jd;D1!!wz%f899bzX~rh`LFJ!14$ACk9lPvmz4VBqy5&;d)v;0w zU^O0q?fI8^Xw}qnH1v>WS2c+#bCyRQz9a#uAs%@^3q`bNM0dk{0VitnnBd3DkjQ=( zDP4O9I_Ev`!~Fj!p&Dly)1#cbDA(0!)84ql%ci-MQO}vP<7JO8c@=x#e)&{^|Lok} zRnc-7hrdm9nR(Gte81{RfAYKlVZ-9z>9)MfD%gpjp6|)F!Hynq=E{W%V-Jqxq1JaI zqza>=m}!3TA-=L((~tG^C;sv`+%%2qe2$u@YS_2&yZb8hw#dW%t70^GLI4f%J zWTxk2NwyS6&1myLC2X0IIB`+KZ6lzraUhWS=eJ~l{Y8HcCn~-^cnMI!HBBux;Btjd z>t2)GJVpyE-~VlQv;G~A$2f57G>g>?+TpWA>$N)Cvu9Wzab5A8_I1v&qq=9{{RXf{ z;ddm+x{vCP*dmDg5|qM(*FSOehjnUtugB=Y0{IjsFsX`sfzR%ROGYaYjo9`*S_Fpu=?vDkVI$CMEm|8xwSh?I%NSZUpou=TdGVxTwdjk6MV^r z?*WHASZ!|gkbb7nCK{~7MxZ;3|Fuwh4e$_SrQj_9W1|sFcz|tqS>{j?Oug-mT=OE_ zE&Y%d+l)(BPiG`w*P>BSX{#49$C1SZ<8{aBr816s?EqX{-|w&Ycj$xbW*4*pqNrhN z4R-(WeEvq4=KJ-pVSJKHQBReRj`5`iNN7xOOV&}!n47ce33*z$vw}a-4 zy*8?!*_bKQ35TP`?m};JcvdrFaKD=kriaYsjQrL*yF0)o`fvoMLBWsBkPkMGLpPx; zK694#|C&SpbmopTmst@|@Na!b8-62~Enz&ZtK$;ed60Dj?ADQttjRzOW7Dt9fc%Z0 z2?s-}Nj)P%<^gR+n3bVD;@}FW%x>0YHEH77zLf#^6>SN0sL#$Sc7I(Wg|*E7V22!h z@Y#K5p(Q5AUtiSnVdCSh$UiRMhUeINFw7lr!HWDgN#%~p+J6oAc}?)u6rq3N+im2>6)G9rZDEd^t21A=2xh}Tlu|G4}) zy>Kz|xRsOw9TtEVuVn8mTCS`6^-G=c9oF^dQbb7YSY4GYTr{)eV$|yI^g66WL~YH* zzBscSkE({<3%4a0SHk4_8A6l<_{F}D9k2K|Hq&n>zWfs;+h8aVRa`sLcZ%}(l8C`{ zxxi(q^5UmnbK%`s`F#JJLmrtjyN`6ros|&a&JxJZaLk~bIOSyFMyR_&>8WVA(mXck zV&QhJ)01OCC4^1Tcs5>y#m4-x{&uYidoyF?!oazzjZHSw4xiG=o`M&#w`2OTWKZ-V z+Dq6e)IHnoh506qWJ`2BTEI$yw`x8HYdmeNbCEFL{@Cov$3HQF2)t&qEMAi^sF|Ta zNS}7_o54}ih7MGSp1GRs^vc45Ke!@G()|ltCehO31l6A>vwa4AWsTSBfRhUhgp1xI zM=C0+638C^bPGA_qhBMKJ;ASUn@vN-HcZ)VX9Nezr;nDS9>%FKIiyf+3$WO>^jCVZ%_A z@Z3*5_+6^?h)jXYM2dyw(rGz4`SPzIFo)dF{FgCS8!>^qqLRkaOcCl0OUDpF<`Ey@ z>gqVfK^=!n^#a+S)4R*t7EMcj5+G{|L3pMzsvR2)ZGN3_hLQ=2Z zNlojpa9i?GF>s@Hcvd2M_|+sFdi=dCd<=PR>^P;~Qq2_eAq+?AvU^s*v=%0Ru75gX zQfwgZ7{lBD`N(f&?TPD}QR!b+Yb>5lmK%)8U2${YSVvX(R{8{OymQu${`~6E)$V7# zD@`Ml7lJ0hj-} z5!*krQ&!N=3za!R)$=ww=5ZEzfU|YU=4@=YkK!&>=KqJ@dKl<*EJ(Wp==Q6FgLa%f zOW#0~y>zkA_j>JGft=L~`2BNZVP-{$xZK0^-cC1xe|>KZdUxvG{Tg zCf5c|wye&s^1c&%#Nfc^?7z$Xs$12WpdvDiv)D16FzH`|<2R6fXQ$ulcKv1*SLb2< zG}wo!LAIGr?=*yf)IYBhCAIQVrVfln^BHA~Gz!zSNeMPN=Ap(qycK@Z(wDvFFGM zq}e~myYv_7mozlWwbZ$KiK)F2KGdpu#C5TAgyq~|6c?Xh?OxLcohZUO@zcT(f508fXPr??Q#~xz0($oTMPCAL_RwLQS4PH-x@dxcu8%` z5p9xpS1RK8zT2hki{z4v=>p7*j1l#MdhuM%fH(6!t%B`yk_CZ@RI4ACAG_%b1t;ef zlHgC#LIz6IW*rkx;k0MVp$NQz>Axb!hyu=nNrYq4zP1u#aWi%FF}7~Ux(=CLs(Yhn zcyzga0nPS{2JTlqP{e<==YXqjO=<_u{9e2_LF+_#^lYZ{j^zhZWp@zDRrlg_q=4=s`jG5@*=xJ+F(S~W2rN&ClZ(rxFzYG zEEUXjGBh?~^}=ziRXK9X^*fl{6&?SBM_iOi1EgE}Drq!mK=9nYX8=$#R%_zm*mH9yUl3hu6f*+`) zgAeR`zdCp*L{{FMab=%5#ARu9YNqe`;IsEY8$l863-MbDiZS#4_1>CR&&A9udLW{< zZ%~U|@)&PNWQ9C=YnMC1)61omko`MU>h^dlO+BVhNq=trDO^0tN92~`&G5Ucp)i`g zkQptJJlN*Jo{AP{1vIuzv!b-VMyQ24@lbv-?PxC(C71esM4b#cBpcuLhT@CLuuUj_ zI)erS_3ELJwf;PSK3Q|8B#T*z>0=0WU2(;zq?)YGs!RI89HjRf ziAr9le9YeT@PfZqTM|3@aQvl%{&L0}j4o^06n=`hCQ^iD*aSS{uF_)+(OXn8b2_FO zK1NlD4h8W)&4c73xOrG_(H^n9!Ys5cSJ<6i|dN8*T0tg6UvzR-BT9$b4hAdC^}5FgV~6 zw&RaGUePvw>qMB_(=D5DEZse&8Fz1JrTFG-h`jScC4cFyEZd=qe`AzDG!Dweag=k{txcYr z&wM&uZX5F-k8H*C;Hp=mMM>-y(AQ{8v2;4@#pvX134nGRn*IKg)2c)GpvIMS_8W1Q zd^s6aBT8ENYrYyDEVk<>&!wHyiJQykFo)+D`JiqI7hLQ##fuub3nl3YdH3Il8AO~# z(yeuZ0j+f@;0(wY2?DU<1j+>^7XADq?8u0j4gN!Qh9V%25k&X2LOMv%Rygvgt*%=X zPrrI+?X@d4YS-W}PtR^OtXpB%OV=Oj(+@gT-#Xivs6VHM4@6#y#N^^MSi_6#-rBlN zDYbD&kOgRC_(;({vxP)Jpp=OekwT zRsdalPpLc+2lVR1)1#7L8Q}GeYoNztmG}A+S_cWu4nf()Sg00DOU*Z#S zz*c|{vb_|$0jp|3@D?ikhh$iwDRN=jp13sJ$k$!nAq;fL5ulmji2~ zJ=FuI_xDQrqVC(HZl7PtyLn&mMrV{??6k}|q}4M9bK%)|;O~zHwFX-B(VD3}nckQK z%%IIx(XikCl`8Ay<~GrT`==)lQKu5$1+IlfgpP4`VF~3TYAIcoKqk!y<3Eg>pVbDb zXy|ZEIuL)y%K^&1UR0Pit^u2EYgEF1@SFiv1jO^Yt1P|*B4{v9vfY&+CvFP;!330GNlR}RD+$kcW4Fh#-X+-@r!yWX_aX^C) zVPTu~;|R4B3eN###*epetw+1Af?_zqyT#-`pgQKf zEj!}`?=5vxNA&UQUXqr6h&_VHN0XIfPoJJD(CXeT^-*lCAWwTG&Z;z!fMG? z1@rEMi0QfW-UV}LabiFt0%yy1jChu}E5g*Fs7!>Qe#nbG!!{mtvM&cnM*U%63B?dW zG5~~&2&ynsAX^V&5t$b;RIk$_3A0tGTy5G9x%`x2uV6NoAXD89@CHxsV%MT*&J!H9Q!q|G zt#@>~-z^BG#rCAXr1tR&biB7rm$nGcy>kcwciD5oNFtpk?Gw^2v4j=jYXvz-&QR2 zg-E9KF3x|vaVbhK6Ay;oywPB5A*$n*Pg!%7U^*I43@t-EH<1m`@5)Pjh_7#=%gTxs zX1Z5!_K2yuyWu(?sE$;of)rD7!`J}}{zT5X7IFJY>*x)hY!TeZT%IlHH&Ngk#88ft zQIn^n-;SP~Ym7*mxnz!v785NbuQ9*_Hyd3v9_9`Xb9KHAjXd#WXG&)&aI4^yPXq{~ zGAGN@uNysYNSBoX%!3I+kW9MtZ?=ymL7ENB1RLnVwQj3AV~3&%V5J}!4}9@_R*E)% zFxreoa)!UI45^4b9ZJs{m^P3s1fQ<#=dQ{I<~qQ2#J-rCR zdgCdHEuLvpf|Z!gin!s->)Z9YK~snHvyEQ(UQ&VEoWg2ITrtGnGj{Z$nVy z={A+Hht##=)&VcWW-(0X=AWk2pp-D{ZPg+SSZ!kn=7`p^rL79)UFzMld}&&!zCz zjp+0G_ReB*u>od-%s6ipz!H%^&*LTeq{!{kmGE0Z&RQz3&Gz++I7l~7Ux0sh|4>$C z>86V5K!SWfU0&}$Bh;SzUl7|#=cLfP8s_q7T{5!TwcCB%!|B>+!}el7wV+*Ja7&7W zahxdXU8KoJAL8jfeEK)syJGx$)C-e2E1Z1qO@zdLE!L`<9)_M}-Ql7*zL1 zT;)RsPnsS&UvZ6=_=P8#M(LP);8M`rY;M2h3#+&&JVx9M9AR+N^hO+oJM%PA46rx4 zM~I}}b*1QmH%K~=6+o(XP|cxH*80Z(bVp0rktYgUWBdbr?f1chJcah)mmD3sU$i=V zx#GT!?^8##C~y^#ANF;N2$cfD65T)j+0(KuP#b6dxvkAph#{VVC4U^MfljHP_J#tG zMlzKH_x0+#M-##E7Qpd9BiYAM*>jmT>M6a}lc|M4s4aLk8Qt--{Q7=QW&TO8f@MR? z56ymW1HJY)_{OaKhQ3u-&iK6rScP!AMK(`Ky5i(;k~xxLr5_B;&vOJI$VYm<+JGPh zGBALjkyK6yTW!aRg7$-_fzhbwa7YLmjd8#qKw#d_zgy;``x<;49#0}5DUjC09uY4K z@CHS2xD2gT45->(gf|LcH@NUc^qh3$Z-cg*^n0$qvv)=hKwJ@b|IO#Vh4*hg5qax& zenPQUfG*YwbUNQ{HiT-Vy?0%>Cd=cK+FTXg**W-`GP!bU8#$%;V-B9y2l@F&vP9vELxM@$+~qcC8M-pSft;{9PEp{!Q*S(plBLOHeM0^{~JepRQ{ zujIEkSYATZ=qTYXPs?k|a#59b!->9zKwa)3@tTONwr0wbswCJ*-)BI4~nLbnW0 zOuNx%{aXC?$Bf64vxQ=%#T{XRRBAL!HKUPgm$g(x#_dPWJ*mBImwPp9WY z!Xa--?-EwtN+NGE#W>R)4C@V>|C37)%k*Z!*VKl`P<>LM$3IN5*b1_r%i@etXPgm!VnWOJ|6!Yx}nycab6 zv}MuK1j@N9)MWht+tMEa;MFcx%&5a zBU{XjL_vv6`I4-+p~j`N)B#@GO1}a?4D5fldlqwZFResVtrRka)?hce2ErE{1+sTy#Ty`54ej-S ztnkH`ZVB7F*aKuWj1}OcS>$$~at%nf2!IDfv7&p?Iz{t~@wXHDd1(Z6FHM8(g@4)a z?4YhcL!`e7&04`>TO?t1Y?c!>(uqoGH#3+X2k|H zsy6cXUAopX%LN)-?Brhc`Lbp(iV#PIZemlZ?r%`9($2Ysd@48n0n3KF3neHB7yzp5 ziLTqX?L4XEHdx_k8NZ>ms@aPybm-g8g5- zQLig=6n^NEMB`+*Odu7nDEs4Kj}*ZzHt7XDyc@dbqjm8 zY=7x4`wV7_4)TT@;okSh9|nv>0WDiLsh+HlEX1bTxIIFYy;Zhaw}z0$WxCenRW-~B zXuD)mVfoI~aMG3#_#35(Pt8_a91^-t1Z`2z2DTsukv7oAh&@2TLXYuCg5}6qUw^n* zZH1&-iN@`DUT6c<4KOOFwtXw2Qwi`(*}Dwd78hqrryb(Uv2CVuY>81;GystqUcAKz zRgz~1ME1K%M5_WAC&An1*V(76b%iUm2bTGE7sPy`@lgf!0$?`nHJS6_0o1Jsh+u2E zreH?Ie=`1fs@a7%h3`mjWtOXVQY(LpOtL9oGEStS^$&PYICZYulFjOe- zkmOzLAU^*O7j2Q;AkpA>Xdq=`r$!R{(P|l;rbanYCwH8vq>V~tJMuIup@~4rBpS-6 zv}1sh0fr1s7uFF--$GU!|Mi_wEI}~Uf?-1YeT0}9onQnY%0bQ7&byJ4AJ2O#%i0v! z!990~bDD2wB0I%{#RYbi4iHXI14v_ae7Ohf$Cz{Wc!tu_Be>m#3XV3kKs4mjuo$QsP~(+E5(bp@hCox<1JdCq#of2yamu zajJ_1aSLXZ4wZ3Fs14{M4TP0Y(H@JPq;kUfonDm;%<^+IlAKsbiUwjGEu?5S1_!D7 zU%1k#z_oAl^CPYz`|OhD9P(mT-yfu^IL%c!Rx#jz6rE%i?+%Fo(Gn?;5wLTCC=m%3 zi@`GIHQkmTcjTF8$w1tLeOi&&I>y|Ij%NYYE0rbK>~)cHOg=U z2?LHi-u!VUmy_#4sk5TgyAAkw$DIu%3Ku0 cN22`Z#@p#Ok!!SvVBqV({==pf#%|I74=f_`-T(jq literal 116562 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ȇ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 hRn&Fjcd?lKrNCUC$>{g4vxR^!oc2Y`V@ Date: Thu, 28 May 2015 01:36:31 +0100 Subject: [PATCH 017/451] Email activation and password resets for users. --- forms.tmpl | 42 ++++++++-- forum.nim | 187 +++++++++++++++++++++++++++++++++++++++---- main.tmpl | 1 + public/css/style.css | 14 ++++ 4 files changed, 224 insertions(+), 20 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index cd0133d..6f536b7 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -10,7 +10,7 @@ # const threadId = 0 # const name = 1 # const views = 2 -# +# # result = "" # count = 0

#let users = getAllRows(db, - # sql("select distinct name, email from person where id in " & + # sql("select distinct name, email from person where id in " & # "(select author from post where thread = ?)"), %threadId)
@@ -55,9 +55,9 @@ #end for
- + #let latestReplyAuthor = getValue(db, sql("select name from person where id = " & - # "(select author from post where id = " & + # "(select author from post where id = " & # "(select max(id) from post where thread = ?))"), %threadId) #let replyProfileUrl = c.req.makeUri("profile/", false) & @@ -112,7 +112,7 @@ # # #proc genPostsList(c: var TForumData, threadId: string, count: var int): string = -# const query = sql"""select p.id, u.name, p.header, p.content, p.creation, p.author, u.email from post p, +# const query = sql"""select p.id, u.name, p.header, p.content, p.creation, p.author, u.email from post p, # person u where u.id = p.author and p.thread = ? order by p.id limit ?, ?""" # const postId = 0 # const userName = 1 @@ -204,7 +204,7 @@
- +
Syntax Cheatsheet
@@ -367,3 +367,33 @@ # end if #end proc # +# +#proc genFormResetPassword(c: var TForumData): string = +# result = "" +
+
+
+ forum index > + Reset Password +
+
+
+
+ + + + + + + + + +
${FieldValid(c, "nick", "Your nickname:")}
${FieldValid(c, "antibot", "What is " & antibot(c) & "?")}${TextWidget(c, "antibot", "", maxlength=4)}
+ #if c.errorMsg != "": +
+ $c.errorMsg +
+ #end if + +
+#end proc diff --git a/forum.nim b/forum.nim index 3eff43d..0801d8b 100644 --- a/forum.nim +++ b/forum.nim @@ -8,12 +8,13 @@ import os, strutils, times, md5, strtabs, cgi, math, db_sqlite, matchers, - rst, rstgen, captchas, scgi, jester, asyncdispatch, asyncnet, cache, sequtils + rst, rstgen, captchas, scgi, jester, asyncdispatch, asyncnet, cache, sequtils, + parseutils, utils when not defined(windows): import bcrypt # TODO -from htmlgen import tr, th, td, span +from htmlgen import tr, th, td, span, input const unselectedThread = -1 @@ -25,6 +26,7 @@ const noPageNums = ["/login", "/register", "/dologin", "/doregister", "/profile"] noHomeBtn = ["/", "/login", "/register", "/dologin", "/doregister", "/profile"] banReasonDeactivated = "DEACTIVATED" + banReasonEmailUnconfirmed = "EMAILCONFIRMATION" type TCrud = enum crCreate, crRead, crUpdate, crDelete @@ -50,6 +52,7 @@ type totalPosts: int search: string noPagenumumNav: bool + config: Config TStyledButton = tuple[text: string, link: string] @@ -72,6 +75,7 @@ var db: TDbConn docConfig: StringTableRef isFTSAvailable: bool + config: Config proc init(c: var TForumData) = c.userPass = "" @@ -246,6 +250,20 @@ proc makePassword(password, salt: string, comparingTo = ""): string = let bcryptSalt = if comparingTo != "": comparingTo else: genSalt(8) result = hash(getMD5(salt & getMD5(password)), bcryptSalt) +proc makeIdentHash(user, password, epoch, secret: string, + comparingTo = ""): string = + ## Creates a hash verifying the identity of a user. Used for password reset + ## links and email activation links. + ## If ``epoch`` is smaller than the epoch of the user's last login then + ## the link is invalid. + ## The ``secret`` is the 'salt' field in the ``person`` table. + echo(user, password, epoch, secret) + when defined(windows): + result = getMD5(user & password & epoch & secret) + else: + let bcryptSalt = if comparingTo != "": comparingTo else: genSalt(8) + result = hash(user & password & epoch & secret, bcryptSalt) + # ----------------------------------------------------------------------------- template `||`(x: expr): expr = (if not isNil(x): x else: "") @@ -274,7 +292,14 @@ proc setError(c: var TForumData, field, msg: string): bool {.inline.} = c.errorMsg = "Error: " & msg return false -proc register(c: var TForumData, name, pass, antibot, email: string): bool = +proc isCaptchaCorrect(c: var TForumData, antibot: string): bool = + ## Determines whether the user typed in the captcha correctly. + let correctRes = getValue(db, + sql"select answer from antibot where ip = ?", c.req.ip) + return antibot == correctRes + +proc register(c: var TForumData, name, pass, antibot, + email: string): bool = # Username validation: if name.len == 0 or not allCharsInSet(name, SecureChars): return setError(c, "name", "Invalid username!") @@ -285,25 +310,68 @@ proc register(c: var TForumData, name, pass, antibot, email: string): bool = if pass.len < 4: return setError(c, "new_password", "Invalid password!") - # antibot validation: - let correctRes = getValue(db, - sql"select answer from antibot where ip = ?", c.req.ip) - if antibot != correctRes: - return setError(c, "antibot", "You seem to be a bot!") + # captcha validation: + if not isCaptchaCorrect(c, antibot): + return setError(c, "antibot", "Answer to captcha incorrect!") # email validation - if not validEmailAddress(email): + if not ('@' in email and '.' in email): return setError(c, "email", "Invalid email address") # perform registration: var salt = makeSalt() + let password = makePassword(pass, salt) + + # Send activation email. + let epoch = $int(epochTime()) + let activateUrl = c.req.makeUri("/activateEmail?nick=$1&epoch=$2&ident=$3" % + [encodeUrl(name), encodeUrl(epoch), + encodeUrl(makeIdentHash(name, password, epoch, salt))]) + + let emailSentFut = sendEmailActivation(c.config, email, name, activateUrl) + # Block until we send the email. + # TODO: This is a workaround for 'var T' not being usable in async procs. + while not emailSentFut.finished: + poll() + if emailSentFut.failed: + echo("[WARNING] Couldn't send activation email: ", emailSentFut.error.msg) + return setError(c, "email", "Couldn't send activation email") + + # add account to person table exec(db, sql("INSERT INTO person(name, password, email, salt, status, lastOnline, " & - "ban) VALUES (?, ?, ?, ?, 'user', DATETIME('now'), '')"), name, - makePassword(pass, salt), email, salt) - # return setError(c, "", "Could not create your account!") + "ban) VALUES (?, ?, ?, ?, 'user', DATETIME('now'), ?)"), name, + password, email, salt, + banReasonEmailUnconfirmed) + return true +proc resetPassword(c: var TForumData, nick, antibot: string): bool = + # Validate captcha + if not isCaptchaCorrect(c, antibot): + return setError(c, "antibot", "Answer to captcha incorrect!") + # Gather some extra information to determine ident hash. + let epoch = $int(epochTime()) + let row = db.getRow( + sql"select password, salt, email from person where name = ?", nick) + if row[0] == "": + return setError(c, "nick", "Nickname not found") + # Generate URL for the email. + # TODO: Get rid of the stupid `%` in main.tmpl as it screws up strutils.% + let resetUrl = c.req.makeUri( + strutils.`%`("/emailResetPassword?nick=$1&epoch=$2&ident=$3", + [encodeUrl(nick), encodeUrl(epoch), + encodeUrl(makeIdentHash(nick, row[0], epoch, row[1]))])) + echo "User's reset URL is: ", resetUrl + # Send the email. + let emailSentFut = sendPassReset(c.config, row[2], nick, resetUrl) + # TODO: This is a workaround for 'var T' not being usable in async procs. + while not emailSentFut.finished: + poll() + if emailSentFut.failed: + echo("[WARNING] Couldn't send activation email: ", emailSentFut.error.msg) + return setError(c, "email", "Couldn't send activation email") + proc checkLoggedIn(c: var TForumData) = let pass = c.req.cookies["sid"] if pass.len == 0: return @@ -519,6 +587,15 @@ proc login(c: var TForumData, name, pass: string): bool = else: return c.setError("password", "Login failed!") +proc verifyIdentHash(c: var TForumData, name, epoch, ident: string): bool = + const query = + sql"select password, salt, strftime('%s', lastOnline) from person where name = ?" + var row = getRow(db, query, name) + if row[0] == "": return false + let newIdent = makeIdentHash(name, row[0], epoch, row[1], ident) + if row[2].parseInt > epoch.parseInt: return false + result = newIdent == ident + proc setBan(c: var TForumData, nick, reason: string): bool = const query = sql("update person set ban = ? where name = ?") @@ -762,6 +839,8 @@ proc genProfile(c: var TForumData, ui: TUserInfo): string = td(case ui.ban of banReasonDeactivated: "Deactivated" + of banReasonEmailUnconfirmed: + "Awaiting email confirmation" of "": "Active" else: @@ -789,6 +868,9 @@ proc genProfile(c: var TForumData, ui: TUserInfo): string = elif ui.ban == banReasonDeactivated: htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "activate"), "Activate user") + elif ui.ban == banReasonEmailUnconfirmed: + htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "activate"), + "Confirm user's email") else: "" else: "") ) @@ -814,6 +896,7 @@ template createTFD(): stmt = c.startTime = epochTime() c.isThreadsList = false c.pageNum = 1 + c.config = config if request.cookies.len > 0: checkLoggedIn(c) @@ -950,8 +1033,9 @@ routes: post "/doregister": createTFD() if c.register(@"name", @"new_password", @"antibot", @"email"): - discard c.login(@"name", @"new_password") - finishLogin() + resp genMain(c, "You are now registered. You must now confirm your" & + " email address by clicking the link sent to " & @"email", + "Registration successful - Nim Forum") else: resp c.genMain(genFormRegister(c)) @@ -1060,6 +1144,80 @@ routes: else: resp genMain(c, "Failure", "Nim Forum") + get "/activateEmail/?": + createTFD() + cond (@"nick" != "") + cond (@"epoch" != "") + cond (@"ident" != "") + var epoch: BiggestInt = 0 + cond(parseBiggestInt(@"epoch", epoch) > 0) + var success = false + if verifyIdentHash(c, @"nick", $epoch, @"ident"): + let ban = db.getValue(sql"select ban from person where name = ?", @"nick") + if ban == banReasonEmailUnconfirmed: + success = setBan(c, @"nick", "") + + if success: + resp genMain(c, "Account activated", "Nim Forum") + else: + resp genMain(c, "Account activation failed", "Nim Forum") + + get "/emailResetPassword/?": + createTFD() + cond (@"nick" != "") + cond (@"epoch" != "") + cond (@"ident" != "") + var epoch: BiggestInt = 0 + cond(parseBiggestInt(@"epoch", epoch) > 0) + if verifyIdentHash(c, @"nick", $epoch, @"ident"): + let formBody = input(`type`="hidden", name="nick", value = @"nick") & + input(`type`="hidden", name="epoch", value = @"epoch") & + input(`type`="hidden", name="ident", value = @"ident") & + input(`type`="password", name="password") & + "
" & + input(`type`="submit", name="submitBtn", + value="Change my password") + let message = htmlgen.p("Please enter a new password for ", + htmlgen.b(@"nick"), ':') + let content = htmlgen.form(action=c.req.makeUri("/doemailresetpassword"), + `method`="POST", message & formBody) + + resp genMain(c, content, "Reset password - Nim Forum") + else: + resp genMain(c, "Invalid ident hash", "Error - Nim Forum") + + post "/doemailresetpassword": + createTFD() + cond (@"nick" != "") + cond (@"epoch" != "") + cond (@"ident" != "") + cond (@"password" != "") + var epoch: BiggestInt = 0 + cond(parseBiggestInt(@"epoch", epoch) > 0) + if verifyIdentHash(c, @"nick", $epoch, @"ident"): + let res = setPassword(c, @"nick", @"password") + if res: + resp genMain(c, "Password reset successfully!", "Nim Forum") + else: + resp genMain(c, "Password reset failure", "Nim Forum") + else: + resp genMain(c, "Invalid ident hash", "Nim Forum") + + get "/resetPassword/?": + createTFD() + + resp genMain(c, genFormResetPassword(c), "Reset Password - Nim Forum") + + post "/doresetpassword": + createTFD() + echo(request.params) + cond (@"nick" != "") + + if resetPassword(c, @"nick", @"antibot"): + resp genMain(c, "Email sent!", "Reset Password - Nim Forum") + else: + resp genMain(c, genFormResetPassword(c), "Reset Password - Nim Forum") + const licenseRst = slurp("static/license.rst") get "/license": createTFD() @@ -1117,6 +1275,7 @@ when isMainModule: database="nimforum") isFTSAvailable = db.getAllRows(sql("SELECT name FROM sqlite_master WHERE " & "type='table' AND name='post_fts'")).len == 1 + config = loadConfig() var http = true if paramCount() > 0: if paramStr(1) == "scgi": diff --git a/main.tmpl b/main.tmpl index 03b28c0..b28af9a 100644 --- a/main.tmpl +++ b/main.tmpl @@ -117,6 +117,7 @@ + Reset password #if c.errorMsg != "" and c.req.pathInfo.normalizeUri == "/dologin": $c.errorMsg #end if diff --git a/public/css/style.css b/public/css/style.css index 9d7d0f2..15100e6 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -516,6 +516,20 @@ div#sidebar .content .search width: 95%; } +div#sidebar .content a#passreset { + color: #CEDAE9; + font-size: 9pt; + display: block; + text-decoration: none; + margin-top: -4pt; +} + +div#sidebar .content a#passreset:hover { + color: #fff; +} + + + span.error { float: left; From 1390d2a22a4fb840dc29f3c1c099d13195604955 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 28 May 2015 22:01:11 +0100 Subject: [PATCH 018/451] Added missing utils.nim module. --- utils.nim | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 utils.nim diff --git a/utils.nim b/utils.nim new file mode 100644 index 0000000..4d3e1c5 --- /dev/null +++ b/utils.nim @@ -0,0 +1,60 @@ +import asyncdispatch, smtp, strutils, json, os + +type + Config* = object + smtpAddress: string + smtpPort: int + smtpUser: string + smtpPassword: string + +proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config = + result = Config(smtpAddress: "localhost", smtpPort: 25, smtpUser: "", + smtpPassword: "") + try: + let root = parseFile(filename) + result.smtpAddress = root["smtpAddress"].getStr("localhost") + result.smtpPort = root["smtpPort"].getNum(25).int + result.smtpUser = root["smtpUser"].getStr("") + result.smtpPassword = root["smtpPassword"].getStr("") + except: + echo("[WARNING] Couldn't read config file: ./forum.json") + +proc sendMail(config: Config, subject, message, recipient: string) {.async.} = + var client = newAsyncSmtp(config.smtpAddress, Port(config.smtpPort)) + await client.connect() + if config.smtpUser.len > 0: + await client.auth(config.smtpUser, config.smtpPassword) + + let toList = @[recipient] + let encoded = createMessage(subject, message, + toList, @[], []) + + await client.sendMail("forum@nim-lang.org", toList, + $encoded) + +proc sendPassReset*(config: Config, email, user, resetUrl: string) {.async.} = + let message = """Hello $1, +A password reset has been requested for your account on the Nim Forum. + +If you did not make this request, you can safely ignore this email. +A password reset request can be made by anyone, and it does not indicate +that your account is in any danger of being accessed by someone else. + +If you do actually want to reset your password, visit this link: + + $2 + +Thank you for being a part of the Nim community!""" % [user, resetUrl] + await sendMail(config, "Nim Forum Password Recovery", message, email) + +proc sendEmailActivation*(config: Config, email, user, activateUrl: string) {.async.} = + let message = """Hello $1, +You have recently registered an account on the Nim Forum. + +As the final step in your registration, we require that you confirm your email +via the following link: + + $2 + +Thank you for registering and becoming a part of the Nim community!""" % [user, activateUrl] + await sendMail(config, "Nim Forum Account Email Confirmation", message, email) From 5fc78ff8310cc57fd171651daaed6bbce33cbce8 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 28 May 2015 22:28:45 +0100 Subject: [PATCH 019/451] Fix success reset password message. --- forum.nim | 2 ++ 1 file changed, 2 insertions(+) diff --git a/forum.nim b/forum.nim index 0801d8b..d62fbdc 100644 --- a/forum.nim +++ b/forum.nim @@ -372,6 +372,8 @@ proc resetPassword(c: var TForumData, nick, antibot: string): bool = echo("[WARNING] Couldn't send activation email: ", emailSentFut.error.msg) return setError(c, "email", "Couldn't send activation email") + return true + proc checkLoggedIn(c: var TForumData) = let pass = c.req.cookies["sid"] if pass.len == 0: return From 8a2267485208460c96c149edfe3435cacd39bfc2 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 29 May 2015 23:40:06 +0100 Subject: [PATCH 020/451] Add allowed time before ident hash expires. --- forum.nim | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/forum.nim b/forum.nim index d62fbdc..dbf3d8f 100644 --- a/forum.nim +++ b/forum.nim @@ -595,7 +595,9 @@ proc verifyIdentHash(c: var TForumData, name, epoch, ident: string): bool = var row = getRow(db, query, name) if row[0] == "": return false let newIdent = makeIdentHash(name, row[0], epoch, row[1], ident) - if row[2].parseInt > epoch.parseInt: return false + # Check that the user has not been logged in since this ident hash has been + # created. Give the timestamp a certain range to prevent false negatives. + if row[2].parseInt > (epoch.parseInt + 60): return false result = newIdent == ident proc setBan(c: var TForumData, nick, reason: string): bool = From 087dbfd60a45aae1bfdcc21082865c1c105a6cc2 Mon Sep 17 00:00:00 2001 From: Federico Ceratto Date: Sun, 21 Jun 2015 17:13:00 +0100 Subject: [PATCH 021/451] Implement basic mailing list mirroring #57 --- forum.nim | 9 +++++++++ utils.nim | 13 +++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/forum.nim b/forum.nim index dbf3d8f..fbb0011 100644 --- a/forum.nim +++ b/forum.nim @@ -483,6 +483,7 @@ template setPreviewData(c: expr) {.immediate, dirty.} = c.currentPost.content = content template writeToDb(c, cr, setPostId: expr) = + # insert a comment in the DB let retID = insertID(db, crud(cr, "post", "author", "ip", "header", "content", "thread"), c.userId, c.req.ip, subject, content, $c.threadId, "") discard tryExec(db, crud(cr, "post_fts", "id", "header", "content"), @@ -490,6 +491,10 @@ template writeToDb(c, cr, setPostId: expr) = if setPostId: c.postId = retID.int +template sendToMailingList(c) = + # send comment to a mailing list (if configured to do so) + discard sendMailToMailingList(c.config, c.username, c.email, subject, content) + proc edit(c: var TForumData, postId: int): bool = checkLogin(c) if c.isPreview: @@ -530,6 +535,7 @@ proc edit(c: var TForumData, postId: int): bool = result = true proc reply(c: var TForumData): bool = + # reply to an existing thread checkLogin(c) retrPost(c) if c.isPreview: @@ -539,9 +545,11 @@ proc reply(c: var TForumData): bool = exec(db, sql"update thread set modified = DATETIME('now') where id = ?", $c.threadId) + sendToMailingList(c) result = true proc newThread(c: var TForumData): bool = + # create new conversation thread (permanent or transient) const query = sql"insert into thread(name, views, modified) values (?, 0, DATETIME('now'))" checkLogin(c) retrPost(c) @@ -556,6 +564,7 @@ proc newThread(c: var TForumData): bool = writeToDb(c, crCreate, false) discard tryExec(db, sql"insert into post_fts(post_fts) values('optimize')") discard tryExec(db, sql"insert into post_fts(thread_fts) values('optimize')") + sendToMailingList(c) result = true proc login(c: var TForumData, name, pass: string): bool = diff --git a/utils.nim b/utils.nim index 4d3e1c5..44a8de4 100644 --- a/utils.nim +++ b/utils.nim @@ -6,6 +6,7 @@ type smtpPort: int smtpUser: string smtpPassword: string + mlistAddress: string proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config = result = Config(smtpAddress: "localhost", smtpPort: 25, smtpUser: "", @@ -16,10 +17,11 @@ proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config = result.smtpPort = root["smtpPort"].getNum(25).int result.smtpUser = root["smtpUser"].getStr("") result.smtpPassword = root["smtpPassword"].getStr("") + result.mlistAddress = root["mlistAddress"].getStr("") except: echo("[WARNING] Couldn't read config file: ./forum.json") -proc sendMail(config: Config, subject, message, recipient: string) {.async.} = +proc sendMail(config: Config, subject, message, recipient: string, from_addr = "forum@nim-lang.org") {.async.} = var client = newAsyncSmtp(config.smtpAddress, Port(config.smtpPort)) await client.connect() if config.smtpUser.len > 0: @@ -29,9 +31,16 @@ proc sendMail(config: Config, subject, message, recipient: string) {.async.} = let encoded = createMessage(subject, message, toList, @[], []) - await client.sendMail("forum@nim-lang.org", toList, + await client.sendMail(from_addr, toList, $encoded) +proc sendMailToMailingList*(config: Config, username, user_email_addr, subject, message: string) {.async.} = + # send message to a mailing list + let from_addr = "$# <$#>" % [username, user_email_addr] + + if config.mlistAddress != "": + await sendMail(config, subject, message, config.mlistAddress, from_addr=from_addr) + proc sendPassReset*(config: Config, email, user, resetUrl: string) {.async.} = let message = """Hello $1, A password reset has been requested for your account on the Nim Forum. From 633435a2fe9012d0281a4f3a3b158de61de7e3f0 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 23 Aug 2015 22:38:57 +0100 Subject: [PATCH 022/451] Added asyncCheck to discarded future and moved out of template. --- forum.nim | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/forum.nim b/forum.nim index fbb0011..c5c8f13 100644 --- a/forum.nim +++ b/forum.nim @@ -491,10 +491,6 @@ template writeToDb(c, cr, setPostId: expr) = if setPostId: c.postId = retID.int -template sendToMailingList(c) = - # send comment to a mailing list (if configured to do so) - discard sendMailToMailingList(c.config, c.username, c.email, subject, content) - proc edit(c: var TForumData, postId: int): bool = checkLogin(c) if c.isPreview: @@ -545,7 +541,8 @@ proc reply(c: var TForumData): bool = exec(db, sql"update thread set modified = DATETIME('now') where id = ?", $c.threadId) - sendToMailingList(c) + asyncCheck sendMailToMailingList(c.config, c.username, c.email, + subject, content) result = true proc newThread(c: var TForumData): bool = @@ -564,7 +561,8 @@ proc newThread(c: var TForumData): bool = writeToDb(c, crCreate, false) discard tryExec(db, sql"insert into post_fts(post_fts) values('optimize')") discard tryExec(db, sql"insert into post_fts(thread_fts) values('optimize')") - sendToMailingList(c) + asyncCheck sendMailToMailingList(c.config, c.username, c.email, + subject, content) result = true proc login(c: var TForumData, name, pass: string): bool = From 0e1062a69226d9bf9c5553c953dd6e98a7159dbe Mon Sep 17 00:00:00 2001 From: Federico Ceratto Date: Thu, 27 Aug 2015 23:17:08 +0100 Subject: [PATCH 023/451] Add Resent-From header when syncing to a mailing list --- utils.nim | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/utils.nim b/utils.nim index 44a8de4..fb4bcf7 100644 --- a/utils.nim +++ b/utils.nim @@ -21,15 +21,22 @@ proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config = except: echo("[WARNING] Couldn't read config file: ./forum.json") -proc sendMail(config: Config, subject, message, recipient: string, from_addr = "forum@nim-lang.org") {.async.} = +proc sendMail(config: Config, subject, message, recipient: string, from_addr = "forum@nim-lang.org", resent_from = "") {.async.} = var client = newAsyncSmtp(config.smtpAddress, Port(config.smtpPort)) await client.connect() if config.smtpUser.len > 0: await client.auth(config.smtpUser, config.smtpPassword) let toList = @[recipient] + + let otherHeaders = + if resent_from == "": + @[] + else: + @[("Resent-From", resent_from)] + let encoded = createMessage(subject, message, - toList, @[], []) + toList, @[], otherHeaders) await client.sendMail(from_addr, toList, $encoded) @@ -39,7 +46,7 @@ proc sendMailToMailingList*(config: Config, username, user_email_addr, subject, let from_addr = "$# <$#>" % [username, user_email_addr] if config.mlistAddress != "": - await sendMail(config, subject, message, config.mlistAddress, from_addr=from_addr) + await sendMail(config, subject, message, config.mlistAddress, from_addr=from_addr, resent_from="forum@nim-lang.org") proc sendPassReset*(config: Config, email, user, resetUrl: string) {.async.} = let message = """Hello $1, From 409b40cffba1ff14ecfc0e822b5e55abde12f1a5 Mon Sep 17 00:00:00 2001 From: Federico Ceratto Date: Sun, 30 Aug 2015 17:45:42 +0100 Subject: [PATCH 024/451] Add author name, dates, threading to emails to the mailing list --- forum.nim | 4 ++-- utils.nim | 37 +++++++++++++++++++++++++------------ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/forum.nim b/forum.nim index c5c8f13..9427fe3 100644 --- a/forum.nim +++ b/forum.nim @@ -542,7 +542,7 @@ proc reply(c: var TForumData): bool = exec(db, sql"update thread set modified = DATETIME('now') where id = ?", $c.threadId) asyncCheck sendMailToMailingList(c.config, c.username, c.email, - subject, content) + subject, content, threadId=c.threadId, postId=c.postID, is_reply=true) result = true proc newThread(c: var TForumData): bool = @@ -562,7 +562,7 @@ proc newThread(c: var TForumData): bool = discard tryExec(db, sql"insert into post_fts(post_fts) values('optimize')") discard tryExec(db, sql"insert into post_fts(thread_fts) values('optimize')") asyncCheck sendMailToMailingList(c.config, c.username, c.email, - subject, content) + subject, content, threadId=c.threadID, postId=c.postID, is_reply=false) result = true proc login(c: var TForumData, name, pass: string): bool = diff --git a/utils.nim b/utils.nim index fb4bcf7..081795d 100644 --- a/utils.nim +++ b/utils.nim @@ -1,4 +1,5 @@ import asyncdispatch, smtp, strutils, json, os +from times import getTime, getGMTime, format type Config* = object @@ -21,7 +22,7 @@ proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config = except: echo("[WARNING] Couldn't read config file: ./forum.json") -proc sendMail(config: Config, subject, message, recipient: string, from_addr = "forum@nim-lang.org", resent_from = "") {.async.} = +proc sendMail(config: Config, subject, message, recipient: string, from_addr = "forum@nim-lang.org", otherHeaders:seq[(string, string)] = @[]) {.async.} = var client = newAsyncSmtp(config.smtpAddress, Port(config.smtpPort)) await client.connect() if config.smtpUser.len > 0: @@ -29,24 +30,36 @@ proc sendMail(config: Config, subject, message, recipient: string, from_addr = " let toList = @[recipient] - let otherHeaders = - if resent_from == "": - @[] - else: - @[("Resent-From", resent_from)] - let encoded = createMessage(subject, message, toList, @[], otherHeaders) - await client.sendMail(from_addr, toList, - $encoded) + await client.sendMail(from_addr, toList, $encoded) -proc sendMailToMailingList*(config: Config, username, user_email_addr, subject, message: string) {.async.} = +proc sendMailToMailingList*(config: Config, username, user_email_addr, subject, message: string, thread_id=0, post_id=0, is_reply=false) {.async.} = # send message to a mailing list + if config.mlistAddress == "": + return + let from_addr = "$# <$#>" % [username, user_email_addr] - if config.mlistAddress != "": - await sendMail(config, subject, message, config.mlistAddress, from_addr=from_addr, resent_from="forum@nim-lang.org") + let date = getTime().getGMTime().format("ddd, d MMM yyyy HH:mm:ss") & " +0000" + var otherHeaders = @[ + ("Date", date), + ("Resent-From", "forum@nim-lang.org"), + ("Resent-date", date) + ] + + if is_reply: + let msg_id = "" % [$thread_id, $post_id] + otherHeaders.add(("Message-ID", msg_id)) + let references = "" % [$thread_id] + otherHeaders.add(("References", references)) + + else: # New thread + let msg_id = "" % $thread_id + otherHeaders.add(("Message-ID", msg_id)) + + await sendMail(config, subject, message, config.mlistAddress, from_addr=from_addr, otherHeaders=otherHeaders) proc sendPassReset*(config: Config, email, user, resetUrl: string) {.async.} = let message = """Hello $1, From 520ec0fa029002292a6b06d40deefe021024ecb8 Mon Sep 17 00:00:00 2001 From: Federico Ceratto Date: Sat, 5 Sep 2015 00:00:12 +0100 Subject: [PATCH 025/451] Add From to every outgoing email --- utils.nim | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/utils.nim b/utils.nim index 081795d..9819f66 100644 --- a/utils.nim +++ b/utils.nim @@ -30,8 +30,11 @@ proc sendMail(config: Config, subject, message, recipient: string, from_addr = " let toList = @[recipient] + var headers = otherHeaders + headers.add(("From", from_addr)) + let encoded = createMessage(subject, message, - toList, @[], otherHeaders) + toList, @[], headers) await client.sendMail(from_addr, toList, $encoded) From 35e01a6fcb308a89059114f543984a813694b842 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 27 Sep 2015 20:47:38 +0100 Subject: [PATCH 026/451] Force users thumbnails to 20px This fixes what appears to be a gravatar bug. See https://twitter.com/d0m96/status/647859715028975616 --- public/css/style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/public/css/style.css b/public/css/style.css index 15100e6..ca97bcc 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -232,6 +232,7 @@ pre .EscapeSequence { margin-bottom: -4pt; cursor: help; + width: 20px; } #talk-threads > div > .detail { width:16%; overflow:hidden; } #talk-thread > div > .author, From bf96ec645ebd67e9833c2faba8bbd74ff30c6bb8 Mon Sep 17 00:00:00 2001 From: Sloane Simmons Date: Sat, 31 Oct 2015 20:03:44 -0500 Subject: [PATCH 027/451] Enforce DB content length limit Content length limit of 1000 characters at DB level should be enforced at the application level. --- forum.nim | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/forum.nim b/forum.nim index 9427fe3..e657a93 100644 --- a/forum.nim +++ b/forum.nim @@ -462,6 +462,10 @@ template retrContent(c: expr) = let content {.inject.} = c.req.params["content"] if content.strip.len < 10: return setError(c, "content", "Content not long enough") + # Enforce DB content length for posts + elif content.len > 1000: + return setError(c, "content", "Post length too long") + if not validateRst(c, content): return false template retrPost(c: expr) = From a5963c9164edd8dfba58b6a92e62fed2fe92293a Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 7 Nov 2015 00:21:31 +0000 Subject: [PATCH 028/451] Fixes for 0.12.0 (tables.[]), and added -d:dev. --- README.md | 6 +++++- createdb.nim | 6 +++--- forum.nim | 23 +++++++++++++---------- utils.nim | 1 + 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 5da3520..1df440e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This is Nim's forum. Available at http://forum.nim-lang.org. ## Building -You can use ``nimble`` (available [here](https://github.com/nim-lang/nimble) +You can use ``nimble`` (available [here](https://github.com/nim-lang/nimble) to get all the necessary [dependencies](https://github.com/nim-lang/nimforum/blob/master/nimforum.nimble#L11). @@ -55,6 +55,10 @@ Requires: "nimrod >= 0.10.3, cairo#head, jester#head, bcrypt >= 0.2.1" **Important: You need to compile and run `createdb` to generate the initial database before you can run `forum` the first time**! +**Note: If you do not have a mail server set up locally, you can specify +``-d:dev`` during compilation to prevent nimforum from attempting to send +emails and to automatically activate user accounts** + This is as simple as: ``` diff --git a/createdb.nim b/createdb.nim index 815b7a8..2d34099 100644 --- a/createdb.nim +++ b/createdb.nim @@ -8,10 +8,10 @@ import strutils, db_sqlite -var db = open(connection="nimforum.db", user="postgres", password="", +var db = open(connection="nimforum.db", user="postgres", password="", database="nimforum") -const +const TUserName = "varchar(20)" TPassword = "varchar(32)" TEmail = "varchar(30)" @@ -63,7 +63,7 @@ create table if not exists post( content varchar(1000) not null, thread integer not null, creation timestamp not null default (DATETIME('now')), - + foreign key (thread) references thread(id), foreign key (author) references person(id) );""", []): diff --git a/forum.nim b/forum.nim index e657a93..ba25752 100644 --- a/forum.nim +++ b/forum.nim @@ -105,18 +105,18 @@ const proc TextWidget(c: TForumData, name, defaultText: string, maxlength = 30, size = -1): string = let x = if defaultText != reuseText: defaultText - else: xmlEncode(c.req.params[name]) + else: xmlEncode(c.req.params.getOrDefault(name)) return """""" % [ name, $maxlength, x, if size != -1: "size=\"" & $size & "\"" else: ""] proc HiddenField(c: TForumData, name, defaultText: string): string = let x = if defaultText != reuseText: defaultText - else: xmlEncode(c.req.params[name]) + else: xmlEncode(c.req.params.getOrDefault(name)) return """""" % [name, x] proc TextAreaWidget(c: TForumData, name, defaultText: string): string = let x = if defaultText != reuseText: defaultText - else: xmlEncode(c.req.params[name]) + else: xmlEncode(c.req.params.getOrDefault(name)) return """""" % [ name, x] @@ -333,16 +333,17 @@ proc register(c: var TForumData, name, pass, antibot, # TODO: This is a workaround for 'var T' not being usable in async procs. while not emailSentFut.finished: poll() - if emailSentFut.failed: - echo("[WARNING] Couldn't send activation email: ", emailSentFut.error.msg) - return setError(c, "email", "Couldn't send activation email") + when not defined(dev): + if emailSentFut.failed: + echo("[WARNING] Couldn't send activation email: ", emailSentFut.error.msg) + return setError(c, "email", "Couldn't send activation email") # add account to person table exec(db, sql("INSERT INTO person(name, password, email, salt, status, lastOnline, " & "ban) VALUES (?, ?, ?, ?, 'user', DATETIME('now'), ?)"), name, password, email, salt, - banReasonEmailUnconfirmed) + when defined(dev): "" else: banReasonEmailUnconfirmed) return true @@ -409,10 +410,10 @@ proc incrementViews(c: var TForumData) = exec(db, query, $c.threadId) proc isPreview(c: TForumData): bool = - result = c.req.params["previewBtn"].len > 0 # TODO: Could be wrong? + result = c.req.params.hasKey("previewBtn") proc isDelete(c: TForumData): bool = - result = c.req.params["delete"].len > 0 + result = c.req.params.hasKey("delete") proc rstToHtml(content: string): string = result = rstgen.rstToHtml(content, {roSupportSmilies, roSupportMarkdown}, @@ -582,6 +583,8 @@ proc login(c: var TForumData, name, pass: string): bool = of "": discard of banReasonDeactivated: return c.setError("name", "Your account has been deactivated.") + of banReasonEmailUnconfirmed: + return c.setError("name", "You need to confirm your email first.") else: return c.setError("name", "You have been banned: " & row[6]) c.userid = row[0] @@ -624,7 +627,7 @@ proc setPassword(c: var TForumData, nick, pass: string): bool = proc hasReplyBtn(c: var TForumData): bool = result = c.req.pathInfo != "/donewthread" and c.req.pathInfo != "/doreply" - result = result and c.req.params["action"] != "reply" + result = result and c.req.params.getOrDefault("action") != "reply" # If the user is not logged in and there are no page numbers then we shouldn't # generate the div. let pages = ceil(c.totalPosts / PostsPerPage).int diff --git a/utils.nim b/utils.nim index 9819f66..ceb8dc2 100644 --- a/utils.nim +++ b/utils.nim @@ -23,6 +23,7 @@ proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config = echo("[WARNING] Couldn't read config file: ./forum.json") proc sendMail(config: Config, subject, message, recipient: string, from_addr = "forum@nim-lang.org", otherHeaders:seq[(string, string)] = @[]) {.async.} = + when defined(dev): return var client = newAsyncSmtp(config.smtpAddress, Port(config.smtpPort)) await client.connect() if config.smtpUser.len > 0: From 1008ab6476dd631ccc01e9cb61395f8ce34fe520 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 7 Nov 2015 00:44:40 +0000 Subject: [PATCH 029/451] Implement primitive spam checking. --- forum.nim | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/forum.nim b/forum.nim index ba25752..f954ad7 100644 --- a/forum.nim +++ b/forum.nim @@ -535,6 +535,27 @@ proc edit(c: var TForumData, postId: int): bool = exec(db, crud(crUpdate, "thread", "name"), subject, $c.threadId) result = true +proc gatherUserInfo(c: var TForumData, nick: string, ui: var TUserInfo): bool +proc spamCheck(c: var TForumData, subject, content: string): bool = + # Check current user's info + var ui: TUserInfo + if gatherUserInfo(c, c.userName, ui): + if ui.posts > 1: return + + # Strip all punctuation + var subjAlphabet = "" + for i in subject: + if i in Letters: + subjAlphabet.add(i) + var contentAlphabet = "" + for i in content: + if i in Letters: + contentAlphabet.add(i) + + for word in ["appliance", "kitchen", "cheap", "sale"]: + if word in subjAlphabet.toLower() or word in contentAlphabet.toLower(): + return true + proc reply(c: var TForumData): bool = # reply to an existing thread checkLogin(c) @@ -542,6 +563,9 @@ proc reply(c: var TForumData): bool = if c.isPreview: setPreviewData(c) else: + if spamCheck(c, subject, content): + echo("[WARNING] Found spam: ", subject) + return true writeToDb(c, crCreate, true) exec(db, sql"update thread set modified = DATETIME('now') where id = ?", @@ -559,6 +583,9 @@ proc newThread(c: var TForumData): bool = setPreviewData(c) c.threadID = transientThread else: + if spamCheck(c, subject, content): + echo("[WARNING] Found spam: ", subject) + return true c.threadID = tryInsertID(db, query, c.req.params["subject"]).int if c.threadID < 0: return setError(c, "subject", "Subject already exists") discard tryExec(db, crud(crCreate, "thread_fts", "id", "name"), From c2f70ce5ad405573c357b1e15f798e16765b1015 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 7 Nov 2015 01:01:42 +0000 Subject: [PATCH 030/451] Fix yet another `[]` crash. --- forum.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forum.nim b/forum.nim index f954ad7..fd7ea39 100644 --- a/forum.nim +++ b/forum.nim @@ -376,8 +376,8 @@ proc resetPassword(c: var TForumData, nick, antibot: string): bool = return true proc checkLoggedIn(c: var TForumData) = + if c.req.cookies.hasKey("sid"): return let pass = c.req.cookies["sid"] - if pass.len == 0: return if execAffectedRows(db, sql("update session set lastModified = DATETIME('now') " & "where ip = ? and password = ?"), From 852b3c13eb4ab0065f0d8d31b430c2c6000a2286 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 7 Nov 2015 01:02:46 +0000 Subject: [PATCH 031/451] Fixed incorrect if condition. --- forum.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forum.nim b/forum.nim index fd7ea39..fd082ef 100644 --- a/forum.nim +++ b/forum.nim @@ -376,7 +376,7 @@ proc resetPassword(c: var TForumData, nick, antibot: string): bool = return true proc checkLoggedIn(c: var TForumData) = - if c.req.cookies.hasKey("sid"): return + if not c.req.cookies.hasKey("sid"): return let pass = c.req.cookies["sid"] if execAffectedRows(db, sql("update session set lastModified = DATETIME('now') " & From 00b01e55856c445031862aabe0a215d84b85fe6e Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 17 Nov 2015 11:24:24 +0000 Subject: [PATCH 032/451] Get rid of post length limit, also decrease lower limit. Ref #71. --- forum.nim | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/forum.nim b/forum.nim index fd082ef..b2d65bd 100644 --- a/forum.nim +++ b/forum.nim @@ -461,11 +461,8 @@ template retrSubject(c: expr) = template retrContent(c: expr) = let content {.inject.} = c.req.params["content"] - if content.strip.len < 10: + if content.strip.len < 2: return setError(c, "content", "Content not long enough") - # Enforce DB content length for posts - elif content.len > 1000: - return setError(c, "content", "Post length too long") if not validateRst(c, content): return false From 0204aadddbe8033c1b76eb875df62fea9f110860 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 30 Jan 2016 10:14:17 +0000 Subject: [PATCH 033/451] Fixes @dom96/jester#53. https://github.com/dom96/jester/issues/53 --- forum.nim | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/forum.nim b/forum.nim index b2d65bd..a51419d 100644 --- a/forum.nim +++ b/forum.nim @@ -71,6 +71,8 @@ type email: string ban: string + ForumError = object of Exception + var db: TDbConn docConfig: StringTableRef @@ -110,8 +112,10 @@ proc TextWidget(c: TForumData, name, defaultText: string, name, $maxlength, x, if size != -1: "size=\"" & $size & "\"" else: ""] proc HiddenField(c: TForumData, name, defaultText: string): string = - let x = if defaultText != reuseText: defaultText - else: xmlEncode(c.req.params.getOrDefault(name)) + let x = xmlencode( + if defaultText != reuseText: defaultText + else: c.req.params.getOrDefault(name) + ) return """""" % [name, x] proc TextAreaWidget(c: TForumData, name, defaultText: string): string = @@ -455,11 +459,15 @@ proc crud(c: TCrud, table: string, data: varargs[string]): TSqlQuery = result = sql("delete from " & table & " where id = ?") template retrSubject(c: expr) = + if not c.req.params.hasKey("subject"): + raise newException(ForumError, "Subject empty") let subject {.inject.} = c.req.params["subject"] if subject.strip.len < 3: return setError(c, "subject", "Subject not long enough") template retrContent(c: expr) = + if not c.req.params.hasKey("content"): + raise newException(ForumError, "Content empty") let content {.inject.} = c.req.params["content"] if content.strip.len < 2: return setError(c, "content", "Content not long enough") From d75c11c5ebc0f721a6159d422eb3249bfc3d2717 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 30 Jan 2016 10:21:25 +0000 Subject: [PATCH 034/451] Fix crash when no mlist address is configured. --- utils.nim | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/utils.nim b/utils.nim index ceb8dc2..5ef5971 100644 --- a/utils.nim +++ b/utils.nim @@ -10,11 +10,11 @@ type mlistAddress: string proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config = - result = Config(smtpAddress: "localhost", smtpPort: 25, smtpUser: "", - smtpPassword: "") + result = Config(smtpAddress: "", smtpPort: 25, smtpUser: "", + smtpPassword: "", mlistAddress: "") try: let root = parseFile(filename) - result.smtpAddress = root["smtpAddress"].getStr("localhost") + result.smtpAddress = root["smtpAddress"].getStr("") result.smtpPort = root["smtpPort"].getNum(25).int result.smtpUser = root["smtpUser"].getStr("") result.smtpPassword = root["smtpPassword"].getStr("") @@ -23,7 +23,10 @@ proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config = echo("[WARNING] Couldn't read config file: ./forum.json") proc sendMail(config: Config, subject, message, recipient: string, from_addr = "forum@nim-lang.org", otherHeaders:seq[(string, string)] = @[]) {.async.} = - when defined(dev): return + if config.smtpAddress.len == 0: + echo("[WARNING] Cannot send mail: no smtp server configured (smtpAddress).") + return + var client = newAsyncSmtp(config.smtpAddress, Port(config.smtpPort)) await client.connect() if config.smtpUser.len > 0: @@ -41,7 +44,8 @@ proc sendMail(config: Config, subject, message, recipient: string, from_addr = " proc sendMailToMailingList*(config: Config, username, user_email_addr, subject, message: string, thread_id=0, post_id=0, is_reply=false) {.async.} = # send message to a mailing list - if config.mlistAddress == "": + if config.mlistAddress.len == 0: + echo("[WARNING] Cannot send mail: no mlistAddress configured.") return let from_addr = "$# <$#>" % [username, user_email_addr] From 6bded9791efa14b3f3915744c94a2d4e4f467cd8 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 30 Jan 2016 10:29:23 +0000 Subject: [PATCH 035/451] Remove replyBtn on edit post page. Remove dead code. --- forum.nim | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/forum.nim b/forum.nim index a51419d..cdb72ca 100644 --- a/forum.nim +++ b/forum.nim @@ -659,29 +659,13 @@ proc setPassword(c: var TForumData, nick, pass: string): bool = proc hasReplyBtn(c: var TForumData): bool = result = c.req.pathInfo != "/donewthread" and c.req.pathInfo != "/doreply" - result = result and c.req.params.getOrDefault("action") != "reply" + result = result and c.req.params.getOrDefault("action") notin ["reply", "edit"] # If the user is not logged in and there are no page numbers then we shouldn't # generate the div. let pages = ceil(c.totalPosts / PostsPerPage).int result = result and (pages > 1 or c.loggedIn) return c.threadId >= 0 and result -proc genActionMenu(c: var TForumData): string = - result = "" - var btns: seq[TStyledButton] = @[] - # TODO: Make this detection better? - if c.req.pathInfo.normalizeUri notin noHomeBtn and not c.isThreadsList: - btns.add(("Thread List", c.req.makeUri("/", false))) - #echo c.loggedIn - if c.loggedIn: - let hasReplyBtn = c.req.pathInfo != "/donewthread" and c.req.pathInfo != "/doreply" - if c.threadId >= 0 and hasReplyBtn: - let replyUrl = c.genThreadUrl(action = "reply", - pageNum = $(ceil(c.totalPosts / PostsPerPage).int)) & "#reply" - btns.add(("Reply", replyUrl)) - btns.add(("New Thread", c.req.makeUri("/newthread", false))) - result = c.genButtons(btns) - proc getStats(c: var TForumData, simple: bool): TForumStats = const totalUsersQuery = sql"select count(*) from person" From 54993195b2cf766205c942a9678bc36b359ee4eb Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 30 Jan 2016 10:46:24 +0000 Subject: [PATCH 036/451] Fixes #74. --- forum.nim | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/forum.nim b/forum.nim index cdb72ca..7296738 100644 --- a/forum.nim +++ b/forum.nim @@ -552,12 +552,21 @@ proc spamCheck(c: var TForumData, subject, content: string): bool = for i in subject: if i in Letters: subjAlphabet.add(i) + case i + of '!': + subjAlphabet.add("i") + else: discard var contentAlphabet = "" for i in content: if i in Letters: contentAlphabet.add(i) + case i + of '!': + subjAlphabet.add("i") + else: discard - for word in ["appliance", "kitchen", "cheap", "sale"]: + for word in ["appliance", "kitchen", "cheap", "sale", "relocating", + "packers", "lenders", "fifa", "coins"]: if word in subjAlphabet.toLower() or word in contentAlphabet.toLower(): return true From b71a08fa211dd2e5764386f9e2ff535028a4b061 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 30 Jan 2016 10:49:03 +0000 Subject: [PATCH 037/451] Fixes #68. --- main.tmpl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main.tmpl b/main.tmpl index b28af9a..6f4a3a5 100644 --- a/main.tmpl +++ b/main.tmpl @@ -24,9 +24,10 @@ From bb494f0b6581dbd5b1c6364bc00104cd6f8ee8e9 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 30 Jan 2016 12:09:08 +0000 Subject: [PATCH 038/451] Implemented an inline rst reference. Ref #75. --- forms.tmpl | 84 ++++++++++++++++++++++++++++++++++++++++++-- public/css/style.css | 41 +++++++++++++++++++-- 2 files changed, 121 insertions(+), 4 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index 6f536b7..d19224b 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -166,7 +166,8 @@ #end proc # -# +#proc genMarkHelp(): string +#end proc #proc genFormPost(c: var TForumData, action: string, # topText, title, content: string, isEdit: bool): string = # result = "" @@ -202,10 +203,11 @@ #end if
+ - Syntax Cheatsheet + ${genMarkHelp()} #end proc @@ -397,3 +399,81 @@ #end proc +#proc genMarkHelp(): string = +#result = "" +
+

nimforum uses a slightly-customized version of + reStructuredText for formatting. See below for some basics, or check + this link for a more detailed help reference.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
you type: + you see: +
*italics*italics +
**bold**bold +
`nim! <http://nim-lang.org>`_nim! +
* item 1 +
* item 2 +
* item 3
+
    +
  • item 1
  • +
  • item 2
  • +
  • item 3
  • +
+
> quoted text +
quoted text
+
The forum supports the Github Markdown syntax +
for code listings: +
+
```nim +
if 1 * 2 < 3: +
  echo "hello, world!" +
``` +
+
The forum supports the Github Markdown syntax +
for code listings: +
+
+if 1*2 < 3:
+  echo "hello, world!"
+                    
+
A horizontal rule can be created
----
but it needs text after it
A horizontal rule can be created
but it needs text after it
+
+#end proc \ No newline at end of file diff --git a/public/css/style.css b/public/css/style.css index ca97bcc..63dab4e 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -287,7 +287,7 @@ pre .EscapeSequence #talk-thread > div > .author > div > .name { } #talk-thread > div > .author > div > .date { font-size: 8pt; color: white; } #talk-thread > div > .topic { width:85%; padding-bottom:10px; margin-left: 15%; } - #talk-thread > div > .topic pre { + #talk-thread > div > .topic pre, #markhelp pre.listing { overflow:auto; margin:0; padding:15px 10px; @@ -299,7 +299,8 @@ pre .EscapeSequence margin-bottom: 10pt; font-family: "DejaVu Sans Mono", monospace; } - #talk-thread > div > .topic a, #talk-thread > div > .topic a:visited + #talk-thread > div > .topic a, #talk-thread > div > .topic a:visited, + #markhelp a, #markhelp a:visited { color: #3680C9; text-decoration: none; @@ -669,3 +670,39 @@ img.rssfeed { float: right; margin-top: 10px; } + + +#markhelp { + width: 80%; + background-color: #cbcfd6; + padding: 2pt 10pt; + margin-top: 10pt; +} + +#markhelp .markheading { + background-color: #6fa1ff; + text-align: center; +} + +#markhelp table.rst { + width: 100%; + margin: 10px 0px; + font-size: 12pt; + border-collapse: collapse; +} + +#markhelp table tr, #markhelp table td { + width: 50%; + border: 1px solid #7d7d7d; +} + +#markhelp table td { + padding:4px 9px; +} + +blockquote { + padding: 0px 8px; + margin: 10px 0px; + border-left: 2px solid rgb(61, 61, 61); + color: rgb(109, 109, 109); +} \ No newline at end of file From f6f4e5c88803ebd465bff5737f20e3585d28dbed Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 30 Jan 2016 15:05:28 +0000 Subject: [PATCH 039/451] Implemented quotes. --- forum.nim | 62 +++++++++++++++++++++++++++++++++++++++++++- public/css/style.css | 5 ++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/forum.nim b/forum.nim index 7296738..79cc0ff 100644 --- a/forum.nim +++ b/forum.nim @@ -9,7 +9,7 @@ import os, strutils, times, md5, strtabs, cgi, math, db_sqlite, matchers, rst, rstgen, captchas, scgi, jester, asyncdispatch, asyncnet, cache, sequtils, - parseutils, utils + parseutils, utils, htmlparser, xmltree, streams when not defined(windows): import bcrypt # TODO @@ -419,9 +419,69 @@ proc isPreview(c: TForumData): bool = proc isDelete(c: TForumData): bool = result = c.req.params.hasKey("delete") +proc processGT(n: XmlNode, tag: string): (int, XmlNode, string) = + result = (0, newElement(tag), tag) + if n.kind == xnElement and len(n) == 1 and n[0].kind == xnElement: + return processGT(n[0], if n[0].kind == xnElement: n[0].tag else: tag) + + var countGT = true + for c in items(n): + case c.kind + of xnText: + if c.text == ">" and countGT: + result[0].inc() + else: + countGT = false + result[1].add(newText(c.text)) + else: + result[1].add(c) + +proc blockquoteFinish(currentBlockquote, newNode: var XmlNode, n: XmlNode) = + if currentBlockquote.len > 0: + #echo(currentBlockquote.repr) + newNode.add(currentBlockquote) + currentBlockquote = newElement("blockquote") + newNode.add(n) + proc rstToHtml(content: string): string = result = rstgen.rstToHtml(content, {roSupportSmilies, roSupportMarkdown}, docConfig) + # Bolt on quotes. + # TODO: Yes, this is ugly. I wrote it quickly. PRs welcome ;) + try: + var node = parseHtml(newStringStream(result)) + var newNode = newElement("document") + if node.kind == xnElement: + var currentBlockquote = newElement("blockquote") + for n in items(node): + case n.kind + of xnElement: + case n.tag + of "p": + let (nesting, contentNode, tag) = processGT(n, "p") + if nesting > 0: + var bq = currentBlockquote + for i in 1 .. Date: Wed, 16 Mar 2016 22:32:35 -0500 Subject: [PATCH 040/451] Make .tmpl files use stdtmpl | standard Wouldn't compile on 0.13.1 with `#! stdtmpl`, would with `#? stdtmpl | standard` --- forms.tmpl | 2 +- main.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index d19224b..4412073 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -1,4 +1,4 @@ -#! stdtmpl +#? stdtmpl | standard # #template `%`(idx: expr): expr {.immediate.} = # row[idx] diff --git a/main.tmpl b/main.tmpl index 6f4a3a5..3d182c0 100644 --- a/main.tmpl +++ b/main.tmpl @@ -1,4 +1,4 @@ -#! stdtmpl +#? stdtmpl | standard #proc genMain(c: var TForumData, content: string, title = "Nim Forum", # additional_headers = "", showRssLinks = false): string = # result = "" From 908c14591dc56b8240c9aa17c9ea5cc8e65c86ff Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 13 May 2016 23:47:06 +0100 Subject: [PATCH 041/451] Increase registration field length limits in HTML --- forms.tmpl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index 4412073..a0ebaf2 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -231,11 +231,11 @@ ${FieldValid(c, "new_password", "Password:")} - + ${FieldValid(c, "email", "E-Mail:")} - ${TextWidget(c, "email", reuseText, maxlength=30)} + ${TextWidget(c, "email", reuseText, maxlength=300)} ${FieldValid(c, "antibot", "What is " & antibot(c) & "?")} @@ -476,4 +476,4 @@ -#end proc \ No newline at end of file +#end proc From 113dcf6def995aeb86e8967a908ee543bdb918a9 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 16 May 2016 13:48:38 +0100 Subject: [PATCH 042/451] Fix utils.loadConfig --- utils.nim | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/utils.nim b/utils.nim index 5ef5971..ff9e855 100644 --- a/utils.nim +++ b/utils.nim @@ -14,13 +14,13 @@ proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config = smtpPassword: "", mlistAddress: "") try: let root = parseFile(filename) - result.smtpAddress = root["smtpAddress"].getStr("") - result.smtpPort = root["smtpPort"].getNum(25).int - result.smtpUser = root["smtpUser"].getStr("") - result.smtpPassword = root["smtpPassword"].getStr("") - result.mlistAddress = root["mlistAddress"].getStr("") + result.smtpAddress = root{"smtpAddress"}.getStr("") + result.smtpPort = root{"smtpPort"}.getNum(25).int + result.smtpUser = root{"smtpUser"}.getStr("") + result.smtpPassword = root{"smtpPassword"}.getStr("") + result.mlistAddress = root{"mlistAddress"}.getStr("") except: - echo("[WARNING] Couldn't read config file: ./forum.json") + echo("[WARNING] Couldn't read config file: ", filename) proc sendMail(config: Config, subject, message, recipient: string, from_addr = "forum@nim-lang.org", otherHeaders:seq[(string, string)] = @[]) {.async.} = if config.smtpAddress.len == 0: From 3b6c89c4917996c576902068cf2047dba9260306 Mon Sep 17 00:00:00 2001 From: Sergey Avseyev Date: Fri, 10 Jun 2016 16:39:54 +0300 Subject: [PATCH 043/451] Fix usage of 'random' after moving from 'math' --- forum.nim | 9 ++++----- nimforum.nimble | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/forum.nim b/forum.nim index 79cc0ff..0cbbe0a 100644 --- a/forum.nim +++ b/forum.nim @@ -9,7 +9,7 @@ import os, strutils, times, md5, strtabs, cgi, math, db_sqlite, matchers, rst, rstgen, captchas, scgi, jester, asyncdispatch, asyncnet, cache, sequtils, - parseutils, utils, htmlparser, xmltree, streams + parseutils, utils, htmlparser, xmltree, streams, random when not defined(windows): import bcrypt # TODO @@ -276,8 +276,8 @@ proc validThreadId(c: TForumData): bool = $c.threadId).len > 0 proc antibot(c: var TForumData): string = - let a = math.random(10)+1 - let b = math.random(1000)+1 + let a = random(10)+1 + let b = random(1000)+1 let answer = $(a+b) exec(db, sql"delete from antibot where ip = ?", c.req.ip) @@ -1373,7 +1373,7 @@ routes: when isMainModule: docConfig = rstgen.defaultConfig() docConfig["doc.smiley_format"] = "/images/smilieys/$1.png" - math.randomize() + randomize() db = open(connection="nimforum.db", user="postgres", password="", database="nimforum") isFTSAvailable = db.getAllRows(sql("SELECT name FROM sqlite_master WHERE " & @@ -1388,4 +1388,3 @@ when isMainModule: runForever() db.close() - diff --git a/nimforum.nimble b/nimforum.nimble index bcb60cc..3591767 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -8,4 +8,4 @@ license = "MIT" bin = "forum" [Deps] -Requires: "nimrod >= 0.10.3, cairo#head, jester#head, bcrypt#head" +Requires: "nim >= 0.14.0, cairo#head, jester#head, bcrypt#head" From e1d68792a472352bc544c34e85eccfe7562dde05 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 16 Jun 2016 21:59:06 +0100 Subject: [PATCH 044/451] Fix bans not working when user is already logged in. --- forum.nim | 41 ++++++++++++++++++++++++++--------------- main.tmpl | 2 +- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/forum.nim b/forum.nim index 0cbbe0a..3c00905 100644 --- a/forum.nim +++ b/forum.nim @@ -379,6 +379,22 @@ proc resetPassword(c: var TForumData, nick, antibot: string): bool = return true +proc logout(c: var TForumData) = + const query = sql"delete from session where ip = ? and password = ?" + c.username = "" + c.userpass = "" + exec(db, query, c.req.ip, c.req.cookies["sid"]) + +proc getBanErrorMsg(banValue: string): string = + case banValue + of "": return "" + of banReasonDeactivated: + return "Your account has been deactivated." + of banReasonEmailUnconfirmed: + return "You need to confirm your email first." + else: + return "You have been banned: " & banValue + proc checkLoggedIn(c: var TForumData) = if not c.req.cookies.hasKey("sid"): return let pass = c.req.cookies["sid"] @@ -392,10 +408,17 @@ proc checkLoggedIn(c: var TForumData) = c.req.ip, pass) let row = getRow(db, - sql"select name, email, admin from person where id = ?", c.userid) + sql"select name, email, admin, ban from person where id = ?", c.userid) c.username = ||row[0] c.email = ||row[1] c.isAdmin = parseBool(||row[2]) + # Check ban status. + let banErrorMsg = getBanErrorMsg(||row[3]) + if banErrorMsg.len > 0: + discard c.setError("name", banErrorMsg) + logout(c) + return + # Update lastOnline db.exec(sql"update person set lastOnline = DATETIME('now') where id = ?", c.userid) @@ -403,12 +426,6 @@ proc checkLoggedIn(c: var TForumData) = else: echo("SID not found in sessions. Assuming logged out.") -proc logout(c: var TForumData) = - const query = sql"delete from session where ip = ? and password = ?" - c.username = "" - c.userpass = "" - exec(db, query, c.req.ip, c.req.cookies["sid"]) - proc incrementViews(c: var TForumData) = const query = sql"update thread set views = views + 1 where id = ?" exec(db, query, $c.threadId) @@ -680,14 +697,8 @@ proc login(c: var TForumData, name, pass: string): bool = var success = false for row in fastRows(db, query, name): if row[2] == makePassword(pass, row[4], row[2]): - case row[6] - of "": discard - of banReasonDeactivated: - return c.setError("name", "Your account has been deactivated.") - of banReasonEmailUnconfirmed: - return c.setError("name", "You need to confirm your email first.") - else: - return c.setError("name", "You have been banned: " & row[6]) + if row[6].len > 0: + return c.setError("name", getBanErrorMsg(row[6])) c.userid = row[0] c.username = row[1] c.userpass = row[2] diff --git a/main.tmpl b/main.tmpl index 3d182c0..3590b8d 100644 --- a/main.tmpl +++ b/main.tmpl @@ -119,7 +119,7 @@ id="hdnLogin" value="Login" /> Reset password - #if c.errorMsg != "" and c.req.pathInfo.normalizeUri == "/dologin": + #if c.errorMsg != "": $c.errorMsg #end if Date: Sun, 19 Jun 2016 19:58:34 +0100 Subject: [PATCH 045/451] Implement /deleteAll. --- forum.nim | 81 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 15 deletions(-) diff --git a/forum.nim b/forum.nim index 3c00905..69f6af7 100644 --- a/forum.nim +++ b/forum.nim @@ -578,6 +578,24 @@ template writeToDb(c, cr, setPostId: expr) = if setPostId: c.postId = retID.int +proc updateThreads(c: var TForumData): int = + ## Removes threads if they have no posts, or changes their modified field + ## if they still contain posts. + const query = + sql"delete from thread where id not in (select thread from post)" + result = execAffectedRows(db, query).int + if result > 0: + discard tryExec(db, sql"delete from thread_fts where id not in (select thread from post)") + else: + # Update corresponding thread's modified field. + let getModifiedSql = "(select creation from post where post.thread = ?" & + " order by creation desc limit 1)" + let updateSql = sql("update thread set modified=" & getModifiedSql & + " where id = ?") + if not tryExec(db, updateSql, $c.threadId, $c.threadId): + result = -1 + discard setError(c, "", "database error") + proc edit(c: var TForumData, postId: int): bool = checkLogin(c) if c.isPreview: @@ -588,21 +606,15 @@ proc edit(c: var TForumData, postId: int): bool = if not tryExec(db, crud(crDelete, "post"), $postId): return setError(c, "", "database error") discard tryExec(db, crud(crDelete, "post_fts"), $postId) + result = true # delete corresponding thread: - if execAffectedRows(db, - sql"delete from thread where id not in (select thread from post)") > 0: + let updateResult = updateThreads(c) + if updateResult > 0: # whole thread has been deleted, so: c.threadId = unselectedThread - discard tryExec(db, sql"delete from thread_fts where id not in (select thread from post)") - else: - # Update corresponding thread's modified field. - let getModifiedSql = "(select creation from post where post.thread = ?" & - " order by creation desc limit 1)" - let updateSql = sql("update thread set modified=" & getModifiedSql & - " where id = ?") - if not tryExec(db, updateSql, $c.threadId, $c.threadId): - return setError(c, "", "database error") - result = true + elif updateResult < 0: + # error occurred + return false else: checkOwnership(c, $postId) retrPost(c) @@ -731,6 +743,12 @@ proc setBan(c: var TForumData, nick, reason: string): bool = sql("update person set ban = ? where name = ?") return tryExec(db, query, reason, nick) +proc deleteAll(c: var TForumData, nick: string): bool = + const query = + sql("delete from post where author = (select id from person where name = ?)") + result = tryExec(db, query, nick) + result = result and updateThreads(c) >= 0 + proc setPassword(c: var TForumData, nick, pass: string): bool = const query = sql("update person set password = ?, salt = ? where name = ?") @@ -987,7 +1005,14 @@ proc genProfile(c: var TForumData, ui: TUserInfo): string = "Confirm user's email") else: "" else: "") - ) + ), + tr( + th(""), + td(if c.isAdmin: + htmlgen.a(href=c.req.makeUri("/deleteAll?nick=$1" % ui.nick), + "Delete all user's posts and threads") + else: "") + ), ) )) @@ -1223,8 +1248,8 @@ routes: "?") del = true formBody.add "" - content = htmlgen.form(action = c.req.makeUri("/dosetban"), - `method` = "POST", formBody) & content + content = content & htmlgen.form(action = c.req.makeUri("/dosetban"), + `method` = "POST", formBody) resp genMain(c, content, "Set user status - Nim Forum") post "/dosetban": @@ -1246,6 +1271,32 @@ routes: resp genMain(c, "Failed to change the ban status of user.", "Error - Nim Forum") + get "/deleteAll/?": + createTFD() + cond (@"nick" != "") + var formBody = "" + var del = false + var content = "" + formBody.add "" + content = htmlgen.p("Are you sure you wish to delete all " & + "the posts and threads created by ", htmlgen.b(@"nick"), "?") + content = content & htmlgen.form(action = c.req.makeUri("/dodeleteall"), + `method` = "POST", formBody) + resp genMain(c, content, "Delete all user's posts & threads - Nim Forum") + + post "/dodeleteall/?": + createTFD() + cond (@"nick" != "") + if not c.isAdmin: + resp genMain(c, "You cannot delete this user's data.", "Error - Nim Forum") + let result = deleteAll(c, @"nick") + if result: + redirect(c.req.makeUri("/profile/" & @"nick")) + else: + resp genMain(c, "Failed to delete all user's posts and threads.", + "Error - NimForum") + get "/setpassword/?": createTFD() cond (@"nick" != "") From c375e39983777f7d62dff5db5d18fc1ef92f959a Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 19 Jun 2016 20:28:39 +0100 Subject: [PATCH 046/451] Add rate limiting. --- forum.nim | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/forum.nim b/forum.nim index 69f6af7..0f0fd8c 100644 --- a/forum.nim +++ b/forum.nim @@ -659,6 +659,25 @@ proc spamCheck(c: var TForumData, subject, content: string): bool = if word in subjAlphabet.toLower() or word in contentAlphabet.toLower(): return true +proc rateLimitCheck(c: var TForumData): bool = + const query40 = + sql("SELECT count(*) FROM post where author = ? and " & + "(strftime('%s', 'now') - strftime('%s', creation)) < 40") + const query90 = + sql("SELECT count(*) FROM post where author = ? and " & + "(strftime('%s', 'now') - strftime('%s', creation)) < 90") + const query300 = + sql("SELECT count(*) FROM post where author = ? and " & + "(strftime('%s', 'now') - strftime('%s', creation)) < 300") + # TODO Why can't I pass the secs as a param? + let last40s = getValue(db, query40, c.userId).parseInt + let last90s = getValue(db, query90, c.userId).parseInt + let last300s = getValue(db, query300, c.userId).parseInt + if last40s > 1: return true + if last90s > 2: return true + if last300s > 6: return true + return false + proc reply(c: var TForumData): bool = # reply to an existing thread checkLogin(c) @@ -669,6 +688,8 @@ proc reply(c: var TForumData): bool = if spamCheck(c, subject, content): echo("[WARNING] Found spam: ", subject) return true + if rateLimitCheck(c): + return setError(c, "subject", "You're posting too fast.") writeToDb(c, crCreate, true) exec(db, sql"update thread set modified = DATETIME('now') where id = ?", @@ -689,6 +710,8 @@ proc newThread(c: var TForumData): bool = if spamCheck(c, subject, content): echo("[WARNING] Found spam: ", subject) return true + if rateLimitCheck(c): + return setError(c, "subject", "You're posting too fast.") c.threadID = tryInsertID(db, query, c.req.params["subject"]).int if c.threadID < 0: return setError(c, "subject", "Subject already exists") discard tryExec(db, crud(crCreate, "thread_fts", "id", "name"), From 1ecca84b2e8655b9fba27e427351719e00f35bc3 Mon Sep 17 00:00:00 2001 From: Sergey Avseyev Date: Sun, 24 Jul 2016 21:21:39 +0300 Subject: [PATCH 047/451] Allow to specify root HTML node for RST formatter It turns out that many RSS readers (or at least two I'm using: http://feedly.com and http://twentyfivesquares.com/press/) do not handle non-HTML tags in Atom content tag and skipping the whole content of it. This patch allows to specify different (in this case
) node to make readers happy. --- forum.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forum.nim b/forum.nim index 0f0fd8c..001380b 100644 --- a/forum.nim +++ b/forum.nim @@ -467,7 +467,7 @@ proc rstToHtml(content: string): string = # TODO: Yes, this is ugly. I wrote it quickly. PRs welcome ;) try: var node = parseHtml(newStringStream(result)) - var newNode = newElement("document") + var newNode = newElement("div") if node.kind == xnElement: var currentBlockquote = newElement("blockquote") for n in items(node): From 82ce6a5ffcd614192e14407f0f1290ca676ec9c7 Mon Sep 17 00:00:00 2001 From: Yuriy Glukhov Date: Tue, 9 Aug 2016 18:32:27 +0300 Subject: [PATCH 048/451] Formatted emails --- forum.nim | 80 ++++++----------------------------------------------- utils.nim | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 88 insertions(+), 75 deletions(-) diff --git a/forum.nim b/forum.nim index 001380b..a20078b 100644 --- a/forum.nim +++ b/forum.nim @@ -8,8 +8,8 @@ import os, strutils, times, md5, strtabs, cgi, math, db_sqlite, matchers, - rst, rstgen, captchas, scgi, jester, asyncdispatch, asyncnet, cache, sequtils, - parseutils, utils, htmlparser, xmltree, streams, random + captchas, scgi, jester, asyncdispatch, asyncnet, cache, sequtils, + parseutils, utils, random, rst when not defined(windows): import bcrypt # TODO @@ -75,7 +75,6 @@ type var db: TDbConn - docConfig: StringTableRef isFTSAvailable: bool config: Config @@ -436,70 +435,6 @@ proc isPreview(c: TForumData): bool = proc isDelete(c: TForumData): bool = result = c.req.params.hasKey("delete") -proc processGT(n: XmlNode, tag: string): (int, XmlNode, string) = - result = (0, newElement(tag), tag) - if n.kind == xnElement and len(n) == 1 and n[0].kind == xnElement: - return processGT(n[0], if n[0].kind == xnElement: n[0].tag else: tag) - - var countGT = true - for c in items(n): - case c.kind - of xnText: - if c.text == ">" and countGT: - result[0].inc() - else: - countGT = false - result[1].add(newText(c.text)) - else: - result[1].add(c) - -proc blockquoteFinish(currentBlockquote, newNode: var XmlNode, n: XmlNode) = - if currentBlockquote.len > 0: - #echo(currentBlockquote.repr) - newNode.add(currentBlockquote) - currentBlockquote = newElement("blockquote") - newNode.add(n) - -proc rstToHtml(content: string): string = - result = rstgen.rstToHtml(content, {roSupportSmilies, roSupportMarkdown}, - docConfig) - # Bolt on quotes. - # TODO: Yes, this is ugly. I wrote it quickly. PRs welcome ;) - try: - var node = parseHtml(newStringStream(result)) - var newNode = newElement("div") - if node.kind == xnElement: - var currentBlockquote = newElement("blockquote") - for n in items(node): - case n.kind - of xnElement: - case n.tag - of "p": - let (nesting, contentNode, tag) = processGT(n, "p") - if nesting > 0: - var bq = currentBlockquote - for i in 1 .. 6: return true return false +proc makeThreadURL(c: var TForumData): string = + c.req.makeUri("/t/" & $c.threadId) + proc reply(c: var TForumData): bool = # reply to an existing thread checkLogin(c) @@ -695,7 +633,7 @@ proc reply(c: var TForumData): bool = exec(db, sql"update thread set modified = DATETIME('now') where id = ?", $c.threadId) asyncCheck sendMailToMailingList(c.config, c.username, c.email, - subject, content, threadId=c.threadId, postId=c.postID, is_reply=true) + subject, content, threadId=c.threadId, postId=c.postID, is_reply=true, threadUrl=c.makeThreadURL()) result = true proc newThread(c: var TForumData): bool = @@ -720,7 +658,7 @@ proc newThread(c: var TForumData): bool = discard tryExec(db, sql"insert into post_fts(post_fts) values('optimize')") discard tryExec(db, sql"insert into post_fts(thread_fts) values('optimize')") asyncCheck sendMailToMailingList(c.config, c.username, c.email, - subject, content, threadId=c.threadID, postId=c.postID, is_reply=false) + subject, content, threadId=c.threadID, postId=c.postID, is_reply=false, threadUrl=c.makeThreadURL()) result = true proc login(c: var TForumData, name, pass: string): bool = @@ -828,7 +766,7 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string = lastUrl = c.req.makeUri("/page/" & $(totalPages)) nextUrl = c.req.makeUri("/page/" & $(c.pageNum+1)) else: - firstUrl = c.req.makeUri("/t/" & $c.threadId) + firstUrl = c.makeThreadURL() if c.pageNum == 1: prevUrl = firstUrl else: @@ -1456,8 +1394,6 @@ routes: textPage "static/search-help" when isMainModule: - docConfig = rstgen.defaultConfig() - docConfig["doc.smiley_format"] = "/images/smilieys/$1.png" randomize() db = open(connection="nimforum.db", user="postgres", password="", database="nimforum") diff --git a/utils.nim b/utils.nim index ff9e855..a8dc3e1 100644 --- a/utils.nim +++ b/utils.nim @@ -1,4 +1,5 @@ -import asyncdispatch, smtp, strutils, json, os +import asyncdispatch, smtp, strutils, json, os, rst, rstgen, xmltree, strtabs, + htmlparser, streams from times import getTime, getGMTime, format type @@ -9,6 +10,11 @@ type smtpPassword: string mlistAddress: string +var docConfig: StringTableRef + +docConfig = rstgen.defaultConfig() +docConfig["doc.smiley_format"] = "/images/smilieys/$1.png" + proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config = result = Config(smtpAddress: "", smtpPort: 25, smtpUser: "", smtpPassword: "", mlistAddress: "") @@ -22,6 +28,70 @@ proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config = except: echo("[WARNING] Couldn't read config file: ", filename) +proc processGT(n: XmlNode, tag: string): (int, XmlNode, string) = + result = (0, newElement(tag), tag) + if n.kind == xnElement and len(n) == 1 and n[0].kind == xnElement: + return processGT(n[0], if n[0].kind == xnElement: n[0].tag else: tag) + + var countGT = true + for c in items(n): + case c.kind + of xnText: + if c.text == ">" and countGT: + result[0].inc() + else: + countGT = false + result[1].add(newText(c.text)) + else: + result[1].add(c) + +proc blockquoteFinish(currentBlockquote, newNode: var XmlNode, n: XmlNode) = + if currentBlockquote.len > 0: + #echo(currentBlockquote.repr) + newNode.add(currentBlockquote) + currentBlockquote = newElement("blockquote") + newNode.add(n) + +proc rstToHtml*(content: string): string = + result = rstgen.rstToHtml(content, {roSupportSmilies, roSupportMarkdown}, + docConfig) + # Bolt on quotes. + # TODO: Yes, this is ugly. I wrote it quickly. PRs welcome ;) + try: + var node = parseHtml(newStringStream(result)) + var newNode = newElement("div") + if node.kind == xnElement: + var currentBlockquote = newElement("blockquote") + for n in items(node): + case n.kind + of xnElement: + case n.tag + of "p": + let (nesting, contentNode, tag) = processGT(n, "p") + if nesting > 0: + var bq = currentBlockquote + for i in 1 .. View thread on Nim forum" + otherHeaders.add(("Content-Type", "text/html; charset=\"UTF-8\"")) + except: + processedMsg = message + + await sendMail(config, subject, processedMsg, config.mlistAddress, from_addr=from_addr, otherHeaders=otherHeaders) proc sendPassReset*(config: Config, email, user, resetUrl: string) {.async.} = let message = """Hello $1, From a6fdd16893b4252e974d55212ed765367cf5714d Mon Sep 17 00:00:00 2001 From: Araq Date: Sat, 31 Dec 2016 11:59:03 +0100 Subject: [PATCH 049/451] get rid of most deprecation warnings --- cache.nim | 4 +-- captchas.nim | 8 ++--- forms.tmpl | 4 +-- forum.nim | 84 +++++++++++++++++++++++++++------------------------- 4 files changed, 50 insertions(+), 50 deletions(-) diff --git a/cache.nim b/cache.nim index 8bc4274..6023393 100644 --- a/cache.nim +++ b/cache.nim @@ -16,13 +16,13 @@ proc newCacheHolder*(): CacheHolder = result.caches = initTable[string, CacheInfo]() proc invalidate*(cache: CacheHolder, name: string) = - cache.caches.mget(name.normalizePath()).valid = false + cache.caches[name.normalizePath()].valid = false proc invalidateAll*(cache: CacheHolder) = for key, val in mpairs(cache.caches): val.valid = false -template get*(cache: CacheHolder, name: string, grabValue: expr): expr = +template get*(cache: CacheHolder, name: string, grabValue: untyped): untyped = ## Check to see if the cache contains value for ``name``. If it does and the ## cache is valid then doesn't recalculate it but returns the cached version. mixin normalizePath diff --git a/captchas.nim b/captchas.nim index c4f6ac2..f569f04 100644 --- a/captchas.nim +++ b/captchas.nim @@ -11,7 +11,7 @@ import cairo, os, strutils, jester proc getCaptchaFilename*(i: int): string {.inline.} = result = "public/captchas/capture_" & $i & ".png" -proc getCaptchaUrl*(req: PRequest, i: int): string = +proc getCaptchaUrl*(req: Request, i: int): string = result = req.makeUri("/captchas/capture_" & $i & ".png", absolute = false) proc createCaptcha*(file, text: string) = @@ -23,17 +23,15 @@ proc createCaptcha*(file, text: string) = setSourceRgb(cr, 1.0, 0.5, 0.0) moveTo(cr, 0.0, 10.0) - showText(cr, repeatChar(text.len, 'O')) + showText(cr, repeat('O', text.len)) setSourceRgb(cr, 0.0, 0.0, 1.0) moveTo(cr, 0.0, 10.0) showText(cr, text) - + destroy(cr) discard writeToPng(surface, file) destroy(surface) when isMainModule: createCaptcha("test.png", "1+33") - - diff --git a/forms.tmpl b/forms.tmpl index a0ebaf2..70bf163 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -1,6 +1,6 @@ #? stdtmpl | standard # -#template `%`(idx: expr): expr {.immediate.} = +#template `%`(idx: untyped): untyped = # row[idx] #end template # @@ -288,7 +288,7 @@ # # #proc genSearchResults(c: var TForumData, -# results: iterator: db_sqlite.TRow {.closure, tags: [FReadDB].}, +# results: iterator: db_sqlite.Row {.closure, tags: [FReadDB].}, # count: var int): string = # const threadId = 0 # const threadName = 1 diff --git a/forum.nim b/forum.nim index a20078b..bc9a47c 100644 --- a/forum.nim +++ b/forum.nim @@ -40,7 +40,7 @@ type TPost = tuple[subject, content: string] TForumData = object of TSession - req: PRequest + req: Request userid: string actionContent: string errorMsg, loginErrorMsg: string @@ -74,7 +74,7 @@ type ForumError = object of Exception var - db: TDbConn + db: DbConn isFTSAvailable: bool config: Config @@ -202,7 +202,7 @@ proc formatTimestamp(t: int): string = return "just now" proc getGravatarUrl(email: string, size = 80): string = - let emailMD5 = email.toLower.toMD5 + let emailMD5 = email.toLowerAscii.toMD5 return ("http://www.gravatar.com/avatar/" & $emailMD5 & "?s=" & $size & "&d=identicon") @@ -268,7 +268,7 @@ proc makeIdentHash(user, password, epoch, secret: string, result = hash(user & password & epoch & secret, bcryptSalt) # ----------------------------------------------------------------------------- -template `||`(x: expr): expr = (if not isNil(x): x else: "") +template `||`(x: untyped): untyped = (if not isNil(x): x else: "") proc validThreadId(c: TForumData): bool = result = getValue(db, sql"select id from thread where id = ?", @@ -442,7 +442,7 @@ proc validateRst(c: var TForumData, content: string): bool = except EParseError: result = setError(c, "", getCurrentExceptionMsg()) -proc crud(c: TCrud, table: string, data: varargs[string]): TSqlQuery = +proc crud(c: TCrud, table: string, data: varargs[string]): SqlQuery = case c of crCreate: var fields = "insert into " & table & "(" @@ -470,14 +470,14 @@ proc crud(c: TCrud, table: string, data: varargs[string]): TSqlQuery = of crDelete: result = sql("delete from " & table & " where id = ?") -template retrSubject(c: expr) = +template retrSubject(c: untyped) = if not c.req.params.hasKey("subject"): raise newException(ForumError, "Subject empty") let subject {.inject.} = c.req.params["subject"] if subject.strip.len < 3: return setError(c, "subject", "Subject not long enough") -template retrContent(c: expr) = +template retrContent(c: untyped) = if not c.req.params.hasKey("content"): raise newException(ForumError, "Content empty") let content {.inject.} = c.req.params["content"] @@ -486,25 +486,25 @@ template retrContent(c: expr) = if not validateRst(c, content): return false -template retrPost(c: expr) = +template retrPost(c: untyped) = retrSubject(c) retrContent(c) -template checkLogin(c: expr) = +template checkLogin(c: untyped) = if not loggedIn(c): return setError(c, "", "User is not logged in") -template checkOwnership(c, postId: expr) = +template checkOwnership(c, postId: untyped) = if not c.isAdmin: let x = getValue(db, sql"select author from post where id = ?", postId) if x != c.userId: return setError(c, "", "You are not the owner of this post") -template setPreviewData(c: expr) {.immediate, dirty.} = +template setPreviewData(c: untyped) {.dirty.} = c.currentPost.subject = subject c.currentPost.content = content -template writeToDb(c, cr, setPostId: expr) = +template writeToDb(c, cr, setPostId: untyped) = # insert a comment in the DB let retID = insertID(db, crud(cr, "post", "author", "ip", "header", "content", "thread"), c.userId, c.req.ip, subject, content, $c.threadId, "") @@ -591,7 +591,8 @@ proc spamCheck(c: var TForumData, subject, content: string): bool = for word in ["appliance", "kitchen", "cheap", "sale", "relocating", "packers", "lenders", "fifa", "coins"]: - if word in subjAlphabet.toLower() or word in contentAlphabet.toLower(): + if word in subjAlphabet.toLowerAscii() or + word in contentAlphabet.toLowerAscii(): return true proc rateLimitCheck(c: var TForumData): bool = @@ -989,7 +990,7 @@ proc prependRe(s: string): string = elif s.startswith("Re:"): s else: "Re: " & s -template createTFD(): stmt = +template createTFD() = var c {.inject.}: TForumData init(c) c.req = request @@ -1031,7 +1032,7 @@ routes: parseInt(@"page", c.pageNum, 0..1000_000) if @"postid".len > 0: parseInt(@"postid", c.postId, 0..1000_000) - cond (c.pageNum > 0) + cond(c.pageNum > 0) var count = 0 var pSubject = getThreadTitle(c.threadid, c.pageNum) cond validThreadId(c) @@ -1066,9 +1067,9 @@ routes: get "/page/?@page?/?": createTFD() c.isThreadsList = true - cond (@"page" != "") + cond(@"page" != "") parseInt(@"page", c.pageNum, 0..1000_000) - cond (c.pageNum > 0) + cond(c.pageNum > 0) var count = 0 let list = genThreadsList(c, count) if count == 0: @@ -1078,7 +1079,7 @@ routes: get "/profile/@nick/?": createTFD() - cond (@"nick" != "") + cond(@"nick" != "") var userinfo: TUserInfo if gatherUserInfo(c, @"nick", userinfo): resp genMain(c, c.genProfile(userinfo), @@ -1099,18 +1100,18 @@ routes: createTFD() resp genMain(c, genFormRegister(c), "Register - Nim Forum") - template readIDs(): stmt = + template readIDs() = # Retrieve the threadid, postid and pagenum if (@"threadid").len > 0: parseInt(@"threadid", c.threadId, -1..1000_000) if (@"postid").len > 0: parseInt(@"postid", c.postId, -1..1000_000) - template finishLogin(): stmt = + template finishLogin() = setCookie("sid", c.userpass, daysForward(7)) redirect(uri("/")) - template handleError(action: string, topText: string, isEdit: bool): stmt = + template handleError(action: string, topText: string, isEdit: bool) = if c.isPreview: body.add genPostPreview(c, @"subject", @"content", c.userName, $getGMTime(getTime())) @@ -1176,8 +1177,8 @@ routes: get "/setUserStatus/?": createTFD() - cond (@"nick" != "") - cond (@"type" != "") + cond(@"nick" != "") + cond(@"type" != "") var formBody = "" var del = false @@ -1208,6 +1209,7 @@ routes: htmlgen.p("Are you sure you wish to activate ", htmlgen.b(@"nick"), "?") del = true + else: discard formBody.add "" content = content & htmlgen.form(action = c.req.makeUri("/dosetban"), `method` = "POST", formBody) @@ -1215,7 +1217,7 @@ routes: post "/dosetban": createTFD() - cond (@"nick" != "") + cond(@"nick" != "") if not c.isAdmin and @"nick" != c.userName: resp genMain(c, "You cannot ban this user.", "Error - Nim Forum") if @"reason" == "" and @"del" != "true": @@ -1234,7 +1236,7 @@ routes: get "/deleteAll/?": createTFD() - cond (@"nick" != "") + cond(@"nick" != "") var formBody = "" var del = false @@ -1248,7 +1250,7 @@ routes: post "/dodeleteall/?": createTFD() - cond (@"nick" != "") + cond(@"nick" != "") if not c.isAdmin: resp genMain(c, "You cannot delete this user's data.", "Error - Nim Forum") let result = deleteAll(c, @"nick") @@ -1260,8 +1262,8 @@ routes: get "/setpassword/?": createTFD() - cond (@"nick" != "") - cond (@"pass" != "") + cond(@"nick" != "") + cond(@"pass" != "") if not c.isAdmin: resp genMain(c, "You cannot change this user's pass.", "Error - Nim Forum") let res = setPassword(c, @"nick", @"pass") @@ -1272,9 +1274,9 @@ routes: get "/activateEmail/?": createTFD() - cond (@"nick" != "") - cond (@"epoch" != "") - cond (@"ident" != "") + cond(@"nick" != "") + cond(@"epoch" != "") + cond(@"ident" != "") var epoch: BiggestInt = 0 cond(parseBiggestInt(@"epoch", epoch) > 0) var success = false @@ -1290,9 +1292,9 @@ routes: get "/emailResetPassword/?": createTFD() - cond (@"nick" != "") - cond (@"epoch" != "") - cond (@"ident" != "") + cond(@"nick" != "") + cond(@"epoch" != "") + cond(@"ident" != "") var epoch: BiggestInt = 0 cond(parseBiggestInt(@"epoch", epoch) > 0) if verifyIdentHash(c, @"nick", $epoch, @"ident"): @@ -1314,10 +1316,10 @@ routes: post "/doemailresetpassword": createTFD() - cond (@"nick" != "") - cond (@"epoch" != "") - cond (@"ident" != "") - cond (@"password" != "") + cond(@"nick" != "") + cond(@"epoch" != "") + cond(@"ident" != "") + cond(@"password" != "") var epoch: BiggestInt = 0 cond(parseBiggestInt(@"epoch", epoch) > 0) if verifyIdentHash(c, @"nick", $epoch, @"ident"): @@ -1337,7 +1339,7 @@ routes: post "/doresetpassword": createTFD() echo(request.params) - cond (@"nick" != "") + cond(@"nick" != "") if resetPassword(c, @"nick", @"antibot"): resp genMain(c, "Email sent!", "Reset Password - Nim Forum") @@ -1362,7 +1364,7 @@ routes: c.search = q.replace("\"","""); if @"page".len > 0: parseInt(@"page", c.pageNum, 0..1000_000) - cond (c.pageNum > 0) + cond(c.pageNum > 0) iterator searchResults(): db_sqlite.TRow {.closure, tags: [FReadDB].} = const queryFT = "fts.sql".slurp.sql for rowFT in fastRows(db, queryFT, @@ -1373,7 +1375,7 @@ routes: additionalHeaders = genRSSHeaders(c), showRssLinks = true) # tries first to read html, then to read rst, convert ot html, cache and return - template textPage(path: string): stmt = + template textPage(path: string) = createTFD() #c.isThreadsList = true var page = "" From aff6e426eb6ed6e50d8d54ba7ff90a62bb4b71bc Mon Sep 17 00:00:00 2001 From: Araq Date: Sun, 1 Jan 2017 20:34:25 +0100 Subject: [PATCH 050/451] new Rank enum; admins and moderators; sane data model --- createdb.nim | 2 +- editdb.nim | 13 ++-- forms.tmpl | 47 +++++++------- forum.nim | 173 ++++++++++++++++++++++++++------------------------- ranks.nim | 12 ++++ utils.nim | 15 ++++- 6 files changed, 148 insertions(+), 114 deletions(-) create mode 100644 ranks.nim diff --git a/createdb.nim b/createdb.nim index 2d34099..13ef072 100644 --- a/createdb.nim +++ b/createdb.nim @@ -36,7 +36,7 @@ create table if not exists person( email $# not null, creation timestamp not null default (DATETIME('now')), salt varbin(128) not null, - status integer not null, + status varchar(30) not null, admin bool default false, lastOnline timestamp not null default (DATETIME('now')) );""" % [TUserName, TPassword, TEmail]), []) diff --git a/editdb.nim b/editdb.nim index ea28f93..13f77de 100644 --- a/editdb.nim +++ b/editdb.nim @@ -1,11 +1,12 @@ -import strutils, db_sqlite +import strutils, db_sqlite, ranks -var db = Open(connection="nimforum.db", user="postgres", password="", +var db = open(connection="nimforum.db", user="postgres", password="", database="nimforum") -db.exec(sql"""ALTER TABLE person add column - lastOnline timestamp -""", []) +db.exec(sql("update person set status = ?"), $User) +db.exec(sql("update person set status = ? where ban <> ''"), $Troll) +db.exec(sql("update person set status = ? where ban like '%spam%'"), $Spammer) +db.exec(sql("update person set status = ? where admin"), $Admin) -close(db) \ No newline at end of file +close(db) diff --git a/forms.tmpl b/forms.tmpl index 70bf163..862aff1 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -6,7 +6,10 @@ # # #proc genThreadsList(c: var TForumData, count: var int): string = -# const query = sql"select id, name, views, modified from thread order by modified desc limit ?, ?" +# const query = sql"""select id, name, views, modified from thread +# where id in (select thread from post where author in +# (select id from person where ban <> 'MODERATED')) +# order by modified desc limit ?, ?""" # const threadId = 0 # const name = 1 # const views = 2 @@ -113,7 +116,9 @@ # #proc genPostsList(c: var TForumData, threadId: string, count: var int): string = # const query = sql"""select p.id, u.name, p.header, p.content, p.creation, p.author, u.email from post p, -# person u where u.id = p.author and p.thread = ? order by p.id limit ?, ?""" +# person u +# where u.id = p.author and p.thread = ? and p.ban <> 'MODERATED' +# order by p.id limit ?, ?""" # const postId = 0 # const userName = 1 # const postHeader = 2 @@ -146,7 +151,7 @@ ${xmlEncode(%userName)} #if c.userId == %postAuthor and c.currentPost.subject.len == 0:
Edit post - #elif c.isAdmin and c.currentPost.subject.len == 0: + #elif c.rank >= Moderator and c.currentPost.subject.len == 0:
Edit post #end if
@@ -184,15 +189,15 @@
#if action == "doreply": - ${HiddenField(c, "subject", title)} + ${hiddenField(c, "subject", title)} #else: - ${FieldValid(c, "subject", "Subject:")} - ${TextWidget(c, "subject", title, maxlength=100)} + ${fieldValid(c, "subject", "Subject:")} + ${textWidget(c, "subject", title, maxlength=100)}
#end if - ${FieldValid(c, "content", "Content:")}
- ${TextAreaWidget(c, "content", content)}
- ${FormSession(c, action)} + ${fieldValid(c, "content", "Content:")}
+ ${textAreaWidget(c, "content", content)}
+ ${formSession(c, action)} # if isEdit: Delete Post
@@ -226,20 +231,20 @@ - - + + - + - - + + - - + +
${FieldValid(c, "name", "Username:")}${TextWidget(c, "name", reuseText, maxlength=20)}${fieldValid(c, "name", "Username:")}${textWidget(c, "name", reuseText, maxlength=20)}
${FieldValid(c, "new_password", "Password:")}${fieldValid(c, "new_password", "Password:")}
${FieldValid(c, "email", "E-Mail:")}${TextWidget(c, "email", reuseText, maxlength=300)}${fieldValid(c, "email", "E-Mail:")}${textWidget(c, "email", reuseText, maxlength=300)}
${FieldValid(c, "antibot", "What is " & antibot(c) & "?")}${TextWidget(c, "antibot", "", maxlength=4)}${fieldValid(c, "antibot", "What is " & antibot(c) & "?")}${textWidget(c, "antibot", "", maxlength=4)}
#if c.errorMsg != "": @@ -288,7 +293,7 @@ # # #proc genSearchResults(c: var TForumData, -# results: iterator: db_sqlite.Row {.closure, tags: [FReadDB].}, +# results: iterator: db_sqlite.Row {.closure, tags: [ReadDbEffect].}, # count: var int): string = # const threadId = 0 # const threadName = 1 @@ -326,7 +331,7 @@ #if c.userId == %postAuthor and c.currentPost.subject.len == 0:
Edit post - #elif c.isAdmin and c.currentPost.subject.len == 0: + #elif c.rank >= Moderator and c.currentPost.subject.len == 0:
Edit post #end if @@ -383,12 +388,12 @@ - + - - + +
${FieldValid(c, "nick", "Your nickname:")}${fieldValid(c, "nick", "Your nickname:")}
${FieldValid(c, "antibot", "What is " & antibot(c) & "?")}${TextWidget(c, "antibot", "", maxlength=4)}${fieldValid(c, "antibot", "What is " & antibot(c) & "?")}${textWidget(c, "antibot", "", maxlength=4)}
#if c.errorMsg != "": diff --git a/forum.nim b/forum.nim index bc9a47c..f13b500 100644 --- a/forum.nim +++ b/forum.nim @@ -7,9 +7,9 @@ # import - os, strutils, times, md5, strtabs, cgi, math, db_sqlite, matchers, + os, strutils, times, md5, strtabs, cgi, math, db_sqlite, captchas, scgi, jester, asyncdispatch, asyncnet, cache, sequtils, - parseutils, utils, random, rst + parseutils, utils, random, rst, ranks when not defined(windows): import bcrypt # TODO @@ -25,8 +25,6 @@ const MaxPagesFromCurrent = 8 noPageNums = ["/login", "/register", "/dologin", "/doregister", "/profile"] noHomeBtn = ["/", "/login", "/register", "/dologin", "/doregister", "/profile"] - banReasonDeactivated = "DEACTIVATED" - banReasonEmailUnconfirmed = "EMAILCONFIRMATION" type TCrud = enum crCreate, crRead, crUpdate, crDelete @@ -35,7 +33,7 @@ type threadid: int postid: int userName, userPass, email: string - isAdmin: bool + rank: Rank TPost = tuple[subject, content: string] @@ -70,6 +68,7 @@ type lastOnline: int email: string ban: string + rank: Rank ForumError = object of Exception @@ -103,27 +102,27 @@ proc loggedIn(c: TForumData): bool = const reuseText = "\1" -proc TextWidget(c: TForumData, name, defaultText: string, +proc textWidget(c: TForumData, name, defaultText: string, maxlength = 30, size = -1): string = let x = if defaultText != reuseText: defaultText else: xmlEncode(c.req.params.getOrDefault(name)) return """""" % [ name, $maxlength, x, if size != -1: "size=\"" & $size & "\"" else: ""] -proc HiddenField(c: TForumData, name, defaultText: string): string = +proc hiddenField(c: TForumData, name, defaultText: string): string = let x = xmlencode( if defaultText != reuseText: defaultText else: c.req.params.getOrDefault(name) ) return """""" % [name, x] -proc TextAreaWidget(c: TForumData, name, defaultText: string): string = +proc textAreaWidget(c: TForumData, name, defaultText: string): string = let x = if defaultText != reuseText: defaultText else: xmlEncode(c.req.params.getOrDefault(name)) return """""" % [ name, x] -proc FieldValid(c: TForumData, name, text: string): string = +proc fieldValid(c: TForumData, name, text: string): string = if name == c.invalidField: result = """$1""" % text else: @@ -141,12 +140,12 @@ proc genThreadUrl(c: TForumData, postId = "", action = "", threadid = "", pageNu result.add("#" & postId) result = c.req.makeUri(result, absolute = false) -proc FormSession(c: var TForumData, nextAction: string): string = +proc formSession(c: var TForumData, nextAction: string): string = return """ """ % [ $c.threadId, $c.postid] -proc UrlButton(c: var TForumData, text, url: string): string = +proc urlButton(c: var TForumData, text, url: string): string = return ("""$2""") % [ url, text] @@ -343,10 +342,10 @@ proc register(c: var TForumData, name, pass, antibot, # add account to person table exec(db, - sql("INSERT INTO person(name, password, email, salt, status, lastOnline, " & - "ban) VALUES (?, ?, ?, ?, 'user', DATETIME('now'), ?)"), name, + sql("INSERT INTO person(name, password, email, salt, status, lastOnline) " & + "VALUES (?, ?, ?, ?, ?, DATETIME('now'))"), name, password, email, salt, - when defined(dev): "" else: banReasonEmailUnconfirmed) + when defined(dev): $User else: $EmailUnconfirmed) return true @@ -384,15 +383,19 @@ proc logout(c: var TForumData) = c.userpass = "" exec(db, query, c.req.ip, c.req.cookies["sid"]) -proc getBanErrorMsg(banValue: string): string = - case banValue - of "": return "" - of banReasonDeactivated: - return "Your account has been deactivated." - of banReasonEmailUnconfirmed: - return "You need to confirm your email first." - else: +proc getBanErrorMsg(banValue: string; rank: Rank): string = + if banValue.len > 0: return "You have been banned: " & banValue + case rank + of Spammer: return "You are a spammer." + of Troll: return "You are a troll." + of Inactive: return "Your account has been deactivated." + of EmailUnconfirmed: + return "You need to confirm your email first." + of Moderated: + return "Your posts await moderation." + of User, Moderator, Admin: + return "" proc checkLoggedIn(c: var TForumData) = if not c.req.cookies.hasKey("sid"): return @@ -407,14 +410,13 @@ proc checkLoggedIn(c: var TForumData) = c.req.ip, pass) let row = getRow(db, - sql"select name, email, admin, ban from person where id = ?", c.userid) + sql"select name, email, status, ban from person where id = ?", c.userid) c.username = ||row[0] c.email = ||row[1] - c.isAdmin = parseBool(||row[2]) - # Check ban status. - let banErrorMsg = getBanErrorMsg(||row[3]) - if banErrorMsg.len > 0: - discard c.setError("name", banErrorMsg) + c.rank = parseEnum[Rank](||row[2]) + let ban = getBanErrorMsg(||row[3], c.rank) + if ban.len > 0: + discard c.setError("name", ban) logout(c) return @@ -494,7 +496,7 @@ template checkLogin(c: untyped) = if not loggedIn(c): return setError(c, "", "User is not logged in") template checkOwnership(c, postId: untyped) = - if not c.isAdmin: + if c.rank < Moderator: let x = getValue(db, sql"select author from post where id = ?", postId) if x != c.userId: @@ -617,6 +619,13 @@ proc rateLimitCheck(c: var TForumData): bool = proc makeThreadURL(c: var TForumData): string = c.req.makeUri("/t/" & $c.threadId) +template postChecks() {.dirty.} = + if spamCheck(c, subject, content): + echo("[WARNING] Found spam: ", subject) + return true + if rateLimitCheck(c): + return setError(c, "subject", "You're posting too fast.") + proc reply(c: var TForumData): bool = # reply to an existing thread checkLogin(c) @@ -624,17 +633,15 @@ proc reply(c: var TForumData): bool = if c.isPreview: setPreviewData(c) else: - if spamCheck(c, subject, content): - echo("[WARNING] Found spam: ", subject) - return true - if rateLimitCheck(c): - return setError(c, "subject", "You're posting too fast.") + postChecks() writeToDb(c, crCreate, true) exec(db, sql"update thread set modified = DATETIME('now') where id = ?", $c.threadId) - asyncCheck sendMailToMailingList(c.config, c.username, c.email, - subject, content, threadId=c.threadId, postId=c.postID, is_reply=true, threadUrl=c.makeThreadURL()) + if c.rank >= User: + asyncCheck sendMailToMailingList(c.config, c.username, c.email, + subject, content, threadId=c.threadId, postId=c.postID, is_reply=true, + threadUrl=c.makeThreadURL()) result = true proc newThread(c: var TForumData): bool = @@ -646,11 +653,7 @@ proc newThread(c: var TForumData): bool = setPreviewData(c) c.threadID = transientThread else: - if spamCheck(c, subject, content): - echo("[WARNING] Found spam: ", subject) - return true - if rateLimitCheck(c): - return setError(c, "subject", "You're posting too fast.") + postChecks() c.threadID = tryInsertID(db, query, c.req.params["subject"]).int if c.threadID < 0: return setError(c, "subject", "Subject already exists") discard tryExec(db, crud(crCreate, "thread_fts", "id", "name"), @@ -658,26 +661,29 @@ proc newThread(c: var TForumData): bool = writeToDb(c, crCreate, false) discard tryExec(db, sql"insert into post_fts(post_fts) values('optimize')") discard tryExec(db, sql"insert into post_fts(thread_fts) values('optimize')") - asyncCheck sendMailToMailingList(c.config, c.username, c.email, - subject, content, threadId=c.threadID, postId=c.postID, is_reply=false, threadUrl=c.makeThreadURL()) + if c.rank >= User: + asyncCheck sendMailToMailingList(c.config, c.username, c.email, + subject, content, threadId=c.threadID, postId=c.postID, is_reply=false, + threadUrl=c.makeThreadURL()) result = true proc login(c: var TForumData, name, pass: string): bool = # get form data: const query = - sql"select id, name, password, email, salt, admin, ban from person where name = ?" + sql"select id, name, password, email, salt, status, ban from person where name = ?" if name.len == 0: return c.setError("name", "Username cannot be nil.") var success = false for row in fastRows(db, query, name): if row[2] == makePassword(pass, row[4], row[2]): - if row[6].len > 0: - return c.setError("name", getBanErrorMsg(row[6])) + c.rank = parseEnum[Rank](row[5]) + let ban = getBanErrorMsg(row[6], c.rank) + if ban.len > 0: + return c.setError("name", ban) c.userid = row[0] c.username = row[1] c.userpass = row[2] c.email = row[3] - c.isAdmin = row[5].parseBool success = true break if success: @@ -705,6 +711,11 @@ proc setBan(c: var TForumData, nick, reason: string): bool = sql("update person set ban = ? where name = ?") return tryExec(db, query, reason, nick) +proc setStatus(c: var TForumData, nick: string, status: Rank): bool = + const query = + sql("update person set status = ? where name = ?") + return tryExec(db, query, $status, nick) + proc deleteAll(c: var TForumData, nick: string): bool = const query = sql("delete from post where author = (select id from person where name = ?)") @@ -874,7 +885,7 @@ proc gatherUserInfo(c: var TForumData, nick: string, ui: var TUserInfo): bool = if uid == "": return false result = true const totalPostsQuery = - sql"SELECT count(*) FROM post WHERE author = ?" + sql"select count(*) from post where author = ?" ui.posts = getValue(db, totalPostsQuery, uid).parseInt const totalThreadsQuery = sql("select count(*) from thread where id in (select thread from post where" & @@ -882,11 +893,13 @@ proc gatherUserInfo(c: var TForumData, nick: string, ui: var TUserInfo): bool = ui.threads = getValue(db, totalThreadsQuery, uid).parseInt const lastOnlineQuery = - sql"select strftime('%s', lastOnline) from person where id = ?" - let lastOnlineDBVal = getValue(db, lastOnlineQuery, uid) - ui.lastOnline = if lastOnlineDBVal != "": lastOnlineDBVal.parseInt else: -1 - ui.email = getValue(db, sql"select email from person where id = ?", uid) - ui.ban = getValue(db, sql"select ban from person where id = ?", uid) + sql"""select strftime('%s', lastOnline), email, ban, status + from person where id = ?""" + let row = db.getRow lastOnlineQuery + ui.lastOnline = if row[0].len > 0: row[0].parseInt else: -1 + ui.email = row[1] + ui.ban = row[2] + ui.rank = parseEnum[Rank](row[3]) proc genSetUserStatusUrl(c: var TForumData, nick: string, typ: string): string = c.req.makeUri("/setUserStatus?nick=$1&type=$2" % [nick, typ]) @@ -930,21 +943,12 @@ proc genProfile(c: var TForumData, ui: TUserInfo): string = ), tr( th("Status"), - td(case ui.ban - of banReasonDeactivated: - "Deactivated" - of banReasonEmailUnconfirmed: - "Awaiting email confirmation" - of "": - "Active" - else: - "Banned: " & ui.ban - ) + td($ui.rank) ), tr( th(""), - td(if c.isAdmin and ui.ban != banReasonDeactivated: - if ui.ban == "": + td(if c.rank >= Moderator and c.rank > ui.rank: + if ui.rank >= EmailUnconfirmed: htmlgen.a( href=c.genSetUserStatusUrl(ui.nick, "ban"), "Ban user") @@ -955,22 +959,22 @@ proc genProfile(c: var TForumData, ui: TUserInfo): string = ), tr( th(""), - td(if c.userName == ui.nick or c.isAdmin: - if ui.ban == "": - htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "deactivate"), - "Deactivate user") - elif ui.ban == banReasonDeactivated: - htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "activate"), - "Activate user") - elif ui.ban == banReasonEmailUnconfirmed: + td(if c.rank >= Moderator and c.rank > ui.rank: + if ui.rank == EmailUnconfirmed: htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "activate"), "Confirm user's email") + elif ui.rank > Moderated: + htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "deactivate"), + "Deactivate user") + elif ui.rank <= Moderated: + htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "activate"), + "Activate user") else: "" else: "") ), tr( th(""), - td(if c.isAdmin: + td(if c.rank >= Moderator: htmlgen.a(href=c.req.makeUri("/deleteAll?nick=$1" % ui.nick), "Delete all user's posts and threads") else: "") @@ -1197,9 +1201,7 @@ routes: "?") del = true of "deactivate": - formBody.add "" & - "" + formBody.add "" content = htmlgen.p("Are you sure you wish to deactivate ", htmlgen.b(@"nick"), "?") @@ -1218,10 +1220,11 @@ routes: post "/dosetban": createTFD() cond(@"nick" != "") - if not c.isAdmin and @"nick" != c.userName: + if c.rank < Moderator: resp genMain(c, "You cannot ban this user.", "Error - Nim Forum") if @"reason" == "" and @"del" != "true": resp genMain(c, "Invalid ban reason.", "Error - Nim Forum") + let result = if @"del" == "true": # Remove the ban. @@ -1251,7 +1254,7 @@ routes: post "/dodeleteall/?": createTFD() cond(@"nick" != "") - if not c.isAdmin: + if c.rank < Moderator: resp genMain(c, "You cannot delete this user's data.", "Error - Nim Forum") let result = deleteAll(c, @"nick") if result: @@ -1264,7 +1267,7 @@ routes: createTFD() cond(@"nick" != "") cond(@"pass" != "") - if not c.isAdmin: + if c.rank < Moderator: resp genMain(c, "You cannot change this user's pass.", "Error - Nim Forum") let res = setPassword(c, @"nick", @"pass") if res: @@ -1281,9 +1284,9 @@ routes: cond(parseBiggestInt(@"epoch", epoch) > 0) var success = false if verifyIdentHash(c, @"nick", $epoch, @"ident"): - let ban = db.getValue(sql"select ban from person where name = ?", @"nick") - if ban == banReasonEmailUnconfirmed: - success = setBan(c, @"nick", "") + let ban = parseEnum[Rank](db.getValue(sql"select status from person where name = ?", @"nick")) + if ban == EmailUnconfirmed: + success = setStatus(c, @"nick", Moderated) if success: resp genMain(c, "Account activated", "Nim Forum") @@ -1361,11 +1364,11 @@ routes: for i in 0 .. q.len-1: if q[i].int < 32: q[i] = ' ' elif q[i] == '\'': q[i] = '"' - c.search = q.replace("\"","""); + c.search = q.replace("\"",""") if @"page".len > 0: parseInt(@"page", c.pageNum, 0..1000_000) cond(c.pageNum > 0) - iterator searchResults(): db_sqlite.TRow {.closure, tags: [FReadDB].} = + iterator searchResults(): db_sqlite.Row {.closure, tags: [ReadDbEffect].} = const queryFT = "fts.sql".slurp.sql for rowFT in fastRows(db, queryFT, [q,q,$ThreadsPerPage,$c.pageNum,$ThreadsPerPage,q, diff --git a/ranks.nim b/ranks.nim new file mode 100644 index 0000000..995fdd1 --- /dev/null +++ b/ranks.nim @@ -0,0 +1,12 @@ + +type + Rank* = enum ## serialized as 'status' + Spammer ## spammer: every post is invisible + Troll ## troll: cannot write new posts + Inactive ## member is not inactive + EmailUnconfirmed ## member with unconfirmed email address + Moderated ## new member: posts manually reviewed before everybody + ## can see them + User ## Ordinary user + Moderator ## Moderator: can ban/troll/moderate users + Admin ## Admin: can do everything diff --git a/utils.nim b/utils.nim index a8dc3e1..002ee94 100644 --- a/utils.nim +++ b/utils.nim @@ -1,7 +1,20 @@ import asyncdispatch, smtp, strutils, json, os, rst, rstgen, xmltree, strtabs, - htmlparser, streams + htmlparser, streams, parseutils from times import getTime, getGMTime, format +proc parseInt*(s: string, value: var int, validRange: Slice[int]) {. + noSideEffect.} = + ## parses `s` into an integer in the range `validRange`. If successful, + ## `value` is modified to contain the result. Otherwise no exception is + ## raised and `value` is not touched; this way a reasonable default value + ## won't be overwritten. + var x = value + try: + discard parseutils.parseInt(s, x, 0) + except OverflowError: + discard + if x in validRange: value = x + type Config* = object smtpAddress: string From 30ed9e2076a2995f499150785d46d3df0d0d17a1 Mon Sep 17 00:00:00 2001 From: Araq Date: Sun, 1 Jan 2017 21:00:37 +0100 Subject: [PATCH 051/451] don't use admin field in db anymore --- createdb.nim | 1 - editdb.nim | 3 ++- forum.nim | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/createdb.nim b/createdb.nim index 13ef072..4162c36 100644 --- a/createdb.nim +++ b/createdb.nim @@ -37,7 +37,6 @@ create table if not exists person( creation timestamp not null default (DATETIME('now')), salt varbin(128) not null, status varchar(30) not null, - admin bool default false, lastOnline timestamp not null default (DATETIME('now')) );""" % [TUserName, TPassword, TEmail]), []) # echo "person table already exists" diff --git a/editdb.nim b/editdb.nim index 13f77de..234d204 100644 --- a/editdb.nim +++ b/editdb.nim @@ -7,6 +7,7 @@ var db = open(connection="nimforum.db", user="postgres", password="", db.exec(sql("update person set status = ?"), $User) db.exec(sql("update person set status = ? where ban <> ''"), $Troll) db.exec(sql("update person set status = ? where ban like '%spam%'"), $Spammer) -db.exec(sql("update person set status = ? where admin"), $Admin) +db.exec(sql("update person set status = ? where ban = 'DEACTIVATED' or ban = 'EMAILCONFIRMATION'"), $Inactive) +db.exec(sql("update person set status = ? where admin = 'true'"), $Admin) close(db) diff --git a/forum.nim b/forum.nim index f13b500..014b00d 100644 --- a/forum.nim +++ b/forum.nim @@ -58,8 +58,8 @@ type totalUsers: int totalPosts: int totalThreads: int - newestMember: tuple[nick: string, id: int, isAdmin: bool] - activeUsers: seq[tuple[nick: string, id: int, isAdmin: bool]] + newestMember: tuple[nick: string, id: int] + activeUsers: seq[tuple[nick: string, id: int]] TUserInfo = object nick: string @@ -750,16 +750,16 @@ proc getStats(c: var TForumData, simple: bool): TForumStats = if not simple: var newestMemberCreation = 0 result.activeUsers = @[] - result.newestMember = ("", -1, false) + result.newestMember = ("", -1) const getUsersQuery = - sql"select id, name, admin, strftime('%s', lastOnline), strftime('%s', creation) from person" + sql"select id, name, strftime('%s', lastOnline), strftime('%s', creation) from person" for row in fastRows(db, getUsersQuery): let secs = if row[3] == "": 0 else: row[3].parseint let lastOnlineSeconds = getTime() - Time(secs) if lastOnlineSeconds < (60 * 5): # 5 minutes - result.activeUsers.add((row[1], row[0].parseInt, row[2].parseBool)) + result.activeUsers.add((row[1], row[0].parseInt)) if row[4].parseInt > newestMemberCreation: - result.newestMember = (row[1], row[0].parseInt, row[2].parseBool) + result.newestMember = (row[1], row[0].parseInt) newestMemberCreation = row[4].parseInt proc genPagenumNav(c: var TForumData, stats: TForumStats): string = From ca06f4e988d5050e4bc81b799ae5288f35c9751d Mon Sep 17 00:00:00 2001 From: Araq Date: Sun, 1 Jan 2017 23:24:58 +0100 Subject: [PATCH 052/451] new version compiles; still unfinished --- forms.tmpl | 35 ++++++++++++-- forum.nim | 137 +++++++++++++++-------------------------------------- 2 files changed, 69 insertions(+), 103 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index 862aff1..91f1b21 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -6,9 +6,11 @@ # # #proc genThreadsList(c: var TForumData, count: var int): string = +# const queryAdmin = sql"""select id, name, views, modified from thread +# order by modified desc limit ?, ?""" # const query = sql"""select id, name, views, modified from thread # where id in (select thread from post where author in -# (select id from person where ban <> 'MODERATED')) +# (select id from person where status <> 'MODERATED')) # order by modified desc limit ?, ?""" # const threadId = 0 # const name = 1 @@ -37,7 +39,8 @@
-# for row in rows(db, query, $((c.pageNum-1) * ThreadsPerPage), $ThreadsPerPage): +# for row in rows(db, if c.rank >= Moderator: queryAdmin else: query, +# $((c.pageNum-1) * ThreadsPerPage), $ThreadsPerPage): # inc(count)
@@ -115,10 +118,11 @@ # # #proc genPostsList(c: var TForumData, threadId: string, count: var int): string = -# const query = sql"""select p.id, u.name, p.header, p.content, p.creation, p.author, u.email from post p, +# let query = sql("""select p.id, u.name, p.header, p.content, p.creation, p.author, u.email from post p, # person u -# where u.id = p.author and p.thread = ? and p.ban <> 'MODERATED' -# order by p.id limit ?, ?""" +# where u.id = p.author and p.thread = ? $# +# order by p.id limit ?, ?""" % +# (if c.rank >= Moderator: "" else: "and p.status <> 'MODERATED'")) # const postId = 0 # const userName = 1 # const postHeader = 2 @@ -256,6 +260,27 @@ #end proc # +#proc genFormSetRank(c: var TForumData; ui: TUserInfo): string = +# result = "" +
+ + + + + + + + + + +#end proc +# #proc genFormLogin(c: var TForumData): string = # result = "" # if not c.loggedIn: diff --git a/forum.nim b/forum.nim index 014b00d..951d710 100644 --- a/forum.nim +++ b/forum.nim @@ -345,7 +345,7 @@ proc register(c: var TForumData, name, pass, antibot, sql("INSERT INTO person(name, password, email, salt, status, lastOnline) " & "VALUES (?, ?, ?, ?, ?, DATETIME('now'))"), name, password, email, salt, - when defined(dev): $User else: $EmailUnconfirmed) + when defined(dev): $Moderated else: $EmailUnconfirmed) return true @@ -706,15 +706,11 @@ proc verifyIdentHash(c: var TForumData, name, epoch, ident: string): bool = if row[2].parseInt > (epoch.parseInt + 60): return false result = newIdent == ident -proc setBan(c: var TForumData, nick, reason: string): bool = +proc setStatus(c: var TForumData, nick: string, status: Rank; + reason: string): bool = const query = - sql("update person set ban = ? where name = ?") - return tryExec(db, query, reason, nick) - -proc setStatus(c: var TForumData, nick: string, status: Rank): bool = - const query = - sql("update person set status = ? where name = ?") - return tryExec(db, query, $status, nick) + sql("update person set status = ?, ban = ? where name = ?") + return tryExec(db, query, $status, reason, nick) proc deleteAll(c: var TForumData, nick: string): bool = const query = @@ -758,9 +754,9 @@ proc getStats(c: var TForumData, simple: bool): TForumStats = let lastOnlineSeconds = getTime() - Time(secs) if lastOnlineSeconds < (60 * 5): # 5 minutes result.activeUsers.add((row[1], row[0].parseInt)) - if row[4].parseInt > newestMemberCreation: + if row[3].parseInt > newestMemberCreation: result.newestMember = (row[1], row[0].parseInt) - newestMemberCreation = row[4].parseInt + newestMemberCreation = row[3].parseInt proc genPagenumNav(c: var TForumData, stats: TForumStats): string = result = "" @@ -895,14 +891,14 @@ proc gatherUserInfo(c: var TForumData, nick: string, ui: var TUserInfo): bool = const lastOnlineQuery = sql"""select strftime('%s', lastOnline), email, ban, status from person where id = ?""" - let row = db.getRow lastOnlineQuery + let row = db.getRow(lastOnlineQuery, $uid) ui.lastOnline = if row[0].len > 0: row[0].parseInt else: -1 ui.email = row[1] ui.ban = row[2] ui.rank = parseEnum[Rank](row[3]) -proc genSetUserStatusUrl(c: var TForumData, nick: string, typ: string): string = - c.req.makeUri("/setUserStatus?nick=$1&type=$2" % [nick, typ]) +include "forms.tmpl" +include "main.tmpl" proc genProfile(c: var TForumData, ui: TUserInfo): string = result = "" @@ -948,28 +944,7 @@ proc genProfile(c: var TForumData, ui: TUserInfo): string = tr( th(""), td(if c.rank >= Moderator and c.rank > ui.rank: - if ui.rank >= EmailUnconfirmed: - htmlgen.a( - href=c.genSetUserStatusUrl(ui.nick, "ban"), - "Ban user") - else: - htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "unban"), - "Unban user") - else: "") - ), - tr( - th(""), - td(if c.rank >= Moderator and c.rank > ui.rank: - if ui.rank == EmailUnconfirmed: - htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "activate"), - "Confirm user's email") - elif ui.rank > Moderated: - htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "deactivate"), - "Deactivate user") - elif ui.rank <= Moderated: - htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "activate"), - "Activate user") - else: "" + c.genFormSetRank(ui) else: "") ), tr( @@ -985,9 +960,6 @@ proc genProfile(c: var TForumData, ui: TUserInfo): string = result = htmlgen.`div`(id = "profile", htmlgen.`div`(id = "left", result)) -include "forms.tmpl" -include "main.tmpl" - proc prependRe(s: string): string = result = if s.len == 0: "" @@ -1179,64 +1151,6 @@ routes: resp genMain(c, genFormPost(c, "donewthread", "New thread", "", "", false), "New Thread - Nim Forum") - get "/setUserStatus/?": - createTFD() - cond(@"nick" != "") - cond(@"type" != "") - var formBody = "" - var del = false - var content = "" - echo("Got type: ", @"type") - case @"type" - of "ban": - formBody.add "" & - "" - content = - htmlgen.p("Please enter a reason for banning this user:") - of "unban": - formBody.add "" - content = - htmlgen.p("Are you sure you wish to unban ", htmlgen.b(@"nick"), - "?") - del = true - of "deactivate": - formBody.add "" - content = - htmlgen.p("Are you sure you wish to deactivate ", htmlgen.b(@"nick"), - "?") - of "activate": - formBody.add "" - content = - htmlgen.p("Are you sure you wish to activate ", htmlgen.b(@"nick"), - "?") - del = true - else: discard - formBody.add "" - content = content & htmlgen.form(action = c.req.makeUri("/dosetban"), - `method` = "POST", formBody) - resp genMain(c, content, "Set user status - Nim Forum") - - post "/dosetban": - createTFD() - cond(@"nick" != "") - if c.rank < Moderator: - resp genMain(c, "You cannot ban this user.", "Error - Nim Forum") - if @"reason" == "" and @"del" != "true": - resp genMain(c, "Invalid ban reason.", "Error - Nim Forum") - - let result = - if @"del" == "true": - # Remove the ban. - setBan(c, @"nick", "") - else: - setBan(c, @"nick", @"reason") - if result: - redirect(c.req.makeUri("/profile/" & @"nick")) - else: - resp genMain(c, "Failed to change the ban status of user.", - "Error - Nim Forum") - get "/deleteAll/?": createTFD() cond(@"nick" != "") @@ -1263,6 +1177,33 @@ routes: resp genMain(c, "Failed to delete all user's posts and threads.", "Error - NimForum") + post "/dosetrank/?": + createTFD() + cond(@"nick" != "") + + if c.rank < Moderator: + resp genMain(c, "You cannot change this user's rank.", "Error - Nim Forum") + + var ui: TUserInfo + if not gatherUserInfo(c, @"nick", ui): + resp genMain(c, "User " & @"nick" & " does not exist.", "Error - Nim Forum") + #elif ui. + # XXX check that moderator can make themselves admins + echo(@"rank") + echo(@"reason") + when false: + let result = + if @"del" == "true": + # Remove the ban. + setBan(c, @"nick", "") + else: + setBan(c, @"nick", @"reason") + if result: + redirect(c.req.makeUri("/profile/" & @"nick")) + else: + resp genMain(c, "Failed to change the ban status of user.", + "Error - Nim Forum") + get "/setpassword/?": createTFD() cond(@"nick" != "") @@ -1286,7 +1227,7 @@ routes: if verifyIdentHash(c, @"nick", $epoch, @"ident"): let ban = parseEnum[Rank](db.getValue(sql"select status from person where name = ?", @"nick")) if ban == EmailUnconfirmed: - success = setStatus(c, @"nick", Moderated) + success = setStatus(c, @"nick", Moderated, "") if success: resp genMain(c, "Account activated", "Nim Forum") From 31e62f83bbfdd2a6c8d41c1ab56f768ab57cc043 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 2 Jan 2017 02:45:27 +0100 Subject: [PATCH 053/451] everything works now --- forms.tmpl | 13 ++++++++----- forum.nim | 31 +++++++++++-------------------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index 91f1b21..5e758a5 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -7,10 +7,11 @@ # #proc genThreadsList(c: var TForumData, count: var int): string = # const queryAdmin = sql"""select id, name, views, modified from thread +# where 1 or id = ? # order by modified desc limit ?, ?""" # const query = sql"""select id, name, views, modified from thread # where id in (select thread from post where author in -# (select id from person where status <> 'MODERATED')) +# (select id from person where status <> 'Moderated' or id = ?)) # order by modified desc limit ?, ?""" # const threadId = 0 # const name = 1 @@ -40,7 +41,7 @@
# for row in rows(db, if c.rank >= Moderator: queryAdmin else: query, -# $((c.pageNum-1) * ThreadsPerPage), $ThreadsPerPage): +# c.userId, $((c.pageNum-1) * ThreadsPerPage), $ThreadsPerPage): # inc(count)
@@ -120,9 +121,9 @@ #proc genPostsList(c: var TForumData, threadId: string, count: var int): string = # let query = sql("""select p.id, u.name, p.header, p.content, p.creation, p.author, u.email from post p, # person u -# where u.id = p.author and p.thread = ? $# +# where u.id = p.author and p.thread = ? and $# # order by p.id limit ?, ?""" % -# (if c.rank >= Moderator: "" else: "and p.status <> 'MODERATED'")) +# (if c.rank >= Moderator: "(1 or u.id = ?)" else: "(u.status <> 'Moderated' or p.author = ?)")) # const postId = 0 # const userName = 1 # const postHeader = 2 @@ -132,7 +133,7 @@ # const userEmail = 6 # result = "" # count = 0 -# let posts = getAllRows(db, query, threadId, $((c.pageNum-1) * PostsPerPage), $PostsPerPage) +# let posts = getAllRows(db, query, threadId, c.userId, $((c.pageNum-1) * PostsPerPage), $PostsPerPage) # if posts.len < 1: return "" # end if
@@ -278,6 +279,8 @@ # end for +
Reason${textWidget(c, "reason", ui.ban, maxlength=100)}
Rank
+ #end proc # diff --git a/forum.nim b/forum.nim index 951d710..518e094 100644 --- a/forum.nim +++ b/forum.nim @@ -392,9 +392,7 @@ proc getBanErrorMsg(banValue: string; rank: Rank): string = of Inactive: return "Your account has been deactivated." of EmailUnconfirmed: return "You need to confirm your email first." - of Moderated: - return "Your posts await moderation." - of User, Moderator, Admin: + of Moderated, User, Moderator, Admin: return "" proc checkLoggedIn(c: var TForumData) = @@ -1177,7 +1175,7 @@ routes: resp genMain(c, "Failed to delete all user's posts and threads.", "Error - NimForum") - post "/dosetrank/?": + post "/dosetrank/?@nick?/?": createTFD() cond(@"nick" != "") @@ -1187,22 +1185,15 @@ routes: var ui: TUserInfo if not gatherUserInfo(c, @"nick", ui): resp genMain(c, "User " & @"nick" & " does not exist.", "Error - Nim Forum") - #elif ui. - # XXX check that moderator can make themselves admins - echo(@"rank") - echo(@"reason") - when false: - let result = - if @"del" == "true": - # Remove the ban. - setBan(c, @"nick", "") - else: - setBan(c, @"nick", @"reason") - if result: - redirect(c.req.makeUri("/profile/" & @"nick")) - else: - resp genMain(c, "Failed to change the ban status of user.", - "Error - Nim Forum") + let newRank = parseEnum[Rank](@"rank") + if newRank > c.rank: + resp genMain(c, "You cannot change this user's rank to this value.", "Error - Nim Forum") + + if setStatus(c, @"nick", newRank, @"reason"): + redirect(c.req.makeUri("/profile/" & @"nick")) + else: + resp genMain(c, "Failed to change the ban status of user.", + "Error - Nim Forum") get "/setpassword/?": createTFD() From a2edc92fc5608be0782bf972462bad2e98afd25f Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 2 Jan 2017 10:55:54 +0100 Subject: [PATCH 054/451] posts from spammers are not visible --- forms.tmpl | 8 +++++--- forum.nim | 18 ++++++++++++------ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index 5e758a5..ba7531f 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -7,11 +7,12 @@ # #proc genThreadsList(c: var TForumData, count: var int): string = # const queryAdmin = sql"""select id, name, views, modified from thread -# where 1 or id = ? +# where id in (select thread from post where author in +# (select id from person where status not in ('Spammer') or id = ?)) # order by modified desc limit ?, ?""" # const query = sql"""select id, name, views, modified from thread # where id in (select thread from post where author in -# (select id from person where status <> 'Moderated' or id = ?)) +# (select id from person where status not in ('Moderated', 'Spammer') or id = ?)) # order by modified desc limit ?, ?""" # const threadId = 0 # const name = 1 @@ -122,6 +123,7 @@ # let query = sql("""select p.id, u.name, p.header, p.content, p.creation, p.author, u.email from post p, # person u # where u.id = p.author and p.thread = ? and $# +# and (u.status <> 'Spammer' or p.author = ?) # order by p.id limit ?, ?""" % # (if c.rank >= Moderator: "(1 or u.id = ?)" else: "(u.status <> 'Moderated' or p.author = ?)")) # const postId = 0 @@ -133,7 +135,7 @@ # const userEmail = 6 # result = "" # count = 0 -# let posts = getAllRows(db, query, threadId, c.userId, $((c.pageNum-1) * PostsPerPage), $PostsPerPage) +# let posts = getAllRows(db, query, threadId, c.userId, c.userId, $((c.pageNum-1) * PostsPerPage), $PostsPerPage) # if posts.len < 1: return "" # end if
diff --git a/forum.nim b/forum.nim index 518e094..b65e6e2 100644 --- a/forum.nim +++ b/forum.nim @@ -704,18 +704,24 @@ proc verifyIdentHash(c: var TForumData, name, epoch, ident: string): bool = if row[2].parseInt > (epoch.parseInt + 60): return false result = newIdent == ident -proc setStatus(c: var TForumData, nick: string, status: Rank; - reason: string): bool = - const query = - sql("update person set status = ?, ban = ? where name = ?") - return tryExec(db, query, $status, reason, nick) - proc deleteAll(c: var TForumData, nick: string): bool = const query = sql("delete from post where author = (select id from person where name = ?)") result = tryExec(db, query, nick) result = result and updateThreads(c) >= 0 +proc setStatus(c: var TForumData, nick: string, status: Rank; + reason: string): bool = + const query = + sql("update person set status = ?, ban = ? where name = ?") + result = tryExec(db, query, $status, reason, nick) + when false: + # for now we filter Spammers in forms.tmpl, so that a moderator + # cannot accidentically delete all of a user's posts. We go even + # further than that and show spammers their own spam postings. + if status == Spammer and result: + result = deleteAll(c, nick) + proc setPassword(c: var TForumData, nick, pass: string): bool = const query = sql("update person set password = ?, salt = ? where name = ?") From 9788e93676443e225732e5887f7e5d1ca24fa03d Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 2 Jan 2017 20:49:27 +0100 Subject: [PATCH 055/451] adjusted to dom's remarks --- editdb.nim | 2 +- forms.tmpl | 4 ++-- forum.nim | 3 +-- ranks.nim | 3 +-- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/editdb.nim b/editdb.nim index 234d204..e12f679 100644 --- a/editdb.nim +++ b/editdb.nim @@ -7,7 +7,7 @@ var db = open(connection="nimforum.db", user="postgres", password="", db.exec(sql("update person set status = ?"), $User) db.exec(sql("update person set status = ? where ban <> ''"), $Troll) db.exec(sql("update person set status = ? where ban like '%spam%'"), $Spammer) -db.exec(sql("update person set status = ? where ban = 'DEACTIVATED' or ban = 'EMAILCONFIRMATION'"), $Inactive) +db.exec(sql("update person set status = ? where ban = 'DEACTIVATED' or ban = 'EMAILCONFIRMATION'"), $EmailUnconfirmed) db.exec(sql("update person set status = ? where admin = 'true'"), $Admin) close(db) diff --git a/forms.tmpl b/forms.tmpl index ba7531f..9d62d95 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -6,7 +6,7 @@ # # #proc genThreadsList(c: var TForumData, count: var int): string = -# const queryAdmin = sql"""select id, name, views, modified from thread +# const queryModAdmin = sql"""select id, name, views, modified from thread # where id in (select thread from post where author in # (select id from person where status not in ('Spammer') or id = ?)) # order by modified desc limit ?, ?""" @@ -41,7 +41,7 @@
-# for row in rows(db, if c.rank >= Moderator: queryAdmin else: query, +# for row in rows(db, if c.rank >= Moderator: queryModAdmin else: query, # c.userId, $((c.pageNum-1) * ThreadsPerPage), $ThreadsPerPage): # inc(count)
diff --git a/forum.nim b/forum.nim index b65e6e2..1b29cdf 100644 --- a/forum.nim +++ b/forum.nim @@ -388,8 +388,7 @@ proc getBanErrorMsg(banValue: string; rank: Rank): string = return "You have been banned: " & banValue case rank of Spammer: return "You are a spammer." - of Troll: return "You are a troll." - of Inactive: return "Your account has been deactivated." + of Troll: return "You have been banned." of EmailUnconfirmed: return "You need to confirm your email first." of Moderated, User, Moderator, Admin: diff --git a/ranks.nim b/ranks.nim index 995fdd1..3b518f4 100644 --- a/ranks.nim +++ b/ranks.nim @@ -3,10 +3,9 @@ type Rank* = enum ## serialized as 'status' Spammer ## spammer: every post is invisible Troll ## troll: cannot write new posts - Inactive ## member is not inactive EmailUnconfirmed ## member with unconfirmed email address Moderated ## new member: posts manually reviewed before everybody ## can see them User ## Ordinary user - Moderator ## Moderator: can ban/troll/moderate users + Moderator ## Moderator: can ban/moderate users Admin ## Admin: can do everything From e65168db55a91874abecefc5abb76aaa4a68e365 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 5 Jan 2017 11:49:13 +0100 Subject: [PATCH 056/451] db model: introduce indexes for better performance --- createdb.nim | 4 ++++ editdb.nim | 16 +++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/createdb.nim b/createdb.nim index 4162c36..83e4b86 100644 --- a/createdb.nim +++ b/createdb.nim @@ -90,6 +90,10 @@ create table if not exists antibot( );""", []): echo "antibot table already exists" + +db.exec sql"create index PersonStatusIdx on person(status);" +db.exec sql"create index PostByAuthorIdx on post(thread, author);" + # -------------------- Search -------------------------------------------------- if not db.tryExec(sql""" diff --git a/editdb.nim b/editdb.nim index e12f679..7588822 100644 --- a/editdb.nim +++ b/editdb.nim @@ -4,10 +4,16 @@ import strutils, db_sqlite, ranks var db = open(connection="nimforum.db", user="postgres", password="", database="nimforum") -db.exec(sql("update person set status = ?"), $User) -db.exec(sql("update person set status = ? where ban <> ''"), $Troll) -db.exec(sql("update person set status = ? where ban like '%spam%'"), $Spammer) -db.exec(sql("update person set status = ? where ban = 'DEACTIVATED' or ban = 'EMAILCONFIRMATION'"), $EmailUnconfirmed) -db.exec(sql("update person set status = ? where admin = 'true'"), $Admin) +when false: + db.exec(sql("update person set status = ?"), $User) + db.exec(sql("update person set status = ? where ban <> ''"), $Troll) + db.exec(sql("update person set status = ? where ban like '%spam%'"), $Spammer) + db.exec(sql("update person set status = ? where ban = 'DEACTIVATED' or ban = 'EMAILCONFIRMATION'"), $EmailUnconfirmed) + db.exec(sql("update person set status = ? where admin = 'true'"), $Admin) +else: + db.exec sql"create index PersonStatusIdx on person(status);" + db.exec sql"create index PostByAuthorIdx on post(thread, author);" + db.exec sql"update person set name = 'cheatfate' where name = 'ka';" + close(db) From 5184c7c281c2df06e044192fdd77bc2ebf10d0e9 Mon Sep 17 00:00:00 2001 From: Simon Krauter Date: Sat, 25 Feb 2017 20:03:07 +0100 Subject: [PATCH 057/451] CSS: Add missing font color definition Fixes: On my pc, text and background have the same color. --- public/css/style.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index fa38840..5a731e7 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -106,7 +106,7 @@ pre .EscapeSequence background:url("/images/glow-line-vert.png") no-repeat; } -#body { z-index:1; position:relative; background:rgba(220,231,248,.6); } +#body { z-index:1; position:relative; background:rgba(220,231,248,.6); color:black; } #body.docs { margin:0 40px 20px 320px; } #body.forum { margin:0 40px 20px 40px; min-height: 700px; } @@ -710,4 +710,4 @@ blockquote { blockquote p { color: rgb(109, 109, 109) !important; -} \ No newline at end of file +} From 720f38b3d45064cd8b0fd5e1e6cf6295ce600709 Mon Sep 17 00:00:00 2001 From: Euan T Date: Tue, 28 Feb 2017 13:14:51 +0000 Subject: [PATCH 058/451] Update Gravatar URL to use HTTPS Gravatars are currently loaded over HTTP, causing mixed content forums. Changing to HTTPS is a trivial change that makes sense in my opinion. --- forum.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forum.nim b/forum.nim index 1b29cdf..e006d7a 100644 --- a/forum.nim +++ b/forum.nim @@ -202,7 +202,7 @@ proc formatTimestamp(t: int): string = proc getGravatarUrl(email: string, size = 80): string = let emailMD5 = email.toLowerAscii.toMD5 - return ("http://www.gravatar.com/avatar/" & $emailMD5 & "?s=" & $size & + return ("https://www.gravatar.com/avatar/" & $emailMD5 & "?s=" & $size & "&d=identicon") proc genGravatar(email: string, size: int = 80): string = From 9200165c42c6fc4fab7d5f8fee9c87b7e4378327 Mon Sep 17 00:00:00 2001 From: Euan Torano Date: Wed, 1 Mar 2017 18:55:58 +0000 Subject: [PATCH 059/451] =?UTF-8?q?Show=20the=20user=E2=80=99s=20last=20IP?= =?UTF-8?q?=20on=20profile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Euan Torano --- forum.nim | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/forum.nim b/forum.nim index e006d7a..7414144 100644 --- a/forum.nim +++ b/forum.nim @@ -69,6 +69,7 @@ type email: string ban: string rank: Rank + lastIp: string ForumError = object of Exception @@ -900,6 +901,10 @@ proc gatherUserInfo(c: var TForumData, nick: string, ui: var TUserInfo): bool = ui.ban = row[2] ui.rank = parseEnum[Rank](row[3]) + const lastIpQuery = sql"select `ip` from `session` where `userid` = ? order by `id` desc limit 1;" + let ipRow = db.getRow(lastIpQuery, $uid) + ui.lastIp = ipRow[0] + include "forms.tmpl" include "main.tmpl" @@ -944,6 +949,12 @@ proc genProfile(c: var TForumData, ui: TUserInfo): string = th("Status"), td($ui.rank) ), + tr( + th(if c.rank >= Moderator: "Last IP" else: ""), + td(if c.rank >= Moderator: + htmlgen.a(href="http://whatismyipaddress.com/ip/" & encodeUrl(ui.lastIp), ui.lastIp) + else: "") + ), tr( th(""), td(if c.rank >= Moderator and c.rank > ui.rank: From c1bd44b997e55ad98d8ea9ccddb13ec53cc3c95d Mon Sep 17 00:00:00 2001 From: Euan Torano Date: Tue, 7 Mar 2017 18:57:05 +0000 Subject: [PATCH 060/451] Adding reCAPTCHA rather than the custom captcha. Signed-off-by: Euan Torano --- captchas.nim | 37 ------------------------------------- forms.tmpl | 6 ++++-- forum.json.example | 4 ++++ forum.nim | 25 ++++++++++++++++++++----- forum.nim.cfg | 2 ++ nimforum.nimble | 2 +- public/captchas/.gitignore | 2 -- utils.nim | 4 ++++ 8 files changed, 35 insertions(+), 47 deletions(-) delete mode 100644 captchas.nim create mode 100644 forum.json.example delete mode 100644 public/captchas/.gitignore diff --git a/captchas.nim b/captchas.nim deleted file mode 100644 index f569f04..0000000 --- a/captchas.nim +++ /dev/null @@ -1,37 +0,0 @@ -# -# -# The Nim Forum -# (c) Copyright 2012 Andreas Rumpf, Dominik Picheta -# Look at license.txt for more info. -# All rights reserved. -# - -import cairo, os, strutils, jester - -proc getCaptchaFilename*(i: int): string {.inline.} = - result = "public/captchas/capture_" & $i & ".png" - -proc getCaptchaUrl*(req: Request, i: int): string = - result = req.makeUri("/captchas/capture_" & $i & ".png", absolute = false) - -proc createCaptcha*(file, text: string) = - var surface = imageSurfaceCreate(FORMAT_ARGB32, int32(10*text.len), int32(10)) - var cr = create(surface) - - selectFontFace(cr, "serif", FONT_SLANT_NORMAL, FONT_WEIGHT_BOLD) - setFontSize(cr, 12.0) - - setSourceRgb(cr, 1.0, 0.5, 0.0) - moveTo(cr, 0.0, 10.0) - showText(cr, repeat('O', text.len)) - - setSourceRgb(cr, 0.0, 0.0, 1.0) - moveTo(cr, 0.0, 10.0) - showText(cr, text) - - destroy(cr) - discard writeToPng(surface, file) - destroy(surface) - -when isMainModule: - createCaptcha("test.png", "1+33") diff --git a/forms.tmpl b/forms.tmpl index 9d62d95..9f6d2a7 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -249,10 +249,12 @@ ${fieldValid(c, "email", "E-Mail:")} ${textWidget(c, "email", reuseText, maxlength=300)} + #if useCaptcha: - ${fieldValid(c, "antibot", "What is " & antibot(c) & "?")} - ${textWidget(c, "antibot", "", maxlength=4)} + ${fieldValid(c, "g-recaptcha-response", "Captcha:")} + ${captcha.render(includeNoScript=true)} + #end if #if c.errorMsg != "":
diff --git a/forum.json.example b/forum.json.example new file mode 100644 index 0000000..8cf9e86 --- /dev/null +++ b/forum.json.example @@ -0,0 +1,4 @@ +{ + "recaptchaSecretKey": "", + "recaptchaSiteKey": "" +} \ No newline at end of file diff --git a/forum.nim b/forum.nim index 7414144..3222112 100644 --- a/forum.nim +++ b/forum.nim @@ -8,8 +8,8 @@ import os, strutils, times, md5, strtabs, cgi, math, db_sqlite, - captchas, scgi, jester, asyncdispatch, asyncnet, cache, sequtils, - parseutils, utils, random, rst, ranks + scgi, jester, asyncdispatch, asyncnet, cache, sequtils, + parseutils, utils, random, rst, ranks, recaptcha when not defined(windows): import bcrypt # TODO @@ -77,6 +77,8 @@ var db: DbConn isFTSAvailable: bool config: Config + useCaptcha: bool + captcha: ReCaptcha proc init(c: var TForumData) = c.userPass = "" @@ -314,8 +316,16 @@ proc register(c: var TForumData, name, pass, antibot, return setError(c, "new_password", "Invalid password!") # captcha validation: - if not isCaptchaCorrect(c, antibot): - return setError(c, "antibot", "Answer to captcha incorrect!") + if useCaptcha: + var captchaValid: bool = false + try: + captchaValid = waitFor captcha.verify(antibot) + except: + echo("[ERROR] Error checking captcha: " & getCurrentExceptionMsg()) + captchaValid = false + + if not captchaValid: + return setError(c, "antibot", "Answer to captcha incorrect!") # email validation if not ('@' in email and '.' in email): @@ -1123,7 +1133,7 @@ routes: post "/doregister": createTFD() - if c.register(@"name", @"new_password", @"antibot", @"email"): + if c.register(@"name", @"new_password", @"g-recaptcha-response", @"email"): resp genMain(c, "You are now registered. You must now confirm your" & " email address by clicking the link sent to " & @"email", "Registration successful - Nim Forum") @@ -1353,6 +1363,11 @@ when isMainModule: isFTSAvailable = db.getAllRows(sql("SELECT name FROM sqlite_master WHERE " & "type='table' AND name='post_fts'")).len == 1 config = loadConfig() + if len(config.recaptchaSecretKey) > 0 and len(config.recaptchaSiteKey) > 0: + useCaptcha = true + captcha = initReCaptcha(config.recaptchaSecretKey, config.recaptchaSiteKey) + else: + useCaptcha = false var http = true if paramCount() > 0: if paramStr(1) == "scgi": diff --git a/forum.nim.cfg b/forum.nim.cfg index 429dc5b..3af1ac0 100644 --- a/forum.nim.cfg +++ b/forum.nim.cfg @@ -2,3 +2,5 @@ # we need the documentation generator of the compiler: path="$lib/packages/docutils" path="$nim" + +-d:ssl \ No newline at end of file diff --git a/nimforum.nimble b/nimforum.nimble index 3591767..b0cdb09 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -8,4 +8,4 @@ license = "MIT" bin = "forum" [Deps] -Requires: "nim >= 0.14.0, cairo#head, jester#head, bcrypt#head" +Requires: "nim >= 0.14.0, cairo#head, jester#head, bcrypt#head, recaptcha >= 1.0.0" diff --git a/public/captchas/.gitignore b/public/captchas/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/public/captchas/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/utils.nim b/utils.nim index 002ee94..ce48804 100644 --- a/utils.nim +++ b/utils.nim @@ -22,6 +22,8 @@ type smtpUser: string smtpPassword: string mlistAddress: string + recaptchaSecretKey*: string + recaptchaSiteKey*: string var docConfig: StringTableRef @@ -38,6 +40,8 @@ proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config = result.smtpUser = root{"smtpUser"}.getStr("") result.smtpPassword = root{"smtpPassword"}.getStr("") result.mlistAddress = root{"mlistAddress"}.getStr("") + result.recaptchaSecretKey = root{"recaptchaSecretKey"}.getStr("") + result.recaptchaSiteKey = root{"recaptchaSiteKey"}.getStr("") except: echo("[WARNING] Couldn't read config file: ", filename) From b780b970f4738ea631dc53231c04a6f54918155c Mon Sep 17 00:00:00 2001 From: Euan Torano Date: Tue, 7 Mar 2017 19:09:46 +0000 Subject: [PATCH 061/451] Use captcha for reset password. Signed-off-by: Euan Torano --- forms.tmpl | 6 ++++-- forum.nim | 46 ++++++++++++++++++---------------------------- 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index 9f6d2a7..010759a 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -423,10 +423,12 @@ ${fieldValid(c, "nick", "Your nickname:")} + #if useCaptcha: - ${fieldValid(c, "antibot", "What is " & antibot(c) & "?")} - ${textWidget(c, "antibot", "", maxlength=4)} + ${fieldValid(c, "g-recaptcha-response", "Captcha:")} + ${captcha.render(includeNoScript=true)} + #end if #if c.errorMsg != "":
diff --git a/forum.nim b/forum.nim index 3222112..cfa6a67 100644 --- a/forum.nim +++ b/forum.nim @@ -276,19 +276,6 @@ proc validThreadId(c: TForumData): bool = result = getValue(db, sql"select id from thread where id = ?", $c.threadId).len > 0 -proc antibot(c: var TForumData): string = - let a = random(10)+1 - let b = random(1000)+1 - let answer = $(a+b) - - exec(db, sql"delete from antibot where ip = ?", c.req.ip) - let captchaId = tryInsertID(db, - sql"insert into antibot(ip, answer) values (?, ?)", c.req.ip, - answer).int mod 10_000 - let captchaFile = getCaptchaFilename(captchaId) - createCaptcha(captchaFile, $a & "+" & $b) - result = """""" % c.req.getCaptchaUrl(captchaId) - const SecureChars = {'A'..'Z', 'a'..'z', '0'..'9', '_', '\128'..'\255'} @@ -297,13 +284,7 @@ proc setError(c: var TForumData, field, msg: string): bool {.inline.} = c.errorMsg = "Error: " & msg return false -proc isCaptchaCorrect(c: var TForumData, antibot: string): bool = - ## Determines whether the user typed in the captcha correctly. - let correctRes = getValue(db, - sql"select answer from antibot where ip = ?", c.req.ip) - return antibot == correctRes - -proc register(c: var TForumData, name, pass, antibot, +proc register(c: var TForumData, name, pass, antibot, userIp, email: string): bool = # Username validation: if name.len == 0 or not allCharsInSet(name, SecureChars): @@ -319,13 +300,13 @@ proc register(c: var TForumData, name, pass, antibot, if useCaptcha: var captchaValid: bool = false try: - captchaValid = waitFor captcha.verify(antibot) + captchaValid = waitFor captcha.verify(antibot, userIp) except: echo("[ERROR] Error checking captcha: " & getCurrentExceptionMsg()) captchaValid = false if not captchaValid: - return setError(c, "antibot", "Answer to captcha incorrect!") + return setError(c, "g-recaptcha-response", "Answer to captcha incorrect!") # email validation if not ('@' in email and '.' in email): @@ -360,10 +341,19 @@ proc register(c: var TForumData, name, pass, antibot, return true -proc resetPassword(c: var TForumData, nick, antibot: string): bool = - # Validate captcha - if not isCaptchaCorrect(c, antibot): - return setError(c, "antibot", "Answer to captcha incorrect!") +proc resetPassword(c: var TForumData, nick, antibot, userIp: string): bool = + # captcha validation: + if useCaptcha: + var captchaValid: bool = false + try: + captchaValid = waitFor captcha.verify(antibot, userIp) + except: + echo("[ERROR] Error checking captcha: " & getCurrentExceptionMsg()) + captchaValid = false + + if not captchaValid: + return setError(c, "g-recaptcha-response", "Answer to captcha incorrect!") + # Gather some extra information to determine ident hash. let epoch = $int(epochTime()) let row = db.getRow( @@ -1133,7 +1123,7 @@ routes: post "/doregister": createTFD() - if c.register(@"name", @"new_password", @"g-recaptcha-response", @"email"): + if c.register(@"name", @"new_password", @"g-recaptcha-response", request.host, @"email"): resp genMain(c, "You are now registered. You must now confirm your" & " email address by clicking the link sent to " & @"email", "Registration successful - Nim Forum") @@ -1302,7 +1292,7 @@ routes: echo(request.params) cond(@"nick" != "") - if resetPassword(c, @"nick", @"antibot"): + if resetPassword(c, @"nick", @"g-recaptcha-response", request.host): resp genMain(c, "Email sent!", "Reset Password - Nim Forum") else: resp genMain(c, genFormResetPassword(c), "Reset Password - Nim Forum") From e6c4c85bb1d1a461a63b6e9c6cb3f6a5b92b7585 Mon Sep 17 00:00:00 2001 From: Euan Torano Date: Sun, 19 Mar 2017 18:43:28 +0000 Subject: [PATCH 062/451] Making TForumData a ref object Signed-off-by: Euan Torano --- forms.tmpl | 20 +++++++------- forum.nim | 81 +++++++++++++++++++++++++++--------------------------- main.tmpl | 8 +++--- 3 files changed, 55 insertions(+), 54 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index 010759a..a7b7006 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -5,7 +5,7 @@ #end template # # -#proc genThreadsList(c: var TForumData, count: var int): string = +#proc genThreadsList(c: TForumData, count: var int): string = # const queryModAdmin = sql"""select id, name, views, modified from thread # where id in (select thread from post where author in # (select id from person where status not in ('Spammer') or id = ?)) @@ -92,7 +92,7 @@ #end proc # # -#proc genPostPreview(c: var TForumData, +#proc genPostPreview(c: TForumData, # title, content, author, date: string): string = # result = "" @@ -119,7 +119,7 @@ #end proc # # -#proc genPostsList(c: var TForumData, threadId: string, count: var int): string = +#proc genPostsList(c: TForumData, threadId: string, count: var int): string = # let query = sql("""select p.id, u.name, p.header, p.content, p.creation, p.author, u.email from post p, # person u # where u.id = p.author and p.thread = ? and $# @@ -180,7 +180,7 @@ # #proc genMarkHelp(): string #end proc -#proc genFormPost(c: var TForumData, action: string, +#proc genFormPost(c: TForumData, action: string, # topText, title, content: string, isEdit: bool): string = # result = ""
@@ -225,7 +225,7 @@ #end proc # # -#proc genFormRegister(c: var TForumData): string = +#proc genFormRegister(c: TForumData): string = # result = ""
@@ -265,7 +265,7 @@ #end proc # -#proc genFormSetRank(c: var TForumData; ui: TUserInfo): string = +#proc genFormSetRank(c: TForumData; ui: TUserInfo): string = # result = ""
@@ -288,7 +288,7 @@ #end proc # -#proc genFormLogin(c: var TForumData): string = +#proc genFormLogin(c: TForumData): string = # result = "" # if not c.loggedIn: @@ -307,7 +307,7 @@ #end proc # # -#proc genListOnline(c: var TForumData, stats: TForumStats): string = +#proc genListOnline(c: TForumData, stats: TForumStats): string = # result = "" # var active: seq[string] = @[] # for i in stats.activeUsers: @@ -324,7 +324,7 @@ # # # -#proc genSearchResults(c: var TForumData, +#proc genSearchResults(c: TForumData, # results: iterator: db_sqlite.Row {.closure, tags: [ReadDbEffect].}, # count: var int): string = # const threadId = 0 @@ -407,7 +407,7 @@ #end proc # # -#proc genFormResetPassword(c: var TForumData): string = +#proc genFormResetPassword(c: TForumData): string = # result = ""
diff --git a/forum.nim b/forum.nim index cfa6a67..7165819 100644 --- a/forum.nim +++ b/forum.nim @@ -37,7 +37,7 @@ type TPost = tuple[subject, content: string] - TForumData = object of TSession + TForumData = ref object of TSession req: Request userid: string actionContent: string @@ -80,7 +80,7 @@ var useCaptcha: bool captcha: ReCaptcha -proc init(c: var TForumData) = +proc init(c: TForumData) = c.userPass = "" c.userName = "" c.threadId = unselectedThread @@ -143,16 +143,16 @@ proc genThreadUrl(c: TForumData, postId = "", action = "", threadid = "", pageNu result.add("#" & postId) result = c.req.makeUri(result, absolute = false) -proc formSession(c: var TForumData, nextAction: string): string = +proc formSession(c: TForumData, nextAction: string): string = return """ """ % [ $c.threadId, $c.postid] -proc urlButton(c: var TForumData, text, url: string): string = +proc urlButton(c: TForumData, text, url: string): string = return ("""$2""") % [ url, text] -proc genButtons(c: var TForumData, btns: seq[TStyledButton]): string = +proc genButtons(c: TForumData, btns: seq[TStyledButton]): string = if btns.len == 1: var anchor = "" @@ -279,13 +279,13 @@ proc validThreadId(c: TForumData): bool = const SecureChars = {'A'..'Z', 'a'..'z', '0'..'9', '_', '\128'..'\255'} -proc setError(c: var TForumData, field, msg: string): bool {.inline.} = +proc setError(c: TForumData, field, msg: string): bool {.inline.} = c.invalidField = field c.errorMsg = "Error: " & msg return false -proc register(c: var TForumData, name, pass, antibot, userIp, - email: string): bool = +proc register(c: TForumData, name, pass, antibot, userIp, + email: string): Future[bool] {.async.} = # Username validation: if name.len == 0 or not allCharsInSet(name, SecureChars): return setError(c, "name", "Invalid username!") @@ -300,7 +300,7 @@ proc register(c: var TForumData, name, pass, antibot, userIp, if useCaptcha: var captchaValid: bool = false try: - captchaValid = waitFor captcha.verify(antibot, userIp) + captchaValid = await captcha.verify(antibot, userIp) except: echo("[ERROR] Error checking captcha: " & getCurrentExceptionMsg()) captchaValid = false @@ -341,12 +341,12 @@ proc register(c: var TForumData, name, pass, antibot, userIp, return true -proc resetPassword(c: var TForumData, nick, antibot, userIp: string): bool = +proc resetPassword(c: TForumData, nick, antibot, userIp: string): Future[bool] {.async.} = # captcha validation: if useCaptcha: var captchaValid: bool = false try: - captchaValid = waitFor captcha.verify(antibot, userIp) + captchaValid = await captcha.verify(antibot, userIp) except: echo("[ERROR] Error checking captcha: " & getCurrentExceptionMsg()) captchaValid = false @@ -378,7 +378,7 @@ proc resetPassword(c: var TForumData, nick, antibot, userIp: string): bool = return true -proc logout(c: var TForumData) = +proc logout(c: TForumData) = const query = sql"delete from session where ip = ? and password = ?" c.username = "" c.userpass = "" @@ -395,7 +395,7 @@ proc getBanErrorMsg(banValue: string; rank: Rank): string = of Moderated, User, Moderator, Admin: return "" -proc checkLoggedIn(c: var TForumData) = +proc checkLoggedIn(c: TForumData) = if not c.req.cookies.hasKey("sid"): return let pass = c.req.cookies["sid"] if execAffectedRows(db, @@ -425,7 +425,7 @@ proc checkLoggedIn(c: var TForumData) = else: echo("SID not found in sessions. Assuming logged out.") -proc incrementViews(c: var TForumData) = +proc incrementViews(c: TForumData) = const query = sql"update thread set views = views + 1 where id = ?" exec(db, query, $c.threadId) @@ -435,7 +435,7 @@ proc isPreview(c: TForumData): bool = proc isDelete(c: TForumData): bool = result = c.req.params.hasKey("delete") -proc validateRst(c: var TForumData, content: string): bool = +proc validateRst(c: TForumData, content: string): bool = result = true try: discard rstToHtml(content) @@ -513,7 +513,7 @@ template writeToDb(c, cr, setPostId: untyped) = if setPostId: c.postId = retID.int -proc updateThreads(c: var TForumData): int = +proc updateThreads(c: TForumData): int = ## Removes threads if they have no posts, or changes their modified field ## if they still contain posts. const query = @@ -531,7 +531,7 @@ proc updateThreads(c: var TForumData): int = result = -1 discard setError(c, "", "database error") -proc edit(c: var TForumData, postId: int): bool = +proc edit(c: TForumData, postId: int): bool = checkLogin(c) if c.isPreview: retrPost(c) @@ -564,8 +564,8 @@ proc edit(c: var TForumData, postId: int): bool = exec(db, crud(crUpdate, "thread", "name"), subject, $c.threadId) result = true -proc gatherUserInfo(c: var TForumData, nick: string, ui: var TUserInfo): bool -proc spamCheck(c: var TForumData, subject, content: string): bool = +proc gatherUserInfo(c: TForumData, nick: string, ui: var TUserInfo): bool +proc spamCheck(c: TForumData, subject, content: string): bool = # Check current user's info var ui: TUserInfo if gatherUserInfo(c, c.userName, ui): @@ -595,7 +595,7 @@ proc spamCheck(c: var TForumData, subject, content: string): bool = word in contentAlphabet.toLowerAscii(): return true -proc rateLimitCheck(c: var TForumData): bool = +proc rateLimitCheck(c: TForumData): bool = const query40 = sql("SELECT count(*) FROM post where author = ? and " & "(strftime('%s', 'now') - strftime('%s', creation)) < 40") @@ -614,7 +614,7 @@ proc rateLimitCheck(c: var TForumData): bool = if last300s > 6: return true return false -proc makeThreadURL(c: var TForumData): string = +proc makeThreadURL(c: TForumData): string = c.req.makeUri("/t/" & $c.threadId) template postChecks() {.dirty.} = @@ -624,7 +624,7 @@ template postChecks() {.dirty.} = if rateLimitCheck(c): return setError(c, "subject", "You're posting too fast.") -proc reply(c: var TForumData): bool = +proc reply(c: TForumData): bool = # reply to an existing thread checkLogin(c) retrPost(c) @@ -642,7 +642,7 @@ proc reply(c: var TForumData): bool = threadUrl=c.makeThreadURL()) result = true -proc newThread(c: var TForumData): bool = +proc newThread(c: TForumData): bool = # create new conversation thread (permanent or transient) const query = sql"insert into thread(name, views, modified) values (?, 0, DATETIME('now'))" checkLogin(c) @@ -665,7 +665,7 @@ proc newThread(c: var TForumData): bool = threadUrl=c.makeThreadURL()) result = true -proc login(c: var TForumData, name, pass: string): bool = +proc login(c: TForumData, name, pass: string): bool = # get form data: const query = sql"select id, name, password, email, salt, status, ban from person where name = ?" @@ -693,7 +693,7 @@ proc login(c: var TForumData, name, pass: string): bool = else: return c.setError("password", "Login failed!") -proc verifyIdentHash(c: var TForumData, name, epoch, ident: string): bool = +proc verifyIdentHash(c: TForumData, name, epoch, ident: string): bool = const query = sql"select password, salt, strftime('%s', lastOnline) from person where name = ?" var row = getRow(db, query, name) @@ -704,13 +704,13 @@ proc verifyIdentHash(c: var TForumData, name, epoch, ident: string): bool = if row[2].parseInt > (epoch.parseInt + 60): return false result = newIdent == ident -proc deleteAll(c: var TForumData, nick: string): bool = +proc deleteAll(c: TForumData, nick: string): bool = const query = sql("delete from post where author = (select id from person where name = ?)") result = tryExec(db, query, nick) result = result and updateThreads(c) >= 0 -proc setStatus(c: var TForumData, nick: string, status: Rank; +proc setStatus(c: TForumData, nick: string, status: Rank; reason: string): bool = const query = sql("update person set status = ?, ban = ? where name = ?") @@ -722,13 +722,13 @@ proc setStatus(c: var TForumData, nick: string, status: Rank; if status == Spammer and result: result = deleteAll(c, nick) -proc setPassword(c: var TForumData, nick, pass: string): bool = +proc setPassword(c: TForumData, nick, pass: string): bool = const query = sql("update person set password = ?, salt = ? where name = ?") var salt = makeSalt() result = tryExec(db, query, makePassword(pass, salt), salt, nick) -proc hasReplyBtn(c: var TForumData): bool = +proc hasReplyBtn(c: TForumData): bool = result = c.req.pathInfo != "/donewthread" and c.req.pathInfo != "/doreply" result = result and c.req.params.getOrDefault("action") notin ["reply", "edit"] # If the user is not logged in and there are no page numbers then we shouldn't @@ -737,7 +737,7 @@ proc hasReplyBtn(c: var TForumData): bool = result = result and (pages > 1 or c.loggedIn) return c.threadId >= 0 and result -proc getStats(c: var TForumData, simple: bool): TForumStats = +proc getStats(c: TForumData, simple: bool): TForumStats = const totalUsersQuery = sql"select count(*) from person" result.totalUsers = getValue(db, totalUsersQuery).parseInt @@ -762,7 +762,7 @@ proc getStats(c: var TForumData, simple: bool): TForumStats = result.newestMember = (row[1], row[0].parseInt) newestMemberCreation = row[3].parseInt -proc genPagenumNav(c: var TForumData, stats: TForumStats): string = +proc genPagenumNav(c: TForumData, stats: TForumStats): string = result = "" var firstUrl = "" @@ -835,22 +835,22 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string = result.add(nextTag) result.add(lastTag) -proc gatherTotalPostsByID(c: var TForumData, thrid: int): int = +proc gatherTotalPostsByID(c: TForumData, thrid: int): int = ## Gets the total post count of a thread. result = getValue(db, sql"select count(*) from post where thread = ?", $thrid).parseInt -proc gatherTotalPosts(c: var TForumData) = +proc gatherTotalPosts(c: TForumData) = if c.totalPosts > 0: return # Gather some data. const totalPostsQuery = sql"select count(*) from post p, person u where u.id = p.author and p.thread = ?" c.totalPosts = getValue(db, totalPostsQuery, $c.threadId).parseInt -proc getPagesInThread(c: var TForumData): int = +proc getPagesInThread(c: TForumData): int = c.gatherTotalPosts() # Get total post count result = ceil(c.totalPosts / PostsPerPage).int-1 -proc getPagesInThreadByID(c: var TForumData, thrid: int): int = +proc getPagesInThreadByID(c: TForumData, thrid: int): int = result = ceil(c.gatherTotalPostsByID(thrid) / PostsPerPage).int proc getThreadTitle(thrid: int, pageNum: int): string = @@ -858,7 +858,7 @@ proc getThreadTitle(thrid: int, pageNum: int): string = if pageNum notin {0,1}: result.add(" - Page " & $pageNum) -proc genPagenumLocalNav(c: var TForumData, thrid: int): string = +proc genPagenumLocalNav(c: TForumData, thrid: int): string = result = "" const maxPostPages = 6 # Maximum links to pages shown. const hmpp = maxPostPages div 2 @@ -878,7 +878,7 @@ proc genPagenumLocalNav(c: var TForumData, thrid: int): string = result = htmlgen.span(class = "pages", result) -proc gatherUserInfo(c: var TForumData, nick: string, ui: var TUserInfo): bool = +proc gatherUserInfo(c: TForumData, nick: string, ui: var TUserInfo): bool = ui.nick = nick const getUIDQuery = sql"select id from person where name = ?" var uid = getValue(db, getUIDQuery, nick) @@ -908,7 +908,7 @@ proc gatherUserInfo(c: var TForumData, nick: string, ui: var TUserInfo): bool = include "forms.tmpl" include "main.tmpl" -proc genProfile(c: var TForumData, ui: TUserInfo): string = +proc genProfile(c: TForumData, ui: TUserInfo): string = result = "" result.add(htmlgen.`div`(id = "talk-head", @@ -982,6 +982,7 @@ proc prependRe(s: string): string = template createTFD() = var c {.inject.}: TForumData + new(c) init(c) c.req = request c.startTime = epochTime() @@ -1123,7 +1124,7 @@ routes: post "/doregister": createTFD() - if c.register(@"name", @"new_password", @"g-recaptcha-response", request.host, @"email"): + if await c.register(@"name", @"new_password", @"g-recaptcha-response", request.host, @"email"): resp genMain(c, "You are now registered. You must now confirm your" & " email address by clicking the link sent to " & @"email", "Registration successful - Nim Forum") @@ -1292,7 +1293,7 @@ routes: echo(request.params) cond(@"nick" != "") - if resetPassword(c, @"nick", @"g-recaptcha-response", request.host): + if await resetPassword(c, @"nick", @"g-recaptcha-response", request.host): resp genMain(c, "Email sent!", "Reset Password - Nim Forum") else: resp genMain(c, genFormResetPassword(c), "Reset Password - Nim Forum") diff --git a/main.tmpl b/main.tmpl index 3590b8d..f52d869 100644 --- a/main.tmpl +++ b/main.tmpl @@ -1,5 +1,5 @@ #? stdtmpl | standard -#proc genMain(c: var TForumData, content: string, title = "Nim Forum", +#proc genMain(c: TForumData, content: string, title = "Nim Forum", # additional_headers = "", showRssLinks = false): string = # result = "" # var stats: TForumStats @@ -170,7 +170,7 @@ #end proc # -#proc genRSSHeaders(c: var TForumData): string = +#proc genRSSHeaders(c: TForumData): string = # result = "" @@ -178,7 +178,7 @@ type="application/atom+xml" rel="alternate"> #end proc # -#proc genThreadsRSS(c: var TForumData): string = +#proc genThreadsRSS(c: TForumData): string = # result = "" # const query = sql"""SELECT A.id, A.name, # strftime('%Y-%m-%dT%H:%M:%SZ', (A.modified)), @@ -226,7 +226,7 @@ ${xmlEncode(rstToHtml(%postContent))} #end proc # -#proc genPostsRSS(c: var TForumData): string = +#proc genPostsRSS(c: TForumData): string = # result = "" # const query = sql"""SELECT A.id, B.name, A.content, A.thread, # A.header, strftime('%Y-%m-%dT%H:%M:%SZ', A.creation), From 57f5a81e48e467939450319d1c88db3227eb6861 Mon Sep 17 00:00:00 2001 From: Silvio Date: Wed, 19 Apr 2017 15:03:33 +0200 Subject: [PATCH 063/451] Fix compile: Move addr, port to connect in smtp Following https://github.com/nim-lang/Nim/commit/fecad72e02256c947e1c16cd003ceca62a3633e5 , moved address and port to `connect` from `newAsyncSmtp` , forum compiles again. --- utils.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils.nim b/utils.nim index 002ee94..c052bd5 100644 --- a/utils.nim +++ b/utils.nim @@ -110,8 +110,8 @@ proc sendMail(config: Config, subject, message, recipient: string, from_addr = " echo("[WARNING] Cannot send mail: no smtp server configured (smtpAddress).") return - var client = newAsyncSmtp(config.smtpAddress, Port(config.smtpPort)) - await client.connect() + var client = newAsyncSmtp() + await client.connect(config.smtpAddress, Port(config.smtpPort)) if config.smtpUser.len > 0: await client.auth(config.smtpUser, config.smtpPassword) From 957b3738f83e54c2ad1b167c425d47610e983f4f Mon Sep 17 00:00:00 2001 From: stisa Date: Sat, 7 Oct 2017 16:54:13 +0200 Subject: [PATCH 064/451] remove cairo dependency and update the readme --- README.md | 25 +++++-------------------- nimforum.nimble | 2 +- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 1df440e..1ec4b8c 100644 --- a/README.md +++ b/README.md @@ -15,23 +15,8 @@ _See also: Running the forum for how to create the database_ ## Dependencies The code depends on the RST parser of the Nim -compiler and on Jester. The code generating captchas for registration uses the -[cairo module](https://github.com/nim-lang/cairo), which requires you to have -the [cairo library](http://cairographics.org) installed when you run the forum, -or you will be greeted by a cryptic error message similar to: - - $ ./forum could not load: libcairo.so(1.2) - -### Mac OS X - -#### cairo -If you are using macosx and have installed the ``cairo`` library through -[MacPorts](https://www.macports.org) you still need to add the library path to -your ``LD_LIBRARY_PATH`` environment variable. Example: - - $ LD_LIBRARY_PATH=/opt/local/lib/ ./forum - -Replace ``/opt/local/lib`` with the correct path on your system. +compiler and on Jester. The captchas for registration uses the +[reCaptcha module](https://github.com/euantorano/recaptcha.nim). #### bcrypt @@ -47,7 +32,7 @@ changing the dependencies slightly. ``` [Deps] -Requires: "nimrod >= 0.10.3, cairo#head, jester#head, bcrypt >= 0.2.1" +Requires: "nim >= 0.14.0, jester#head, bcrypt#head, recaptcha >= 1.0.0" ``` # Running the forum @@ -69,12 +54,12 @@ After that you can just run `forum` and if everything is ok you will get the inf _There is an update helper `editdb` which you can safely ignore for now._ -_The files `captchas.nim`, `cache.nim` are included by `forum.nim` and do +_The file `cache.nim` is included by `forum.nim` and do not need to be compiled by you._ # Copyright -Copyright (c) 2012-2015 Andreas Rumpf, Dominik Picheta. +Copyright (c) 2012-2017 Andreas Rumpf, Dominik Picheta. All rights reserved. diff --git a/nimforum.nimble b/nimforum.nimble index b0cdb09..b6943e0 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -8,4 +8,4 @@ license = "MIT" bin = "forum" [Deps] -Requires: "nim >= 0.14.0, cairo#head, jester#head, bcrypt#head, recaptcha >= 1.0.0" +Requires: "nim >= 0.14.0, jester#head, bcrypt#head, recaptcha >= 1.0.0" From 13f2de94a3bfd8f640ad460620b17237d1c4401e Mon Sep 17 00:00:00 2001 From: stisa Date: Sat, 7 Oct 2017 17:11:50 +0200 Subject: [PATCH 065/451] macos bcrypt may need a fixed version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ec4b8c..1e3205c 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ changing the dependencies slightly. ``` [Deps] -Requires: "nim >= 0.14.0, jester#head, bcrypt#head, recaptcha >= 1.0.0" +Requires: "nim >= 0.14.0, jester#head, bcrypt >= 0.2.1, recaptcha >= 1.0.0" ``` # Running the forum From 8bbe9356febfef1a5816f06d60bc9056eb0bf0a9 Mon Sep 17 00:00:00 2001 From: stisa Date: Sat, 7 Oct 2017 17:17:08 +0200 Subject: [PATCH 066/451] close a paren, fix a link --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1e3205c..b0ee351 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,18 @@ This is Nim's forum. Available at http://forum.nim-lang.org. ## Building -You can use ``nimble`` (available [here](https://github.com/nim-lang/nimble) +You can use ``nimble`` (available [here](https://github.com/nim-lang/nimble)) to get all the necessary [dependencies](https://github.com/nim-lang/nimforum/blob/master/nimforum.nimble#L11). Clone this repo and execute ``nimble build`` in this repositories directory. -_See also: Running the forum for how to create the database_ +See also: Running the forum for how to create the database. ## Dependencies -The code depends on the RST parser of the Nim -compiler and on Jester. The captchas for registration uses the -[reCaptcha module](https://github.com/euantorano/recaptcha.nim). +The code depends on the RST parser of the Nim compiler and on Jester. +The captchas for registration uses the [reCaptcha module](https://github.com/euantorano/recaptcha.nim). #### bcrypt From d441f9445d48781c598bba990073330ae94f8c23 Mon Sep 17 00:00:00 2001 From: stisa Date: Sat, 7 Oct 2017 17:32:34 +0200 Subject: [PATCH 067/451] repurpose rst.txt as rst help --- rst.txt => static/rst.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) rename rst.txt => static/rst.rst (92%) diff --git a/rst.txt b/static/rst.rst similarity index 92% rename from rst.txt rename to static/rst.rst index 70bbd63..f8dc48a 100644 --- a/rst.txt +++ b/static/rst.rst @@ -3,7 +3,7 @@ =========================================================================== This is a cheat sheet for the *reStructuredText* dialect as implemented by -Nimrod's documentation generator which has been reused for this forum. :-) +Nim's documentation generator which has been reused for this forum. See also the `official RST cheat sheet `_ @@ -32,14 +32,14 @@ Plain text Result Links ===== -Links are either direct URLs like ``http://nimrod-lang.org`` or written like +Links are either direct URLs like ``http://nim-lang.org`` or written like this:: - `Nimrod `_ + `Nim `_ Or like this:: - ``_ + ``_ Code blocks @@ -47,7 +47,7 @@ Code blocks are done this way:: - .. code-block:: nimrod + .. code-block:: nim if x == "abc": echo "xyz" @@ -55,25 +55,25 @@ are done this way:: Is rendered as: -.. code-block:: nimrod +.. code-block:: nim if x == "abc": echo "xyz" -Except Nimrod, the programming languages C, C++, Java and C# have highlighting +Except Nim, the programming languages C, C++, Java and C# have highlighting support. An alternative github-like syntax is also supported. This has the advantage that no excessive indentation is needed:: - ```nimrod + ```nim if x == "abc": echo "xyz"``` Is rendered as: -.. code-block:: nimrod +.. code-block:: nim if x == "abc": echo "xyz" From 6c72d077e378a8d6c71976c61f9b4d5ff659ec4e Mon Sep 17 00:00:00 2001 From: stisa Date: Sat, 7 Oct 2017 17:33:21 +0200 Subject: [PATCH 068/451] link to rst help - fixes #102 --- forms.tmpl | 2 +- forum.nim | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/forms.tmpl b/forms.tmpl index a7b7006..b28f637 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -443,7 +443,7 @@

nimforum uses a slightly-customized version of reStructuredText for formatting. See below for some basics, or check - this link for a more detailed help reference.

+ this link for a more detailed help reference.

diff --git a/forum.nim b/forum.nim index 7165819..11c098d 100644 --- a/forum.nim +++ b/forum.nim @@ -1346,6 +1346,8 @@ routes: resp genMain(c, page) get "/search-help": textPage "static/search-help" + get "/rst": + textPage "static/rst" when isMainModule: randomize() From 25d4303c6990761f4186eb965d525a56de83eb04 Mon Sep 17 00:00:00 2001 From: stisa Date: Sat, 7 Oct 2017 22:15:23 +0200 Subject: [PATCH 069/451] style pre blocks --- public/css/style.css | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/public/css/style.css b/public/css/style.css index 5a731e7..7c35f66 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -9,7 +9,19 @@ body { font: 13pt Helvetica,Arial,sans-serif; background:#152534 url("/images/bg.png") no-repeat fixed center top; } -pre { color: #F5F5F5;} +pre { + color: #F5F5F5; + overflow:auto; + margin:0; + padding:15px 10px; + font-size:10pt; + font-style:normal; + line-height:14pt; + background:rgba(0,0,0,.75); + border-left:8px solid rgba(0,0,0,.3); + margin-bottom: 10pt; + font-family: "DejaVu Sans Mono", monospace; +} pre, pre * { cursor:text; } pre .Comment { color:#6D6D6D; font-style:italic; } pre .Keyword { color:#43A8CF; font-weight:bold; } From bd730df6c4c032007e9df9a8d7745219294c412d Mon Sep 17 00:00:00 2001 From: stisa Date: Sat, 7 Oct 2017 22:18:38 +0200 Subject: [PATCH 070/451] use relative path for rst help --- forms.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forms.tmpl b/forms.tmpl index b28f637..2872e90 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -443,7 +443,7 @@

nimforum uses a slightly-customized version of reStructuredText for formatting. See below for some basics, or check - this link for a more detailed help reference.

+ this link for a more detailed help reference.

From afe9b41249d9c62dceac82295d0753e90a7be0c1 Mon Sep 17 00:00:00 2001 From: stisa Date: Sat, 7 Oct 2017 22:30:50 +0200 Subject: [PATCH 071/451] proper h1/h2, add images --- static/rst.rst | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/static/rst.rst b/static/rst.rst index f8dc48a..6b584fc 100644 --- a/static/rst.rst +++ b/static/rst.rst @@ -1,5 +1,4 @@ -=========================================================================== - reStructuredText cheat sheet +reStructuredText cheat sheet =========================================================================== This is a cheat sheet for the *reStructuredText* dialect as implemented by @@ -14,7 +13,7 @@ Elements of **markdown** are also supported. Inline elements -=============== +--------------- Ordinary text may contain *inline elements*: @@ -30,20 +29,20 @@ Plain text Result =============================== ============================================ Links -===== +----- -Links are either direct URLs like ``http://nim-lang.org`` or written like +Links are either direct URLs like ``https://nim-lang.org`` or written like this:: - `Nim `_ + `Nim `_ Or like this:: - ``_ + ``_ Code blocks -=========== +----------- are done this way:: @@ -81,7 +80,7 @@ Is rendered as: Literal blocks -============== +-------------- Are introduced by '::' and a newline. The block is indicated by indentation: @@ -98,7 +97,7 @@ Is rendered as:: Bullet lists -============ +------------ look like this:: @@ -123,7 +122,7 @@ Is rendered as: Enumerated lists -================ +---------------- are written like this:: @@ -143,7 +142,7 @@ Is rendered as: Quoting someone -=============== +--------------- quotes are just:: @@ -160,7 +159,7 @@ Is rendered as: Definition lists -================ +---------------- are written like this:: @@ -190,7 +189,7 @@ how Tables -====== +------ Only *simple tables* are supported. They are of the form:: @@ -218,3 +217,10 @@ Cell 4 Cell 5; any Cell 6 multiple lines Cell 7 Cell 8 Cell 9 ================== =============== =================== + +Images +------ + +``` +.. image:: path/to/img.png +``` \ No newline at end of file From 2cc8eb926907ea6ba4c71cbdd5e445e628a5a258 Mon Sep 17 00:00:00 2001 From: Daniil Yarancev <21169548+Yardanico@users.noreply.github.com> Date: Tue, 17 Oct 2017 15:10:15 +0300 Subject: [PATCH 072/451] Fix link for the Araq's blog Also make a new github link for nim :) --- main.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.tmpl b/main.tmpl index f52d869..70c5527 100644 --- a/main.tmpl +++ b/main.tmpl @@ -139,7 +139,7 @@

Community

@@ -151,7 +151,7 @@
From 55d4790060a0de9434cc31de577f4e5ef61812ee Mon Sep 17 00:00:00 2001 From: Daniil Yarancev <21169548+Yardanico@users.noreply.github.com> Date: Tue, 17 Oct 2017 18:44:11 +0300 Subject: [PATCH 073/451] Fix link --- main.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.tmpl b/main.tmpl index 70c5527..f295433 100644 --- a/main.tmpl +++ b/main.tmpl @@ -151,7 +151,7 @@ From 50587abe9cfce0603b0e43e32258b30096f42590 Mon Sep 17 00:00:00 2001 From: stisa Date: Sun, 22 Oct 2017 17:08:50 +0200 Subject: [PATCH 074/451] tiny js script that runs listings in the playground --- forms.tmpl | 38 ++++++++++++++++++++++++++++++++++++++ public/css/style.css | 27 +++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/forms.tmpl b/forms.tmpl index a7b7006..bd4b239 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -175,6 +175,44 @@ # end for + #end proc # diff --git a/public/css/style.css b/public/css/style.css index 5a731e7..07e9567 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -711,3 +711,30 @@ blockquote p { color: rgb(109, 109, 109) !important; } + +.runDiv > hr { + border: 1px solid slategray; +} + +.runDiv > button{ + cursor: pointer; + background-color: lightslategray; + text-decoration: none; + float: right; + color: #FFF; + display: block; +} + +.runDiv > .resDiv { + width: 80%; + margin-left: 1em; + padding: 0.2em 1em 0.2em 1em; + display: inline-block; +} + +.successComp { + color: lightgreen; +} +.failedComp { + color: lightcoral; +} From 95280674e57b2944a873492395c5d2c98ffc06f6 Mon Sep 17 00:00:00 2001 From: stisa Date: Sun, 22 Oct 2017 17:24:06 +0200 Subject: [PATCH 075/451] avoid innerHtml --- forms.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index bd4b239..06e7268 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -195,9 +195,9 @@ var resDiv = document.createElement("DIV") resDiv.setAttribute("class", "resDiv successComp") if (res.log != "Compilation Failed\u000A"){ - resDiv.innerHTML = res.log + resDiv.textContent = res.log } else { - resDiv.innerHTML = res.compileLog + resDiv.textContent = res.compileLog resDiv.setAttribute("class","resDiv failedComp") } runDiv.appendChild(resDiv) From 1b0d39d706ebec16b430969d98cba2b49bab9fe7 Mon Sep 17 00:00:00 2001 From: stisa Date: Sun, 22 Oct 2017 22:01:30 +0200 Subject: [PATCH 076/451] overwrite result instead of appending multiple times --- forms.tmpl | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index 06e7268..39bd5e2 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -192,15 +192,19 @@ if (httpRequest.readyState!=httpRequest.DONE){return} if (httpRequest.status == 200){ var res = JSON.parse(httpRequest.responseText) - var resDiv = document.createElement("DIV") - resDiv.setAttribute("class", "resDiv successComp") + // this works because only 1 `element` is inside `element` + var resDiv = element.getElementsByClassName("resDiv")[0] + if (resDiv == null) { + resDiv = document.createElement("DIV") + runDiv.appendChild(resDiv) + } if (res.log != "Compilation Failed\u000A"){ resDiv.textContent = res.log + resDiv.setAttribute("class", "resDiv successComp") } else { resDiv.textContent = res.compileLog resDiv.setAttribute("class","resDiv failedComp") } - runDiv.appendChild(resDiv) } else { console.log("There was a problem with the request.") } From 31f30a2fde4c02362dc5a6432d0b85c4561dfcd8 Mon Sep 17 00:00:00 2001 From: stisa Date: Sun, 22 Oct 2017 22:05:00 +0200 Subject: [PATCH 077/451] use button 4; remove margin from result --- public/css/style.css | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index 07e9567..c79e7ee 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -721,13 +721,20 @@ blockquote p { background-color: lightslategray; text-decoration: none; float: right; - color: #FFF; + color: #e5e8ef; display: block; + padding: 0.3em 0.5em 0.3em 0.5em; + border: none; + border-bottom: 0.2em solid #b9b9b9 +} + +.runDiv > button:active{ + border: none; + border-top: 0.2em solid #b9b9b9 } .runDiv > .resDiv { width: 80%; - margin-left: 1em; padding: 0.2em 1em 0.2em 1em; display: inline-block; } From a956261ae8762acc80e48ea7afd2a1c1d38e64fc Mon Sep 17 00:00:00 2001 From: stisa Date: Mon, 23 Oct 2017 20:11:04 +0200 Subject: [PATCH 078/451] change btn style to match sidebar --- public/css/style.css | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index c79e7ee..141746b 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -441,7 +441,7 @@ div#sidebar .content } -div#sidebar .content .button +div#sidebar .content .button, .runDiv>button { background-color: rgba(0,0,0,0.2); text-decoration: none; @@ -713,24 +713,18 @@ blockquote p { } .runDiv > hr { - border: 1px solid slategray; + border: 1px solid #80828d; } -.runDiv > button{ +.runDiv > button { cursor: pointer; - background-color: lightslategray; - text-decoration: none; - float: right; - color: #e5e8ef; - display: block; - padding: 0.3em 0.5em 0.3em 0.5em; - border: none; - border-bottom: 0.2em solid #b9b9b9 + border: none; /* remove border from runDiv>button */ + border-bottom: 2px solid rgba(0,0,0,0.24); + background-color: #80828d; } -.runDiv > button:active{ - border: none; - border-top: 0.2em solid #b9b9b9 +.runDiv>button:hover { + border-bottom: 2px solid rgba(255, 255, 255, 0.5); } .runDiv > .resDiv { From 0121f8bd9d02ac3605553bad23595a32795176bc Mon Sep 17 00:00:00 2001 From: stisa Date: Tue, 24 Oct 2017 23:12:19 +0200 Subject: [PATCH 079/451] loading; open in playground; improve compat (ie9+) --- forms.tmpl | 83 ++++++++++++++++++++++++++++++-------------- public/css/style.css | 13 +++++-- 2 files changed, 66 insertions(+), 30 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index 3b33156..fcb2623 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -115,6 +115,7 @@ + #end proc # @@ -177,45 +178,73 @@ # end for #end proc diff --git a/public/css/style.css b/public/css/style.css index 3359cfc..028cf56 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -453,7 +453,7 @@ div#sidebar .content } -div#sidebar .content .button, .runDiv>button +div#sidebar .content .button, .runDiv>button, .runDiv>a { background-color: rgba(0,0,0,0.2); text-decoration: none; @@ -728,14 +728,21 @@ blockquote p { border: 1px solid #80828d; } -.runDiv > button { +.runDiv > a { + color: #FFF !important; + padding: 3.5pt; + margin-right: 3pt; +} + +.runDiv > button, .runDiv > a { cursor: pointer; border: none; /* remove border from runDiv>button */ border-bottom: 2px solid rgba(0,0,0,0.24); background-color: #80828d; } -.runDiv>button:hover { +.runDiv>button:hover, .runDiv > a:hover { + text-decoration: none !important; border-bottom: 2px solid rgba(255, 255, 255, 0.5); } From f47449ea5c135121e0b48364a426cf9c083a56e5 Mon Sep 17 00:00:00 2001 From: stisa Date: Mon, 30 Oct 2017 19:59:01 +0100 Subject: [PATCH 080/451] add post link to date; ref #113 --- forms.tmpl | 2 +- public/css/style.css | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/forms.tmpl b/forms.tmpl index 3b33156..3e8ccb4 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -170,7 +170,7 @@ #except EParseError: # c.errorMsg = getCurrentExceptionMsg() #end - ${xmlEncode(%postCreation)} + ${xmlEncode(%postCreation)} diff --git a/public/css/style.css b/public/css/style.css index 3359cfc..c43b92e 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -751,3 +751,9 @@ blockquote p { .failedComp { color: lightcoral; } +.date > a, .date > a:hover, .date > a:visited { + color: #3D3D3D !important; +} +.date > a:hover { + text-decoration: none !important; +} \ No newline at end of file From 212f49623e936d860ec387aca73f6c44daa42096 Mon Sep 17 00:00:00 2001 From: stisa Date: Mon, 30 Oct 2017 20:05:40 +0100 Subject: [PATCH 081/451] merge css rules --- public/css/style.css | 2 -- 1 file changed, 2 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index c43b92e..d4aa78d 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -753,7 +753,5 @@ blockquote p { } .date > a, .date > a:hover, .date > a:visited { color: #3D3D3D !important; -} -.date > a:hover { text-decoration: none !important; } \ No newline at end of file From a44e17d03a0c2de022113a24a92fa39e40b5c56b Mon Sep 17 00:00:00 2001 From: stisa Date: Tue, 7 Nov 2017 23:35:59 +0100 Subject: [PATCH 082/451] restrict run to langNim --- forms.tmpl | 2 +- utils.nim | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/forms.tmpl b/forms.tmpl index fcb2623..3fa8481 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -241,7 +241,7 @@ element.appendChild(runDiv); } function appendRunBtn(){ - var els = Array.prototype.slice.call(document.getElementsByClassName("listing")); + var els = Array.prototype.slice.call(document.getElementsByClassName("langNim")); els.forEach(appendEachRunBtn, this); } appendRunBtn() diff --git a/utils.nim b/utils.nim index 10c57ab..f30ad60 100644 --- a/utils.nim +++ b/utils.nim @@ -28,6 +28,7 @@ type var docConfig: StringTableRef docConfig = rstgen.defaultConfig() +docConfig["doc.listing_start"] = "
"
 docConfig["doc.smiley_format"] = "/images/smilieys/$1.png"
 
 proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config =

From 6200a30ccf8ea22d93133e711842a4497659ef6b Mon Sep 17 00:00:00 2001
From: Dominik Picheta 
Date: Mon, 7 May 2018 18:45:03 +0100
Subject: [PATCH 083/451] Add Spectre skeleton with builder.

---
 .gitmodules            | 3 +++
 redesign/builder.nim   | 4 ++++
 redesign/nimforum.scss | 8 ++++++++
 redesign/spectre       | 1 +
 4 files changed, 16 insertions(+)
 create mode 100644 .gitmodules
 create mode 100644 redesign/builder.nim
 create mode 100644 redesign/nimforum.scss
 create mode 160000 redesign/spectre

diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..ef41742
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "redesign/spectre"]
+	path = redesign/spectre
+	url = https://github.com/picturepan2/spectre
diff --git a/redesign/builder.nim b/redesign/builder.nim
new file mode 100644
index 0000000..5b2e528
--- /dev/null
+++ b/redesign/builder.nim
@@ -0,0 +1,4 @@
+import sass
+
+when isMainModule:
+  compileFile("nimforum.scss", "nimforum.css")
\ No newline at end of file
diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss
new file mode 100644
index 0000000..30a60d6
--- /dev/null
+++ b/redesign/nimforum.scss
@@ -0,0 +1,8 @@
+// Based on
+// https://picturepan2.github.io/spectre/getting-started.html#installation
+// Define variables to override default ones
+$primary-color: #2e5bec;
+$dark-color: #3e396b;
+
+// Import full Spectre source code
+@import "spectre/src/spectre";
\ No newline at end of file
diff --git a/redesign/spectre b/redesign/spectre
new file mode 160000
index 0000000..7a6af53
--- /dev/null
+++ b/redesign/spectre
@@ -0,0 +1 @@
+Subproject commit 7a6af53bca72b549b2cbf1948763348bd5b30dcd

From 86a7280585cc66f5dfd2af3f2c920794da7cb8dc Mon Sep 17 00:00:00 2001
From: Dominik Picheta 
Date: Mon, 7 May 2018 18:53:31 +0100
Subject: [PATCH 084/451] Adds mockup skeleton HTML file.

---
 redesign/index.html | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)
 create mode 100644 redesign/index.html

diff --git a/redesign/index.html b/redesign/index.html
new file mode 100644
index 0000000..57820a8
--- /dev/null
+++ b/redesign/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+  
+  
+  
+
+  The Nim programming language forum
+
+  
+  
+
+
+
+
+
+
+
\ No newline at end of file

From c10d5f4e44fb66d37a6e9a1c577b80f1de5df0aa Mon Sep 17 00:00:00 2001
From: Dominik Picheta 
Date: Mon, 7 May 2018 19:58:42 +0100
Subject: [PATCH 085/451] Simple navbar. Will need to fix colours later.

---
 redesign/index.html    | 15 +++++++++++++++
 redesign/nimforum.scss | 22 +++++++++++++++++++++-
 2 files changed, 36 insertions(+), 1 deletion(-)

diff --git a/redesign/index.html b/redesign/index.html
index 57820a8..606a07c 100644
--- a/redesign/index.html
+++ b/redesign/index.html
@@ -13,6 +13,21 @@
 
 
 
+    
 
 
 
diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss
index 30a60d6..2f7065c 100644
--- a/redesign/nimforum.scss
+++ b/redesign/nimforum.scss
@@ -5,4 +5,24 @@ $primary-color: #2e5bec;
 $dark-color: #3e396b;
 
 // Import full Spectre source code
-@import "spectre/src/spectre";
\ No newline at end of file
+@import "spectre/src/spectre";
+
+// Custom styles.
+// - Navigation bar.
+$navbar-height: 60px;
+$logo-height: $navbar-height - 20px;
+
+#main-navbar {
+  background-color: #17181f;
+  height: $navbar-height;
+}
+
+#img-logo {
+  vertical-align: middle;
+  height: $logo-height;
+}
+
+.navbar-control {
+  margin-left: $control-padding-x;
+
+}
\ No newline at end of file

From 736ec8c09a0de5adabea40a7b470204df17515bf Mon Sep 17 00:00:00 2001
From: Dominik Picheta 
Date: Mon, 7 May 2018 20:26:17 +0100
Subject: [PATCH 086/451] Implement secondary "navbar" containing top buttons.

---
 redesign/index.html    | 21 +++++++++++++++++++++
 redesign/nimforum.scss | 11 +++++++++++
 2 files changed, 32 insertions(+)

diff --git a/redesign/index.html b/redesign/index.html
index 606a07c..a11e367 100644
--- a/redesign/index.html
+++ b/redesign/index.html
@@ -9,6 +9,7 @@
   The Nim programming language forum
 
   
+  
   
 
 
@@ -29,6 +30,26 @@
       
     
 
+    
+
 
 
 
\ No newline at end of file
diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss
index 2f7065c..e4766d5 100644
--- a/redesign/nimforum.scss
+++ b/redesign/nimforum.scss
@@ -7,6 +7,12 @@ $dark-color: #3e396b;
 // Import full Spectre source code
 @import "spectre/src/spectre";
 
+// Global styles.
+// - TODO: Make these non-global.
+.btn {
+  margin-left: $control-padding-x;
+}
+
 // Custom styles.
 // - Navigation bar.
 $navbar-height: 60px;
@@ -24,5 +30,10 @@ $logo-height: $navbar-height - 20px;
 
 .navbar-control {
   margin-left: $control-padding-x;
+}
 
+// - Main buttons
+#main-buttons {
+  margin-top: $control-padding-y;
+  margin-bottom: $control-padding-y;
 }
\ No newline at end of file

From 25b1b42f1317de73824ef584b0212a10f916d348 Mon Sep 17 00:00:00 2001
From: Dominik Picheta 
Date: Mon, 7 May 2018 20:37:47 +0100
Subject: [PATCH 087/451] Create threads table.

---
 redesign/index.html    | 64 ++++++++++++++++++++++++++++++++++++++++++
 redesign/nimforum.scss |  2 +-
 2 files changed, 65 insertions(+), 1 deletion(-)

diff --git a/redesign/index.html b/redesign/index.html
index a11e367..d14cc27 100644
--- a/redesign/index.html
+++ b/redesign/index.html
@@ -50,6 +50,70 @@
 
     
 
+    
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TopicCategoryUsersRepliesViewsActivity
Few mixed up questionshelp + + + + + + 554745m
Lexers and parsers in Nimcommunity + + 01444m
I need helphelp + + + + 424.5k1d
Nim v1.0 is here!announcement + + + 486.1k4d
+ + \ No newline at end of file diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index e4766d5..47f997d 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -2,7 +2,7 @@ // https://picturepan2.github.io/spectre/getting-started.html#installation // Define variables to override default ones $primary-color: #2e5bec; -$dark-color: #3e396b; +// $dark-color: #3e396b; // Import full Spectre source code @import "spectre/src/spectre"; From 591f665cef3d10af3f111f5fcc65470e65712aff Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 7 May 2018 20:56:27 +0100 Subject: [PATCH 088/451] Better avatars. --- redesign/index.html | 39 ++++++++++++++++++++++++++++----------- redesign/nimforum.scss | 9 ++++++++- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/redesign/index.html b/redesign/index.html index d14cc27..82c750b 100644 --- a/redesign/index.html +++ b/redesign/index.html @@ -67,11 +67,18 @@ Few mixed up questions help - - - - - +
+ +
+
+ +
+
+
+
+
+
+
5 547 @@ -81,7 +88,8 @@ Lexers and parsers in Nim community - +
+
0 14 @@ -91,9 +99,14 @@ I need help help - - - +
+ +
+
+ +
+
+
4 24.5k @@ -103,8 +116,12 @@ Nim v1.0 is here! announcement - - +
+ +
+
+ +
4 86.1k diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index 47f997d..601c6b4 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -13,6 +13,12 @@ $primary-color: #2e5bec; margin-left: $control-padding-x; } +// Spectre fixes. +// - Weird avatar outline. +.avatar { + background: transparent; +} + // Custom styles. // - Navigation bar. $navbar-height: 60px; @@ -36,4 +42,5 @@ $logo-height: $navbar-height - 20px; #main-buttons { margin-top: $control-padding-y; margin-bottom: $control-padding-y; -} \ No newline at end of file +} + From 91207650ccda2c304eba35fb3e3f00670daf9a4b Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 7 May 2018 21:06:21 +0100 Subject: [PATCH 089/451] Smaller table headings. --- redesign/nimforum.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index 601c6b4..adc3ce2 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -13,6 +13,9 @@ $primary-color: #2e5bec; margin-left: $control-padding-x; } +table th { + font-size: 0.65rem; +} // Spectre fixes. // - Weird avatar outline. .avatar { From 24b60f36a51a2015f4019ae38d46117cd65efd24 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 7 May 2018 21:21:34 +0100 Subject: [PATCH 090/451] Some messing about with view/reply/activity colours. --- redesign/index.html | 16 ++++++++-------- redesign/nimforum.scss | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/redesign/index.html b/redesign/index.html index 82c750b..929b0e8 100644 --- a/redesign/index.html +++ b/redesign/index.html @@ -81,8 +81,8 @@ 5 - 547 - 45m + 547 + 45m Lexers and parsers in Nim @@ -92,8 +92,8 @@ 0 - 14 - 44m + 14 + 44m I need help @@ -109,7 +109,7 @@ 4 - 24.5k + 1.4k 1d @@ -123,9 +123,9 @@ - 4 - 86.1k - 4d + 4 + 24.2k + 4d diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index adc3ce2..839d172 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -47,3 +47,19 @@ $logo-height: $navbar-height - 20px; margin-bottom: $control-padding-y; } +// - Thread table +$super-popular-color: #f86713; +$popular-color: darken($super-popular-color, 25%); +$views-color: #545d70; + +.super-popular-text { + color: $super-popular-color; +} + +.popular-text { + color: $popular-color; +} + +.views-text { + color: $views-color; +} \ No newline at end of file From 6e032045e631cde1605a3d12df00fc99b8882a62 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 7 May 2018 21:37:35 +0100 Subject: [PATCH 091/451] Create unread count label. --- redesign/index.html | 2 +- redesign/nimforum.scss | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/redesign/index.html b/redesign/index.html index 929b0e8..9dfd870 100644 --- a/redesign/index.html +++ b/redesign/index.html @@ -96,7 +96,7 @@ 44m - I need help + I need help 2 help
diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index 839d172..e3b6b2b 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -3,6 +3,7 @@ // Define variables to override default ones $primary-color: #2e5bec; // $dark-color: #3e396b; +$label-color: #7cd2ff; // Import full Spectre source code @import "spectre/src/spectre"; @@ -16,6 +17,7 @@ $primary-color: #2e5bec; table th { font-size: 0.65rem; } + // Spectre fixes. // - Weird avatar outline. .avatar { @@ -62,4 +64,14 @@ $views-color: #545d70; .views-text { color: $views-color; +} + +.label-custom { + color: white; + background-color: $label-color; + + font-size: 0.6rem; + padding-left: 0.3rem; + padding-right: 0.3rem; + border-radius: 5rem; } \ No newline at end of file From 88b0bf8ae5d003bcaa45e6557d0820e574cd6559 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 7 May 2018 22:09:09 +0100 Subject: [PATCH 092/451] Create last visit separator. --- redesign/index.html | 9 ++++++++- redesign/nimforum.scss | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/redesign/index.html b/redesign/index.html index 9dfd870..ad74ed3 100644 --- a/redesign/index.html +++ b/redesign/index.html @@ -95,7 +95,7 @@ 14 44m - + I need help 2 help @@ -112,6 +112,13 @@ 1.4k 1d + + + + last visit + + + Nim v1.0 is here! announcement diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index e3b6b2b..8d72d0c 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -74,4 +74,26 @@ $views-color: #545d70; padding-left: 0.3rem; padding-right: 0.3rem; border-radius: 5rem; +} + +.last-visit-separator { + td { + border-bottom: 1px solid $super-popular-color; + line-height: 0.1rem; + padding: 0; + text-align: center; + } + + span { + color: $super-popular-color; + padding: 0 8px; + font-size: 0.7rem; + background-color: $body-bg; + } +} + +.last-visit { + td { + border: none; + } } \ No newline at end of file From c62046d2a34650d22ecdbbddfe1d83f92065e7d0 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 7 May 2018 22:16:37 +0100 Subject: [PATCH 093/451] Add locked thread and solved thread icons. --- redesign/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redesign/index.html b/redesign/index.html index ad74ed3..ba41127 100644 --- a/redesign/index.html +++ b/redesign/index.html @@ -64,7 +64,7 @@ - Few mixed up questions + Few mixed up questions help
@@ -85,7 +85,7 @@ 45m - Lexers and parsers in Nim + Lexers and parsers in Nim community
From 7a13c32ccff1d34482ec7e87a5f9216d0915197a Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 7 May 2018 22:31:00 +0100 Subject: [PATCH 094/451] Triangles for category colours? Being different for the sake of it probably isn't a good idea, but I'll see how it goes. --- redesign/index.html | 8 ++++---- redesign/nimforum.scss | 9 +++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/redesign/index.html b/redesign/index.html index ba41127..78e8fd6 100644 --- a/redesign/index.html +++ b/redesign/index.html @@ -65,7 +65,7 @@ Few mixed up questions - help +
help
@@ -86,7 +86,7 @@ Lexers and parsers in Nim - community +
community
@@ -97,7 +97,7 @@ I need help 2 - help +
help
@@ -121,7 +121,7 @@ Nim v1.0 is here! - announcement +
announcement
diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index 8d72d0c..3826213 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -96,4 +96,13 @@ $views-color: #545d70; td { border: none; } +} + +.triangle { + width: 0; + height: 0; + border-left: 0.3rem solid transparent; + border-right: 0.3rem solid transparent; + border-bottom: 0.6rem solid #98c766; + display: inline-block; } \ No newline at end of file From 088afbb9ac47d6e6b676af964dc355091baabdf5 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 7 May 2018 23:18:09 +0100 Subject: [PATCH 095/451] Fixes navbar colours. --- redesign/index.html | 2 +- redesign/nimforum.scss | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/redesign/index.html b/redesign/index.html index 78e8fd6..732a6bc 100644 --- a/redesign/index.html +++ b/redesign/index.html @@ -17,7 +17,7 @@ @@ -35,7 +35,7 @@
diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index c2a3689..6579c70 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -4,6 +4,7 @@ $primary-color: #f99c19; // $dark-color: #3e396b; $label-color: #7cd2ff; +$filter-color: #f1f1f1; // Define nav bar colours. $navbar-color: #17181f; @@ -15,8 +16,8 @@ $navbar-primary-color: #fee860; // Global styles. // - TODO: Make these non-global. -.btn { - margin-left: $control-padding-x; +.btn, .form-input { + margin-right: $control-padding-x; } table th { @@ -62,14 +63,18 @@ $logo-height: $navbar-height - 20px; height: $logo-height; } -.navbar-control { - margin-left: $control-padding-x; -} - // - Main buttons #main-buttons { margin-top: $control-padding-y; margin-bottom: $control-padding-y; + + .dropdown > .btn { + background: $filter-color; + border-color: darken($filter-color, 5%); + color: invert($filter-color); + + margin-right: $control-padding-x*2; + } } // - Thread table From 343a842fe0ceae2809be3b7567f6f2e7240239f4 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 8 May 2018 13:38:15 +0100 Subject: [PATCH 099/451] Add icons to sign up/log in buttons. --- redesign/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redesign/index.html b/redesign/index.html index 64b5c18..5d0e13f 100644 --- a/redesign/index.html +++ b/redesign/index.html @@ -26,8 +26,8 @@
- - + +
From 72a1863c2933c7a31d46b717a17f682fd122babd Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 8 May 2018 13:49:25 +0100 Subject: [PATCH 100/451] Larger margin on top buttons. --- redesign/nimforum.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index 6579c70..d69ca98 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -65,8 +65,8 @@ $logo-height: $navbar-height - 20px; // - Main buttons #main-buttons { - margin-top: $control-padding-y; - margin-bottom: $control-padding-y; + margin-top: $control-padding-y*2; + margin-bottom: $control-padding-y*2; .dropdown > .btn { background: $filter-color; From e51b74a017600e0bcac957a9d3d7ffc156dfa494 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 8 May 2018 16:02:43 +0100 Subject: [PATCH 101/451] Initial thread design. --- redesign/nimforum.scss | 102 ++++++++++++++++++++++++++++++-- redesign/thread.html | 129 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 226 insertions(+), 5 deletions(-) create mode 100644 redesign/thread.html diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index d69ca98..72f9ba0 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -2,9 +2,9 @@ // https://picturepan2.github.io/spectre/getting-started.html#installation // Define variables to override default ones $primary-color: #f99c19; -// $dark-color: #3e396b; +// $dark-color: #17181f; $label-color: #7cd2ff; -$filter-color: #f1f1f1; +$secondary-btn-color: #f1f1f1; // Define nav bar colours. $navbar-color: #17181f; @@ -69,9 +69,9 @@ $logo-height: $navbar-height - 20px; margin-bottom: $control-padding-y*2; .dropdown > .btn { - background: $filter-color; - border-color: darken($filter-color, 5%); - color: invert($filter-color); + background: $secondary-btn-color; + border-color: darken($secondary-btn-color, 5%); + color: invert($secondary-btn-color); margin-right: $control-padding-x*2; } @@ -133,4 +133,96 @@ $views-color: #545d70; border-right: 0.3rem solid transparent; border-bottom: 0.6rem solid #98c766; display: inline-block; +} + +// - Thread view +.title { + margin-top: $control-padding-y*2; + margin-bottom: $control-padding-y*2; + + p { + font-size: 1.4rem; + font-weight: bold; + + color: darken($dark-color, 20%); + + margin: 0; + } +} + +.posts { + @extend .grid-sm; + @extend .container; + margin: 0; +} + +.post { + @extend .tile; +} + +.post-icon { + @extend .tile-icon; +} + +.post-avatar { + @extend .avatar; + @extend .avatar-xl; +} + +.post-main { + @extend .tile-content; + + margin-bottom: $control-padding-y-lg*2; +} + +.post-title { + margin-bottom: $control-padding-y*2; + + .post-username { + font-weight: bold; + display: inline-block; + } + + .post-time { + float: right; + } +} + +.post-content { + +} + +.post-buttons { + float: right; + + > div { + display: inline-block; + } + + .btn { + background: transparent; + border-color: transparent; + color: darken($secondary-btn-color, 40%); + + margin: 0; + margin-left: $control-padding-y-sm; + } + + .btn:hover { + background: $secondary-btn-color; + border-color: darken($secondary-btn-color, 5%); + color: invert($secondary-btn-color); + } + + .btn:focus { + @include control-shadow(darken($secondary-btn-color, 50%)); + } + + .btn:active { + box-shadow: inset 0 0 .4rem .01rem darken($secondary-btn-color, 80%); + } + + .like-button i { + color: #f783ac; + } } \ No newline at end of file diff --git a/redesign/thread.html b/redesign/thread.html new file mode 100644 index 0000000..513f1df --- /dev/null +++ b/redesign/thread.html @@ -0,0 +1,129 @@ + + + + + + + + + The Nim programming language forum + + + + + + + + + +
+
+

Lexers and parsers in nim

+
community +
+
+
+
+
+ Avatar +
+
+
+
+
+ ErikCampobadal +
+
44m
+
+
+

Hey! I'm willing to create a programming language using nim.

+ +

It's an educational project. Been reading about compilers for weeks now and I started using tools like flex and bison for lexer and parser. I know nim have a parsing library but nowhere near that level.

+ +

There is an old post (2014) with a similar question so I'm bringing that back a few years later. Is there anything anyone know that could speed up the process of developing a programing language using nim? (I can have c code if needed ofc)

+
+
+ +
+ +
+
+ +
+
+
+
+ +
+
+
+ Avatar +
+
+
+
+
+ twetzel59 +
+
44m
+
+
+

Wow, I was just reading about the compilation pipeline today!

+ +

I suppose you could use at least the lexing part from a generator like flex, not so sure about using AST generators easily (it's possible).

+ +

Is your language complicated enough to warrant a parser generator or could you just use a custom parser?

+
+
+ +
+ +
+
+ +
+
+
+
+ +
+
+ + + + \ No newline at end of file From 514bcf28edd0c44a9412d7328599902c82e2759c Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 8 May 2018 16:11:12 +0100 Subject: [PATCH 102/451] Small adjustments --- redesign/nimforum.scss | 3 +++ redesign/thread.html | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index 72f9ba0..a0f5d55 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -154,10 +154,13 @@ $views-color: #545d70; @extend .grid-sm; @extend .container; margin: 0; + padding: 0; } .post { @extend .tile; + border-top: 1px solid $border-color; + padding-top: $control-padding-y-lg; } .post-icon { diff --git a/redesign/thread.html b/redesign/thread.html index 513f1df..8e301ff 100644 --- a/redesign/thread.html +++ b/redesign/thread.html @@ -91,12 +91,12 @@
twetzel59
-
44m
+
32m

Wow, I was just reading about the compilation pipeline today!

-

I suppose you could use at least the lexing part from a generator like flex, not so sure about using AST generators easily (it's possible).

+

I suppose you could use at least the lexing part from a generator like flex, not so sure about using AST generators easily (it's possible).

Is your language complicated enough to warrant a parser generator or could you just use a custom parser?

From f4a1a97ccfb521ad81cd03f739d41b13133ad085 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 8 May 2018 16:54:35 +0100 Subject: [PATCH 103/451] Adjustments to code and blockquote styles. --- redesign/nimforum.scss | 24 +++++++++++++++-- redesign/thread.html | 61 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index a0f5d55..0cc5a6a 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -2,7 +2,8 @@ // https://picturepan2.github.io/spectre/getting-started.html#installation // Define variables to override default ones $primary-color: #f99c19; -// $dark-color: #17181f; +$body-font-color: #292929; +$dark-color: #525252; $label-color: #7cd2ff; $secondary-btn-color: #f1f1f1; @@ -180,7 +181,7 @@ $views-color: #545d70; .post-title { margin-bottom: $control-padding-y*2; - + color: lighten($body-font-color, 20%); .post-username { font-weight: bold; display: inline-block; @@ -228,4 +229,23 @@ $views-color: #545d70; .like-button i { color: #f783ac; } +} + +blockquote { + border-left: 0.2rem solid darken($bg-color, 10%); + background-color: $bg-color; + + .detail { + margin-bottom: $control-padding-y; + color: lighten($body-font-color, 20%); + } +} + +.quote-avatar { + @extend .avatar; + @extend .avatar-sm; +} + +.quote-link { + float: right; } \ No newline at end of file diff --git a/redesign/thread.html b/redesign/thread.html index 8e301ff..4fa5144 100644 --- a/redesign/thread.html +++ b/redesign/thread.html @@ -121,6 +121,67 @@
+ +
+
+
+ Avatar +
+
+
+
+
+ dom96 +
+
32m
+
+
+

Let us test this new design a bit, shall we?

+
proc hello(x: int) =
+  echo("Hello ", x)
+
+42.hello()
+ +

The greatest function ever written is hello.

+
+

Designing websites is often a pain.

+
Multi-level baby!
+

True that.

+

I also want to be able to support more detailed quoting:

+
+
+
+ Avatar +
+ Araq: + +
+ Unix is a cancer. +
+

We also want to be able to highlight user mentions:

+

Please let @Araq know that this forum is awesome.

+
+
+ +
+ +
+
+ +
+
+
+
+
From 3a44489fcc17041a92936b682b8e37c0bca3c658 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 8 May 2018 17:12:42 +0100 Subject: [PATCH 104/451] Use mentions in threads. --- redesign/nimforum.scss | 16 ++++++++++++++++ redesign/thread.html | 6 +++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index 0cc5a6a..a5ce088 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -248,4 +248,20 @@ blockquote { .quote-link { float: right; +} + +.user-mention { + @extend .chip; + vertical-align: initial; + font-weight: bold; + display: inline-block; + font-size: 85%; + height: inherit; + padding: 0.08rem 0.4rem; + background-color: darken($bg-color-dark, 5%); + + img { + @extend .avatar; + @extend .avatar-sm; + } } \ No newline at end of file diff --git a/redesign/thread.html b/redesign/thread.html index 4fa5144..56b4679 100644 --- a/redesign/thread.html +++ b/redesign/thread.html @@ -159,7 +159,11 @@ Unix is a cancer.

We also want to be able to highlight user mentions:

-

Please let @Araq know that this forum is awesome.

+

Please let + + @Araq + + know that this forum is awesome.

Hey! I'm willing to create a programming language using nim.

@@ -91,7 +91,7 @@
twetzel59
-
32m
+
Jan 2015

Wow, I was just reading about the compilation pipeline today!

@@ -121,6 +121,16 @@
+
+
+ +
+
+
+ 3 YEARS LATER +
+
+
From 00ac2332a5c7ab28028ea75a7f5af2ebfb385584 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 9 May 2018 12:58:34 +0100 Subject: [PATCH 109/451] Add simple "reply" info box. --- redesign/thread.html | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/redesign/thread.html b/redesign/thread.html index 4706d8c..7b6c470 100644 --- a/redesign/thread.html +++ b/redesign/thread.html @@ -196,6 +196,20 @@
+
+
+ +
+
+
+ Replying to "Lexers and parsers in nim" +
+
+ +
+
+
+
From 5795235a475ac09077a08f7a1641ca59bf1025b0 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 9 May 2018 13:23:14 +0100 Subject: [PATCH 110/451] Load More Threads button in thread list. --- redesign/index.html | 11 +++++++++-- redesign/nimforum.scss | 21 ++++++++++++++++++++- redesign/thread.html | 2 +- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/redesign/index.html b/redesign/index.html index 5d0e13f..b836674 100644 --- a/redesign/index.html +++ b/redesign/index.html @@ -96,7 +96,7 @@ 14 44m - + I need help 2
help @@ -120,7 +120,7 @@ - + Nim v1.0 is here!
announcement @@ -135,6 +135,13 @@ 24.2k 4d + + + + load more threads + + + diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index 8c5c8d1..eca1060 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -136,13 +136,14 @@ $views-color: #545d70; } } -.last-visit { +.no-border { td { border: none; } } .triangle { + // TODO: Abstract this into a "category" class. width: 0; height: 0; border-left: 0.3rem solid transparent; @@ -151,6 +152,24 @@ $views-color: #545d70; display: inline-block; } +.load-more-separator { + text-align: center; + color: darken($label-color, 35%); + // $border-color: desaturate(darken($label-color, 10%), 80%); + // border-top: 1px solid $border-color; + // border-bottom: 1px solid $border-color; + background-color: lighten($label-color, 15%); + text-transform: uppercase; + font-weight: bold; + font-size: 80%; + cursor: pointer; + + td { + border: none; + padding: $control-padding-x $control-padding-y/2; + } +} + // - Thread view .title { margin-top: $control-padding-y*2; diff --git a/redesign/thread.html b/redesign/thread.html index 7b6c470..e1015b2 100644 --- a/redesign/thread.html +++ b/redesign/thread.html @@ -205,7 +205,7 @@ Replying to "Lexers and parsers in nim"
- +
From 07e8af644e0518098ac93d697e56558968cefb64 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 9 May 2018 13:38:50 +0100 Subject: [PATCH 111/451] Load More Posts in thread.html. --- redesign/nimforum.scss | 21 ++++++++++++++++++--- redesign/thread.html | 20 +++++++++++++++++--- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index eca1060..7ed37ed 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -155,9 +155,6 @@ $views-color: #545d70; .load-more-separator { text-align: center; color: darken($label-color, 35%); - // $border-color: desaturate(darken($label-color, 10%), 80%); - // border-top: 1px solid $border-color; - // border-bottom: 1px solid $border-color; background-color: lighten($label-color, 15%); text-transform: uppercase; font-weight: bold; @@ -327,6 +324,10 @@ blockquote { .information-title { font-weight: bold; } + + &.no-border { + border: none; + } } .information-icon { @@ -338,4 +339,18 @@ blockquote { font-size: 1rem; } +} + +.time-passed { + text-transform: uppercase; +} + +.load-more-posts { + text-align: center; + color: darken($label-color, 35%); + background-color: lighten($label-color, 15%); + border: none; + text-transform: uppercase; + font-weight: bold; + cursor: pointer; } \ No newline at end of file diff --git a/redesign/thread.html b/redesign/thread.html index e1015b2..298bb80 100644 --- a/redesign/thread.html +++ b/redesign/thread.html @@ -121,13 +121,13 @@ -
+
- 3 YEARS LATER + 3 years later
@@ -196,7 +196,21 @@
-
+ + +
From 9e61224b2cc4494c22eb1f80fe50230ce6eb9872 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 9 May 2018 14:54:32 +0100 Subject: [PATCH 112/451] Beginnings with karax. --- redesign/forum.nim | 24 ++++++++++++++++++++++++ redesign/karax.html | 22 ++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 redesign/forum.nim create mode 100644 redesign/karax.html diff --git a/redesign/forum.nim b/redesign/forum.nim new file mode 100644 index 0000000..7287a50 --- /dev/null +++ b/redesign/forum.nim @@ -0,0 +1,24 @@ +include karax/prelude + + +proc genHeader(): VNode = + result = buildHtml(header(id="main-navbar")): + tdiv(class="navbar container grid-xl"): + section(class="navbar-section"): + a(href="/"): + img(src="images/crown.png", id="img-logo") # TODO: Customisable. + section(class="navbar-section"): + tdiv(class="input-group input-inline"): + input(class="form-input input-sm", `type`="text", placeholder="search") + button(class="btn btn-primary btn-sm"): + italic(class="fas fa-user-plus") + text " Sign up" + button(class="btn btn-primary btn-sm"): + italic(class="fas fa-sign-in-alt") + text " Log in" + +proc createDom(): VNode = + result = buildHtml(tdiv()): + genHeader() + +setRenderer createDom \ No newline at end of file diff --git a/redesign/karax.html b/redesign/karax.html new file mode 100644 index 0000000..054116d --- /dev/null +++ b/redesign/karax.html @@ -0,0 +1,22 @@ + + + + + + + + + The Nim programming language forum + + + + + + + + +
+ + + + From a88f879d36c88e482ef21a0476c81aa6029f3f96 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 9 May 2018 15:01:22 +0100 Subject: [PATCH 113/451] Top buttons in Karax. --- redesign/forum.nim | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/redesign/forum.nim b/redesign/forum.nim index 7287a50..ca9a847 100644 --- a/redesign/forum.nim +++ b/redesign/forum.nim @@ -6,7 +6,7 @@ proc genHeader(): VNode = tdiv(class="navbar container grid-xl"): section(class="navbar-section"): a(href="/"): - img(src="images/crown.png", id="img-logo") # TODO: Customisable. + img(src="images/crown.png", id="img-logo") # TODO: Customisation. section(class="navbar-section"): tdiv(class="input-group input-inline"): input(class="form-input input-sm", `type`="text", placeholder="search") @@ -17,8 +17,26 @@ proc genHeader(): VNode = italic(class="fas fa-sign-in-alt") text " Log in" +proc genTopButtons(): VNode = + result = buildHtml(): + section(class="navbar container grid-xl", id="main-buttons"): + section(class="navbar-section"): + tdiv(class="dropdown"): + a(href="#", class="btn dropdown-toggle"): + text "Filter " + italic(class="fas fa-caret-down") + ul(class="menu"): + li: text "community" + li: text "dev" + button(class="btn btn-primary"): text "Latest" + button(class="btn btn-link"): text "Most Active" + button(class="btn btn-link"): text "Categories" + section(class="navbar-section") + + proc createDom(): VNode = result = buildHtml(tdiv()): genHeader() + genTopButtons() setRenderer createDom \ No newline at end of file From 2efd6946607a8fad1fd151b3529330a3e9a7ee80 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 9 May 2018 17:15:03 +0100 Subject: [PATCH 114/451] Fixes forum for 0.18.1. --- forum.nim | 92 +++++++++++++++++++++++++++---------------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/forum.nim b/forum.nim index 11c098d..73f1ede 100644 --- a/forum.nim +++ b/forum.nim @@ -182,26 +182,26 @@ proc toInterval(diff: int64): TimeInterval = remaining -= hours * 3600 let minutes = remaining div 60 remaining -= minutes * 60 - result = initInterval(0, remaining.int, minutes.int, hours.int, days.int, + result = initInterval(remaining.int, minutes.int, hours.int, days.int, months.int, years.int) proc formatTimestamp(t: int): string = - let t2 = Time(t) + let t2 = fromUnix(t) let now = getTime() - let diff = (now - t2).toInterval() - if diff.years > 0: - return getGMTime(t2).format("MMMM d',' yyyy") - elif diff.months > 0: - return $diff.months & (if diff.months > 1: " months ago" else: " month ago") - elif diff.days > 0: - return $diff.days & (if diff.days > 1: " days ago" else: " day ago") - elif diff.hours > 0: - return $diff.hours & (if diff.hours > 1: " hours ago" else: " hour ago") - elif diff.minutes > 0: - return $diff.minutes & - (if diff.minutes > 1: " minutes ago" else: " minute ago") - else: - return "just now" + # let diff = (now - t2).toInterval() + # if diff.years > 0: + # return getGMTime(t2).format("MMMM d',' yyyy") + # elif diff.months > 0: + # return $diff.months & (if diff.months > 1: " months ago" else: " month ago") + # elif diff.days > 0: + # return $diff.days & (if diff.days > 1: " days ago" else: " day ago") + # elif diff.hours > 0: + # return $diff.hours & (if diff.hours > 1: " hours ago" else: " hour ago") + # elif diff.minutes > 0: + # return $diff.minutes & + # (if diff.minutes > 1: " minutes ago" else: " minute ago") + # else: + return "just now" proc getGravatarUrl(email: string, size = 80): string = let emailMD5 = email.toLowerAscii.toMD5 @@ -755,9 +755,10 @@ proc getStats(c: TForumData, simple: bool): TForumStats = sql"select id, name, strftime('%s', lastOnline), strftime('%s', creation) from person" for row in fastRows(db, getUsersQuery): let secs = if row[3] == "": 0 else: row[3].parseint - let lastOnlineSeconds = getTime() - Time(secs) - if lastOnlineSeconds < (60 * 5): # 5 minutes - result.activeUsers.add((row[1], row[0].parseInt)) + when false: + let lastOnlineSeconds = getTime() - Time(secs) + if lastOnlineSeconds < (60 * 5): # 5 minutes + result.activeUsers.add((row[1], row[0].parseInt)) if row[3].parseInt > newestMemberCreation: result.newestMember = (row[1], row[0].parseInt) newestMemberCreation = row[3].parseInt @@ -854,6 +855,7 @@ proc getPagesInThreadByID(c: TForumData, thrid: int): int = result = ceil(c.gatherTotalPostsByID(thrid) / PostsPerPage).int proc getThreadTitle(thrid: int, pageNum: int): string = + echo thrid result = getValue(db, sql"select name from thread where id = ?", $thrid) if pageNum notin {0,1}: result.add(" - Page " & $pageNum) @@ -923,7 +925,7 @@ proc genProfile(c: TForumData, ui: TUserInfo): string = ) ) result.add(htmlgen.`div`(id = "avatar", genGravatar(ui.email, 250))) - let t2 = if ui.lastOnline != -1: getGMTime(Time(ui.lastOnline)) + let t2 = if ui.lastOnline != -1: getGMTime(fromUnix(ui.lastOnline)) else: getGMTime(getTime()) result.add(htmlgen.`div`(id = "info", @@ -980,6 +982,23 @@ proc prependRe(s: string): string = elif s.startswith("Re:"): s else: "Re: " & s +proc initialise() = + randomize() + db = open(connection="nimforum.db", user="postgres", password="", + database="nimforum") + isFTSAvailable = db.getAllRows(sql("SELECT name FROM sqlite_master WHERE " & + "type='table' AND name='post_fts'")).len == 1 + config = loadConfig() + if len(config.recaptchaSecretKey) > 0 and len(config.recaptchaSiteKey) > 0: + useCaptcha = true + captcha = initReCaptcha(config.recaptchaSecretKey, config.recaptchaSiteKey) + else: + useCaptcha = false + var http = true + if paramCount() > 0: + if paramStr(1) == "scgi": + http = false + template createTFD() = var c {.inject.}: TForumData new(c) @@ -992,6 +1011,9 @@ template createTFD() = if request.cookies.len > 0: checkLoggedIn(c) + +initialise() + routes: get "/": createTFD() @@ -1104,9 +1126,9 @@ routes: template handleError(action: string, topText: string, isEdit: bool) = if c.isPreview: - body.add genPostPreview(c, @"subject", @"content", + body().add genPostPreview(c, @"subject", @"content", c.userName, $getGMTime(getTime())) - body.add genFormPost(c, action, topText, reuseText, reuseText, isEdit) + body().add genFormPost(c, action, topText, reuseText, reuseText, isEdit) resp genMain(c, body(), "Nim Forum - " & (if c.isPreview: "Preview" else: "Error")) @@ -1185,8 +1207,8 @@ routes: cond(@"nick" != "") if c.rank < Moderator: resp genMain(c, "You cannot delete this user's data.", "Error - Nim Forum") - let result = deleteAll(c, @"nick") - if result: + let res = deleteAll(c, @"nick") + if res: redirect(c.req.makeUri("/profile/" & @"nick")) else: resp genMain(c, "Failed to delete all user's posts and threads.", @@ -1348,25 +1370,3 @@ routes: textPage "static/search-help" get "/rst": textPage "static/rst" - -when isMainModule: - randomize() - db = open(connection="nimforum.db", user="postgres", password="", - database="nimforum") - isFTSAvailable = db.getAllRows(sql("SELECT name FROM sqlite_master WHERE " & - "type='table' AND name='post_fts'")).len == 1 - config = loadConfig() - if len(config.recaptchaSecretKey) > 0 and len(config.recaptchaSiteKey) > 0: - useCaptcha = true - captcha = initReCaptcha(config.recaptchaSecretKey, config.recaptchaSiteKey) - else: - useCaptcha = false - var http = true - if paramCount() > 0: - if paramStr(1) == "scgi": - http = false - - #run("", port = TPort(9000), http = http) - - runForever() - db.close() From 8910e55ad1b30f22a27fee85ab4749494ec8f93d Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 9 May 2018 19:02:18 +0100 Subject: [PATCH 115/451] Implements thread list in Karax and backend. --- forms.tmpl | 66 +++++++++++------------ forum.nim | 64 +++++++++++++++++++++- main.tmpl | 40 +++++++------- redesign/forum.nim | 42 +++++++++++++-- redesign/forum.nim.cfg | 1 + redesign/karaxutils.nim | 5 ++ redesign/threadlist.nim | 115 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 276 insertions(+), 57 deletions(-) create mode 100644 redesign/forum.nim.cfg create mode 100644 redesign/karaxutils.nim create mode 100644 redesign/threadlist.nim diff --git a/forms.tmpl b/forms.tmpl index c9025d2..c88ac9a 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -1,6 +1,6 @@ #? stdtmpl | standard # -#template `%`(idx: untyped): untyped = +#template `!`(idx: untyped): untyped = # row[idx] #end template # @@ -47,15 +47,15 @@
- ${xmlEncode(%name)} - ${genPagenumLocalNav(c, (%threadid).parseInt)} + ${xmlEncode(!name)} + ${genPagenumLocalNav(c, (!threadid).parseInt)}
#let users = getAllRows(db, # sql("select distinct name, email from person where id in " & - # "(select author from post where thread = ?)"), %threadId) + # "(select author from post where thread = ?)"), !threadId)
#for i in 0 .. min(6, users.len-1): @@ -66,19 +66,19 @@ #let latestReplyAuthor = getValue(db, sql("select name from person where id = " & # "(select author from post where id = " & - # "(select max(id) from post where thread = ?))"), %threadId) + # "(select max(id) from post where thread = ?))"), !threadId) #let replyProfileUrl = c.req.makeUri("profile/", false) & # xmlEncode(latestReplyAuthor) -# let posts = getValue(db, sql"select count(*) from post where thread = ?", %threadId) +# let posts = getValue(db, sql"select count(*) from post where thread = ?", !threadId)
-
${xmlEncode(%views)}
+
${xmlEncode(!views)}
$posts
#let latestReplyDate = getValue(db, sql("SELECT strftime('%s', " & - # "(select creation from post where id = (select max(id) from post where thread = ?)))"), %threadId) + # "(select creation from post where id = (select max(id) from post where thread = ?)))"), !threadId) #let timeStr = formatTimestamp(latestReplyDate.parseInt())
@@ -150,28 +150,28 @@
# for row in posts: # inc(count) - -
+ +
- #let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(%userName) -
${genGravatar(%userEmail)}
- ${xmlEncode(%userName)} - #if c.userId == %postAuthor and c.currentPost.subject.len == 0: -
Edit post + #let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(!userName) +
${genGravatar(!userEmail)}
+ ${xmlEncode(!userName)} + #if c.userId == !postAuthor and c.currentPost.subject.len == 0: +
Edit post #elif c.rank >= Moderator and c.currentPost.subject.len == 0: -
Edit post +
Edit post #end if
#try: - ${(%postContent).rstToHtml} + ${(!postContent).rstToHtml} #except EParseError: # c.errorMsg = getCurrentExceptionMsg() #end - ${xmlEncode(%postCreation)} + ${xmlEncode(!postCreation)}
@@ -421,43 +421,43 @@
# for row in results(): # inc(count) -# let isThread = %what == "0" +# let isThread = !what == "0" # inc(whCount[isThread]) -# let postUrl = c.genThreadUrl(%postId,"",%threadId,"") -# let threadUrl = c.genThreadUrl("","",%threadId) +# let postUrl = c.genThreadUrl(!postId,"",!threadId,"") +# let threadUrl = c.genThreadUrl("","",!threadId) # var headersDiffer = false
- #let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(%userName) - - - #if c.userId == %postAuthor and c.currentPost.subject.len == 0: -
Edit post + #let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(!userName) + + + #if c.userId == !postAuthor and c.currentPost.subject.len == 0: +
Edit post #elif c.rank >= Moderator and c.currentPost.subject.len == 0: -
Edit post +
Edit post #end if
- #if %postHeader != "": + #if !postHeader != "": #end if #if not isThread: #try: - ${(%postContent).rstToHtml} + ${(!postContent).rstToHtml} #except EParseError: # c.errorMsg = getCurrentExceptionMsg() - ${xmlEncode(%postContent)} + ${xmlEncode(!postContent)} #end #end if - ${xmlEncode(%postCreation)} + ${xmlEncode(!postCreation)}
diff --git a/forum.nim b/forum.nim index 73f1ede..19d3a50 100644 --- a/forum.nim +++ b/forum.nim @@ -9,7 +9,9 @@ import os, strutils, times, md5, strtabs, cgi, math, db_sqlite, scgi, jester, asyncdispatch, asyncnet, cache, sequtils, - parseutils, utils, random, rst, ranks, recaptcha + parseutils, utils, random, rst, ranks, recaptcha, json + +import redesign/threadlist except User when not defined(windows): import bcrypt # TODO @@ -1024,6 +1026,66 @@ routes: additionalHeaders = genRSSHeaders(c), showRssLinks = true) resp data + get "/karax.html": + resp readFile("redesign/karax.html") + get "/nimforum.css": + resp readFile("redesign/nimforum.css"), "text/css" + get "/nimcache/forum.js": + resp readFile("redesign/nimcache/forum.js"), "application/javascript" + get "/images/crown.png": + resp readFile("redesign/images/crown.png"), "image/png" + + get "/threads.json": + var + start = 0 + count = 30 + parseInt(@"start", start, 0..1_000_000) + parseInt(@"count", start, 0..1_000_000) + + const threadsQuery = + sql"""select id, name, views, strftime('%s', modified) from thread + order by modified desc limit ?, ?;""" + const postsQuery = + sql"""select count(*), strftime('%s', creation) from post + where thread = ? + order by creation asc limit 1;""" + const usersListQuery = + sql"""select distinct name, email, strftime('%s',lastOnline) + from person where id in + (select author from post where thread = ?);""" + + let thrCount = getValue(db, sql"select count(*) from thread;").parseInt() + let moreCount = max(0, thrCount - (start + count)) + + var list = ThreadList(threads: @[], lastVisit: 0, moreCount: moreCount) + for data in getAllRows(db, threadsQuery, start, count): + let posts = getRow(db, postsQuery, data[0]) + + var thread = Thread( + id: data[0].parseInt, + topic: data[1], + category: Category(id: "", color: "#ff0000"), # TODO + users: @[], + replies: posts[0].parseInt, + views: data[2].parseInt, + activity: data[3].parseInt, + creation: posts[1].parseInt, + isLocked: false, + isSolved: false # TODO: ^ and this. Add a field to `post` to identify. + ) + + # Gather the users list. + for user in getAllRows(db, usersListQuery, thread.id): + let isOnline = getTime().toUnix() - user[2].parseInt > (60*5) + thread.users.add(threadlist.User( + name: user[0], + avatarUrl: user[1].getGravatarUrl(), + isOnline: isOnline + )) + list.threads.add(thread) + + resp $(%list), "application/json" + get "/threadActivity.xml": createTFD() c.isThreadsList = true diff --git a/main.tmpl b/main.tmpl index f295433..55ea1de 100644 --- a/main.tmpl +++ b/main.tmpl @@ -207,20 +207,20 @@ ${recent} # for row in rows(db, query, 10): - ${xmlEncode(%name)} - urn:entry:${%threadid} - # let url = c.genThreadUrl(threadid = %threadid, - # pageNum = $(ceil(parseInt(%postCount) / PostsPerPage).int)) & - # "#" & %postId + ${xmlEncode(!name)} + urn:entry:${!threadid} + # let url = c.genThreadUrl(threadid = !threadid, + # pageNum = $(ceil(parseInt(!postCount) / PostsPerPage).int)) & + # "#" & !postId - ${%threadDate} - ${%threadDate} - ${xmlEncode(%postAuthor)} + ${!threadDate} + ${!threadDate} + ${xmlEncode(!postAuthor)} Posts ${%postCount}, ${xmlEncode(%postAuthor)} said: +>Posts ${!postCount}, ${xmlEncode(!postAuthor)} said: <p> -${xmlEncode(rstToHtml(%postContent))} +${xmlEncode(rstToHtml(!postContent))} # end for @@ -256,20 +256,20 @@ ${xmlEncode(rstToHtml(%postContent))} ${recent} # for row in rows(db, query, 10): - ${xmlEncode(%postHeader)} - urn:entry:${%postId} - # let url = c.genThreadUrl(threadid = %postThread, - # pageNum = $(ceil(parseInt(%postPosition) / PostsPerPage).int)) & - # "#" & %postId + ${xmlEncode(!postHeader)} + urn:entry:${!postId} + # let url = c.genThreadUrl(threadid = !postThread, + # pageNum = $(ceil(parseInt(!postPosition) / PostsPerPage).int)) & + # "#" & !postId - ${%postRssDate} - ${%postRssDate} - ${xmlEncode(%postAuthor)} + ${!postRssDate} + ${!postRssDate} + ${xmlEncode(!postAuthor)} On ${xmlEncode(%postHumanDate)}, ${xmlEncode(%postAuthor)} said: +>On ${xmlEncode(!postHumanDate)}, ${xmlEncode(!postAuthor)} said: <p> -${xmlEncode(rstToHtml(%postContent))} +${xmlEncode(rstToHtml(!postContent))} # end for diff --git a/redesign/forum.nim b/redesign/forum.nim index ca9a847..15080b2 100644 --- a/redesign/forum.nim +++ b/redesign/forum.nim @@ -1,5 +1,21 @@ -include karax/prelude +import strformat, times, options, json +include karax/prelude +import karax / [vstyles, kajax] + +import threadlist, karaxutils + +type + State = ref object + list: Option[ThreadList] + +proc newState(): State = + State( + list: none[ThreadList]() + ) + +const + baseUrl = "http://localhost:5000/" proc genHeader(): VNode = result = buildHtml(header(id="main-navbar")): @@ -34,9 +50,29 @@ proc genTopButtons(): VNode = section(class="navbar-section") -proc createDom(): VNode = +var state = newState() + +proc onThreadList(httpStatus: int, response: kstring) = + let parsed = parseJson($response) + let list = to(parsed, ThreadList) + + if state.list.isSome: + state.list.get().threads.add(list.threads) + state.list.get().moreCount = list.moreCount + state.list.get().lastVisit = list.lastVisit + else: + state.list = some(list) + +proc render(): VNode = + if state.list.isNone: + ajaxGet(baseUrl & "threads.json", @[], onThreadList) + result = buildHtml(tdiv()): genHeader() genTopButtons() + if state.list.isNone: + tdiv(class="loading loading-lg") + else: + genThreadList(state.list.get()) -setRenderer createDom \ No newline at end of file +setRenderer render \ No newline at end of file diff --git a/redesign/forum.nim.cfg b/redesign/forum.nim.cfg new file mode 100644 index 0000000..6869201 --- /dev/null +++ b/redesign/forum.nim.cfg @@ -0,0 +1 @@ +-d:js \ No newline at end of file diff --git a/redesign/karaxutils.nim b/redesign/karaxutils.nim new file mode 100644 index 0000000..efda777 --- /dev/null +++ b/redesign/karaxutils.nim @@ -0,0 +1,5 @@ +proc class*(classes: varargs[tuple[name: string, present: bool]], + defaultClasses: string = ""): string = + result = defaultClasses & " " + for class in classes: + if class.present: result.add(class.name & " ") \ No newline at end of file diff --git a/redesign/threadlist.nim b/redesign/threadlist.nim new file mode 100644 index 0000000..594bf1b --- /dev/null +++ b/redesign/threadlist.nim @@ -0,0 +1,115 @@ +import strformat, times + +type + User* = object + name*: string + avatarUrl*: string + isOnline*: bool + + Category* = object + id*: string + color*: string + + Thread* = object + id*: int + topic*: string + category*: Category + users*: seq[User] + replies*: int + views*: int + activity*: int64 ## Unix timestamp + creation*: int64 ## Unix timestamp + isLocked*: bool + isSolved*: bool + + ThreadList* = ref object + threads*: seq[Thread] + lastVisit*: int64 ## Unix timestamp + moreCount*: int ## How many more threads are left + +when defined(js): + include karax/prelude + import karax / [vstyles] + + import karaxutils + + proc genUserAvatars(users: seq[User]): VNode = + result = buildHtml(td): + for user in users: + figure(class="avatar avatar-sm"): + img(src=user.avatarUrl, title=user.name) + if user.isOnline: + italic(class="avatar-presense online") + + proc renderActivity(activity: int64): string = + let currentTime = getTime() + let activityTime = fromUnix(activity) + let duration = currentTime - activityTime + if duration.days > 300: + return activityTime.local().format("MMM yyyy") + elif duration.days > 30 and duration.days < 300: + return activityTime.local().format("MMM dd") + elif duration.days != 0: + return $duration.days & "d" + elif duration.hours != 0: + return $duration.hours & "h" + elif duration.minutes != 0: + return $duration.minutes & "m" + else: + return $duration.seconds & "s" + + proc genThread(thread: Thread, isNew: bool, noBorder: bool): VNode = + result = buildHtml(): + tr(class=class({"no-border": noBorder})): + td(): + if thread.isLocked: + italic(class="fas fa-lock fa-xs") + text thread.topic + td(): + tdiv(class="triangle", + style=style( + (StyleAttr.borderBottom, kstring"0.6rem solid " & thread.category.color) + )): + text thread.category.id + genUserAvatars(thread.users) + td(): text $thread.replies + td(class=class({ + "views-text": thread.views < 999, + "popular-text": thread.views > 999 and thread.views < 5000, + "super-popular-text": thread.views > 5000 + })): + if thread.views > 999: + text fmt"{thread.views/1000:.1f}" + else: + text $thread.views + td(class=class({"text-success": isNew, "text-gray": not isNew})): # TODO: Colors. + text renderActivity(thread.activity) + + proc genThreadList*(list: ThreadList): VNode = + result = buildHtml(): + section(class="container grid-xl"): # TODO: Rename to `.thread-list`. + table(class="table"): + thead(): + tr: + th(text "Topic") + th(text "Category") + th(text "Users") + th(text "Replies") + th(text "Views") + th(text "Activity") + tbody(): + for i in 0 ..< list.threads.len: + let thread = list.threads[i] + let isLastVisit = + i+1 < list.threads.len and list.threads[i].activity < list.lastVisit + let isNew = thread.creation < list.lastVisit + genThread(thread, isNew, noBorder=isLastVisit) + if isLastVisit: + tr(class="last-visit-separator"): + td(colspan="6"): + span(text "last visit") + + if list.moreCount > 0: + tr(class="load-more-separator"): + td(colspan="6"): + span(text "load more threads") From ca0de4d2aef914c36dc584e2a0ecb424fe2c3b1f Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 9 May 2018 19:16:12 +0100 Subject: [PATCH 116/451] Small fixes and adjustments to thread list. --- forum.nim | 5 +++-- redesign/threadlist.nim | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/forum.nim b/forum.nim index 19d3a50..c9220c3 100644 --- a/forum.nim +++ b/forum.nim @@ -1050,9 +1050,10 @@ routes: where thread = ? order by creation asc limit 1;""" const usersListQuery = - sql"""select distinct name, email, strftime('%s',lastOnline) + sql"""select distinct name, email, strftime('%s', lastOnline) from person where id in - (select author from post where thread = ?);""" + (select author from post where thread = ?) + limit 5;""" # TODO: Order by most posts. let thrCount = getValue(db, sql"select count(*) from thread;").parseInt() let moreCount = max(0, thrCount - (start + count)) diff --git a/redesign/threadlist.nim b/redesign/threadlist.nim index 594bf1b..f4646bc 100644 --- a/redesign/threadlist.nim +++ b/redesign/threadlist.nim @@ -40,6 +40,7 @@ when defined(js): img(src=user.avatarUrl, title=user.name) if user.isOnline: italic(class="avatar-presense online") + text " " proc renderActivity(activity: int64): string = let currentTime = getTime() @@ -66,11 +67,12 @@ when defined(js): italic(class="fas fa-lock fa-xs") text thread.topic td(): - tdiv(class="triangle", - style=style( - (StyleAttr.borderBottom, kstring"0.6rem solid " & thread.category.color) - )): - text thread.category.id + if thread.category.id.len > 0: + tdiv(class="triangle", + style=style( + (StyleAttr.borderBottom, kstring"0.6rem solid " & thread.category.color) + )): + text thread.category.id genUserAvatars(thread.users) td(): text $thread.replies td(class=class({ @@ -79,7 +81,7 @@ when defined(js): "super-popular-text": thread.views > 5000 })): if thread.views > 999: - text fmt"{thread.views/1000:.1f}" + text fmt"{thread.views/1000:.1f}k" else: text $thread.views td(class=class({"text-success": isNew, "text-gray": not isNew})): # TODO: Colors. @@ -93,7 +95,7 @@ when defined(js): tr: th(text "Topic") th(text "Category") - th(text "Users") + th(style=style((StyleAttr.width, kstring"8rem"))): text "Users" th(text "Replies") th(text "Views") th(text "Activity") @@ -103,7 +105,8 @@ when defined(js): let isLastVisit = i+1 < list.threads.len and list.threads[i].activity < list.lastVisit let isNew = thread.creation < list.lastVisit - genThread(thread, isNew, noBorder=isLastVisit) + genThread(thread, isNew, + noBorder=isLastVisit or i+1 == list.threads.len) if isLastVisit: tr(class="last-visit-separator"): td(colspan="6"): From 2e95d078e15cdc08e41e97505a854a6aaae196cd Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 9 May 2018 22:00:30 +0100 Subject: [PATCH 117/451] Move genTopButtons to threadlist module. --- redesign/forum.nim | 17 ----------------- redesign/threadlist.nim | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/redesign/forum.nim b/redesign/forum.nim index 15080b2..36a9667 100644 --- a/redesign/forum.nim +++ b/redesign/forum.nim @@ -33,23 +33,6 @@ proc genHeader(): VNode = italic(class="fas fa-sign-in-alt") text " Log in" -proc genTopButtons(): VNode = - result = buildHtml(): - section(class="navbar container grid-xl", id="main-buttons"): - section(class="navbar-section"): - tdiv(class="dropdown"): - a(href="#", class="btn dropdown-toggle"): - text "Filter " - italic(class="fas fa-caret-down") - ul(class="menu"): - li: text "community" - li: text "dev" - button(class="btn btn-primary"): text "Latest" - button(class="btn btn-link"): text "Most Active" - button(class="btn btn-link"): text "Categories" - section(class="navbar-section") - - var state = newState() proc onThreadList(httpStatus: int, response: kstring) = diff --git a/redesign/threadlist.nim b/redesign/threadlist.nim index f4646bc..7a38e5e 100644 --- a/redesign/threadlist.nim +++ b/redesign/threadlist.nim @@ -33,6 +33,22 @@ when defined(js): import karaxutils + proc genTopButtons*(): VNode = + result = buildHtml(): + section(class="navbar container grid-xl", id="main-buttons"): + section(class="navbar-section"): + tdiv(class="dropdown"): + a(href="#", class="btn dropdown-toggle"): + text "Filter " + italic(class="fas fa-caret-down") + ul(class="menu"): + li: text "community" + li: text "dev" + button(class="btn btn-primary"): text "Latest" + button(class="btn btn-link"): text "Most Active" + button(class="btn btn-link"): text "Categories" + section(class="navbar-section") + proc genUserAvatars(users: seq[User]): VNode = result = buildHtml(td): for user in users: From 032d70a233182f3d8b5795494ebc2b63437cbe7e Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 9 May 2018 22:10:03 +0100 Subject: [PATCH 118/451] Move everything related to `ThreadList` to the threadlist module. --- redesign/forum.nim | 30 +++----------------------- redesign/threadlist.nim | 48 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/redesign/forum.nim b/redesign/forum.nim index 36a9667..a5ebbb9 100644 --- a/redesign/forum.nim +++ b/redesign/forum.nim @@ -1,21 +1,15 @@ import strformat, times, options, json include karax/prelude -import karax / [vstyles, kajax] + import threadlist, karaxutils type State = ref object - list: Option[ThreadList] proc newState(): State = - State( - list: none[ThreadList]() - ) - -const - baseUrl = "http://localhost:5000/" + State() proc genHeader(): VNode = result = buildHtml(header(id="main-navbar")): @@ -35,27 +29,9 @@ proc genHeader(): VNode = var state = newState() -proc onThreadList(httpStatus: int, response: kstring) = - let parsed = parseJson($response) - let list = to(parsed, ThreadList) - - if state.list.isSome: - state.list.get().threads.add(list.threads) - state.list.get().moreCount = list.moreCount - state.list.get().lastVisit = list.lastVisit - else: - state.list = some(list) - proc render(): VNode = - if state.list.isNone: - ajaxGet(baseUrl & "threads.json", @[], onThreadList) - result = buildHtml(tdiv()): genHeader() - genTopButtons() - if state.list.isNone: - tdiv(class="loading loading-lg") - else: - genThreadList(state.list.get()) + renderThreadList() setRenderer render \ No newline at end of file diff --git a/redesign/threadlist.nim b/redesign/threadlist.nim index 7a38e5e..7149cec 100644 --- a/redesign/threadlist.nim +++ b/redesign/threadlist.nim @@ -1,4 +1,4 @@ -import strformat, times +import strformat, times, options, json type User* = object @@ -27,13 +27,28 @@ type lastVisit*: int64 ## Unix timestamp moreCount*: int ## How many more threads are left +const + baseUrl = "http://localhost:5000/" + when defined(js): include karax/prelude - import karax / [vstyles] + import karax / [vstyles, kajax, kdom] import karaxutils - proc genTopButtons*(): VNode = + type + State = ref object + list: Option[ThreadList] + + proc newState(): State = + State( + list: none[ThreadList]() + ) + + var + state = newState() + + proc genTopButtons(): VNode = result = buildHtml(): section(class="navbar container grid-xl", id="main-buttons"): section(class="navbar-section"): @@ -103,7 +118,24 @@ when defined(js): td(class=class({"text-success": isNew, "text-gray": not isNew})): # TODO: Colors. text renderActivity(thread.activity) - proc genThreadList*(list: ThreadList): VNode = + proc onThreadList(httpStatus: int, response: kstring) = + let parsed = parseJson($response) + let list = to(parsed, ThreadList) + + if state.list.isSome: + state.list.get().threads.add(list.threads) + state.list.get().moreCount = list.moreCount + state.list.get().lastVisit = list.lastVisit + else: + state.list = some(list) + + proc genThreadList(): VNode = + if state.list.isNone: + ajaxGet(baseUrl & "threads.json", @[], onThreadList) + + return buildHtml(tdiv(class="loading loading-lg")) + + let list = state.list.get() result = buildHtml(): section(class="container grid-xl"): # TODO: Rename to `.thread-list`. table(class="table"): @@ -119,7 +151,8 @@ when defined(js): for i in 0 ..< list.threads.len: let thread = list.threads[i] let isLastVisit = - i+1 < list.threads.len and list.threads[i].activity < list.lastVisit + i+1 < list.threads.len and + list.threads[i].activity < list.lastVisit let isNew = thread.creation < list.lastVisit genThread(thread, isNew, noBorder=isLastVisit or i+1 == list.threads.len) @@ -132,3 +165,8 @@ when defined(js): tr(class="load-more-separator"): td(colspan="6"): span(text "load more threads") + + proc renderThreadList*(): VNode = + result = buildHtml(tdiv): + genTopButtons() + genThreadList() \ No newline at end of file From 656b679a51d423f70a36679696b1a02ab3320283 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 9 May 2018 22:38:02 +0100 Subject: [PATCH 119/451] Load more button works. --- redesign/threadlist.nim | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/redesign/threadlist.nim b/redesign/threadlist.nim index 7149cec..d686567 100644 --- a/redesign/threadlist.nim +++ b/redesign/threadlist.nim @@ -39,10 +39,12 @@ when defined(js): type State = ref object list: Option[ThreadList] + loading: bool proc newState(): State = State( - list: none[ThreadList]() + list: none[ThreadList](), + loading: false ) var @@ -119,6 +121,7 @@ when defined(js): text renderActivity(thread.activity) proc onThreadList(httpStatus: int, response: kstring) = + state.loading = false let parsed = parseJson($response) let list = to(parsed, ThreadList) @@ -129,6 +132,11 @@ when defined(js): else: state.list = some(list) + proc onLoadMore(ev: Event, n: VNode) = + state.loading = true + let start = state.list.get().threads.len + ajaxGet(baseUrl & "threads.json?start=" & $start, @[], onThreadList) + proc genThreadList(): VNode = if state.list.isNone: ajaxGet(baseUrl & "threads.json", @[], onThreadList) @@ -163,8 +171,12 @@ when defined(js): if list.moreCount > 0: tr(class="load-more-separator"): - td(colspan="6"): - span(text "load more threads") + if state.loading: + td(colspan="6"): + tdiv(class="loading loading-lg") + else: + td(colspan="6", onClick=onLoadMore): + span(text "load more threads") proc renderThreadList*(): VNode = result = buildHtml(tdiv): From 40e948bcf8762f1b65d6b93c179610811c92c3d9 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 9 May 2018 22:41:25 +0100 Subject: [PATCH 120/451] Move karax redesign to /karax/ --- forum.nim | 10 +++++----- redesign/threadlist.nim | 7 ++----- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/forum.nim b/forum.nim index c9220c3..f69298f 100644 --- a/forum.nim +++ b/forum.nim @@ -1026,16 +1026,16 @@ routes: additionalHeaders = genRSSHeaders(c), showRssLinks = true) resp data - get "/karax.html": + get "/karax/": resp readFile("redesign/karax.html") - get "/nimforum.css": + get "/karax/nimforum.css": resp readFile("redesign/nimforum.css"), "text/css" - get "/nimcache/forum.js": + get "/karax/nimcache/forum.js": resp readFile("redesign/nimcache/forum.js"), "application/javascript" - get "/images/crown.png": + get "/karax/images/crown.png": resp readFile("redesign/images/crown.png"), "image/png" - get "/threads.json": + get "/karax/threads.json": var start = 0 count = 30 diff --git a/redesign/threadlist.nim b/redesign/threadlist.nim index d686567..396e7b2 100644 --- a/redesign/threadlist.nim +++ b/redesign/threadlist.nim @@ -27,9 +27,6 @@ type lastVisit*: int64 ## Unix timestamp moreCount*: int ## How many more threads are left -const - baseUrl = "http://localhost:5000/" - when defined(js): include karax/prelude import karax / [vstyles, kajax, kdom] @@ -135,11 +132,11 @@ when defined(js): proc onLoadMore(ev: Event, n: VNode) = state.loading = true let start = state.list.get().threads.len - ajaxGet(baseUrl & "threads.json?start=" & $start, @[], onThreadList) + ajaxGet("threads.json?start=" & $start, @[], onThreadList) proc genThreadList(): VNode = if state.list.isNone: - ajaxGet(baseUrl & "threads.json", @[], onThreadList) + ajaxGet("threads.json", @[], onThreadList) return buildHtml(tdiv(class="loading loading-lg")) From b2225dec34700f0469408c485d6dc05ad263cabc Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 10 May 2018 00:22:05 +0100 Subject: [PATCH 121/451] Implements navigation. --- redesign/forum.nim | 23 +++++++++++++++++++---- redesign/karaxutils.nim | 36 +++++++++++++++++++++++++++++++++++- redesign/threadlist.nim | 2 +- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/redesign/forum.nim b/redesign/forum.nim index a5ebbb9..93c0da2 100644 --- a/redesign/forum.nim +++ b/redesign/forum.nim @@ -1,4 +1,5 @@ import strformat, times, options, json +from dom import window, Location include karax/prelude @@ -7,9 +8,21 @@ import threadlist, karaxutils type State = ref object + url: Location proc newState(): State = - State() + State( + url: window.location + ) + +var state = newState() +proc onPopState(event: dom.Event) = + # This event is usually only called when the user moves back in their + # history. I fire it in karaxutils.anchorCB as well to ensure the URL is + # always updated. This should be moved into Karax in the future. + kout(kstring"New URL: ", window.location.href) + state.url = window.location + redraw() proc genHeader(): VNode = result = buildHtml(header(id="main-navbar")): @@ -27,11 +40,13 @@ proc genHeader(): VNode = italic(class="fas fa-sign-in-alt") text " Log in" -var state = newState() - proc render(): VNode = result = buildHtml(tdiv()): genHeader() - renderThreadList() + if "/t/" in state.url.pathname: + text "" + else: + renderThreadList() +window.onPopState = onPopState setRenderer render \ No newline at end of file diff --git a/redesign/karaxutils.nim b/redesign/karaxutils.nim index efda777..3949da0 100644 --- a/redesign/karaxutils.nim +++ b/redesign/karaxutils.nim @@ -1,5 +1,39 @@ +import strutils +import dom except window + +include karax/prelude +import karax / [kdom] + +const appName = "/karax/" + proc class*(classes: varargs[tuple[name: string, present: bool]], defaultClasses: string = ""): string = result = defaultClasses & " " for class in classes: - if class.present: result.add(class.name & " ") \ No newline at end of file + if class.present: result.add(class.name & " ") + +proc makeUri*(relative: string, appName=appName): string = + ## Concatenates ``relative`` to the current URL in a way that is sane. + var relative = relative + assert appName in $window.location.pathname + if relative[0] == '/': relative = relative[1..^1] + + return $window.location.protocol & "//" & + $window.location.host & + appName & + relative & + $window.location.search & + $window.location.hash + + +proc anchorCB*(e: kdom.Event, n: VNode) = # TODO: Why does this need disamb? + e.preventDefault() + + # TODO: Why does Karax have it's own Node type? That's just silly. + let url = cast[dom.Node](n.dom).getAttribute(cstring"href") + + # TODO: This was annoying. Karax also shouldn't have its own `window`. + dom.pushState(dom.window.history, 5, cstring"Thread", url) + + # Fire the popState event. + dom.window.dispatchEvent(newEvent("popstate")) \ No newline at end of file diff --git a/redesign/threadlist.nim b/redesign/threadlist.nim index 396e7b2..3d8bb9f 100644 --- a/redesign/threadlist.nim +++ b/redesign/threadlist.nim @@ -95,7 +95,7 @@ when defined(js): td(): if thread.isLocked: italic(class="fas fa-lock fa-xs") - text thread.topic + a(href=makeUri("/t/" & $thread.id), onClick=anchorCB): text thread.topic td(): if thread.category.id.len > 0: tdiv(class="triangle", From 366bdadc90da4a20c66fc72b0f08c0d240e2a77a Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 10 May 2018 16:05:42 +0100 Subject: [PATCH 122/451] Implements post list in front end and backend. --- forum.nim | 131 ++++++++++++++++++++++++++++------------ redesign/category.nim | 23 +++++++ redesign/error.nim | 17 ++++++ redesign/forum.nim | 4 +- redesign/postlist.nim | 117 +++++++++++++++++++++++++++++++++++ redesign/threadlist.nim | 45 ++++++++------ utils.nim | 5 ++ 7 files changed, 283 insertions(+), 59 deletions(-) create mode 100644 redesign/category.nim create mode 100644 redesign/error.nim create mode 100644 redesign/postlist.nim diff --git a/forum.nim b/forum.nim index f69298f..4bd503d 100644 --- a/forum.nim +++ b/forum.nim @@ -12,6 +12,7 @@ import parseutils, utils, random, rst, ranks, recaptcha, json import redesign/threadlist except User +import redesign/[category, postlist] when not defined(windows): import bcrypt # TODO @@ -1013,6 +1014,47 @@ template createTFD() = if request.cookies.len > 0: checkLoggedIn(c) +#[ DB functions. TODO: Move to another module? ]# + +proc selectUser(userRow: seq[string]): threadlist.User = + let isOnline = getTime().toUnix() - userRow[2].parseInt > (60*5) + return threadlist.User( + name: userRow[0], + avatarUrl: userRow[1].getGravatarUrl(), + isOnline: isOnline + ) + +proc selectThread(threadRow: seq[string]): Thread = + const postsQuery = + sql"""select count(*), strftime('%s', creation) from post + where thread = ? + order by creation asc limit 1;""" + const usersListQuery = + sql"""select distinct name, email, strftime('%s', lastOnline) + from person where id in + (select author from post where thread = ?) + limit 5;""" # TODO: Order by most posts. + + let posts = getRow(db, postsQuery, threadRow[0]) + + var thread = Thread( + id: threadRow[0].parseInt, + topic: threadRow[1], + category: Category(id: "", color: "#ff0000"), # TODO + users: @[], + replies: posts[0].parseInt, + views: threadRow[2].parseInt, + activity: threadRow[3].parseInt, + creation: posts[1].parseInt, + isLocked: false, + isSolved: false # TODO: ^ and this. Add a field to `post` to identify. + ) + + # Gather the users list. + for user in getAllRows(db, usersListQuery, thread.id): + thread.users.add(selectUser(user)) + + return thread initialise() @@ -1037,56 +1079,71 @@ routes: get "/karax/threads.json": var - start = 0 - count = 30 - parseInt(@"start", start, 0..1_000_000) - parseInt(@"count", start, 0..1_000_000) + start = getInt(@"start", 0) + count = getInt(@"count", 30) const threadsQuery = sql"""select id, name, views, strftime('%s', modified) from thread - order by modified desc limit ?, ?;""" - const postsQuery = - sql"""select count(*), strftime('%s', creation) from post - where thread = ? - order by creation asc limit 1;""" - const usersListQuery = - sql"""select distinct name, email, strftime('%s', lastOnline) - from person where id in - (select author from post where thread = ?) - limit 5;""" # TODO: Order by most posts. + order by modified desc limit ?, ?;""" # TODO: Moderation let thrCount = getValue(db, sql"select count(*) from thread;").parseInt() let moreCount = max(0, thrCount - (start + count)) var list = ThreadList(threads: @[], lastVisit: 0, moreCount: moreCount) for data in getAllRows(db, threadsQuery, start, count): - let posts = getRow(db, postsQuery, data[0]) - - var thread = Thread( - id: data[0].parseInt, - topic: data[1], - category: Category(id: "", color: "#ff0000"), # TODO - users: @[], - replies: posts[0].parseInt, - views: data[2].parseInt, - activity: data[3].parseInt, - creation: posts[1].parseInt, - isLocked: false, - isSolved: false # TODO: ^ and this. Add a field to `post` to identify. - ) - - # Gather the users list. - for user in getAllRows(db, usersListQuery, thread.id): - let isOnline = getTime().toUnix() - user[2].parseInt > (60*5) - thread.users.add(threadlist.User( - name: user[0], - avatarUrl: user[1].getGravatarUrl(), - isOnline: isOnline - )) + let thread = selectThread(data) list.threads.add(thread) resp $(%list), "application/json" + get "/karax/posts.json": + createTFD() + var + id = getInt(@"id", -1) + start = getInt(@"start", 0) + count = getInt(@"count", 5) + cond id != -1 + + const threadsQuery = + sql"""select id, name, views, strftime('%s', modified) from thread + where id = ?;""" + + let threadRow = getRow(db, threadsQuery, id) + let thread = selectThread(threadRow) + + let modClause = + if c.rank >= Moderator: + "(1 or u.id = ?)" + else: + "(u.status <> 'Moderated' or p.author = ?)" + let postsQuery = + sql( + """select p.id, p.content, strftime('%s', p.creation), p.author, + u.name, u.email, strftime('%s', u.lastOnline) + from post p, person u + where u.id = p.author and p.thread = ? and $# + and (u.status <> 'Spammer' or p.author = ?) + order by p.id limit ?, ?""" % modClause + ) + + var list = PostList(posts: @[], history: @[], thread: thread) + for post in getAllRows(db, postsQuery, id, c.userId, c.userId, + start, count): + list.posts.add(Post( + id: post[0].parseInt, + author: selectUser(@[post[4], post[5], post[6]]), + likes: @[], # TODO: + seen: false, # TODO: + history: @[], # TODO: + info: PostInfo( + creation: post[2].parseInt, + content: post[1] + ) + )) + + resp $(%list), "application/json" + + get "/threadActivity.xml": createTFD() c.isThreadsList = true diff --git a/redesign/category.nim b/redesign/category.nim new file mode 100644 index 0000000..70c1293 --- /dev/null +++ b/redesign/category.nim @@ -0,0 +1,23 @@ + +type + Category* = object + id*: string + color*: string + + +when defined(js): + include karax/prelude + import karax / [vstyles, kajax, kdom] + + import karaxutils + + proc render*(category: Category): VNode = + result = buildHtml(): + if category.id.len > 0: + tdiv(class="triangle", + style=style( + (StyleAttr.borderBottom, kstring"0.6rem solid " & category.color) + )): + text category.id + else: + span() \ No newline at end of file diff --git a/redesign/error.nim b/redesign/error.nim new file mode 100644 index 0000000..a670804 --- /dev/null +++ b/redesign/error.nim @@ -0,0 +1,17 @@ +include karax/prelude +import karax / [vstyles, kajax, kdom] + + +proc renderError*(message: string): VNode = + result = buildHtml(): + tdiv(class="empty error"): + tdiv(class="empty icon"): + italic(class="fas fa-bug fa-5x") + p(class="empty-title h5"): + text message + p(class="empty-subtitle"): + text "Please report this issue to us so we can fix it!" + tdiv(class="empty-action"): + a(href="https://github.com/nim-lang/nimforum/issues", target="_blank"): + button(class="btn btn-primary"): + text "Report issue" \ No newline at end of file diff --git a/redesign/forum.nim b/redesign/forum.nim index 93c0da2..f24cfc7 100644 --- a/redesign/forum.nim +++ b/redesign/forum.nim @@ -4,7 +4,7 @@ from dom import window, Location include karax/prelude -import threadlist, karaxutils +import threadlist, postlist, karaxutils type State = ref object @@ -44,7 +44,7 @@ proc render(): VNode = result = buildHtml(tdiv()): genHeader() if "/t/" in state.url.pathname: - text "" + renderPostList(3806, false) else: renderThreadList() diff --git a/redesign/postlist.nim b/redesign/postlist.nim new file mode 100644 index 0000000..33ed677 --- /dev/null +++ b/redesign/postlist.nim @@ -0,0 +1,117 @@ + +import options, json, times, httpcore, strformat + +import threadlist, category +type + PostInfo* = object + creation*: int64 + content*: string + + Post* = object + id*: int + author*: User + likes*: seq[User] ## Users that liked this post. + seen*: bool ## Determines whether the current user saw this post. + ## I considered using a simple timestamp for each thread, + ## but that wouldn't work when a user navigates to the last + ## post in a thread for example. + history*: seq[PostInfo] ## If the post was edited this will contain the + ## older versions of the post. + info*: PostInfo + + PostList* = ref object + thread*: Thread + history*: seq[Thread] ## If the thread was edited this will contain the + ## older versions of the thread (title/category + ## changes). + posts*: seq[Post] + +when defined(js): + include karax/prelude + import karax / [vstyles, kajax, kdom] + + import karaxutils, error + + type + State = ref object + list: Option[PostList] + loading: bool + status: HttpCode + + proc newState(): State = + State( + list: none[PostList](), + loading: false, + status: Http200 + ) + + var + state = newState() + + proc onPostList(httpStatus: int, response: kstring) = + state.loading = false + state.status = httpStatus.HttpCode + if state.status != Http200: return + + let parsed = parseJson($response) + let list = to(parsed, PostList) + + if state.list.isSome: + state.list.get().posts.add(list.posts) + # TODO: Incorporate other possible changes? + else: + state.list = some(list) + + proc renderPostUrl(post: Post, thread: Thread): string = + makeUri(fmt"/t/{thread.id}/p/{post.id}") + + proc genPost(post: Post, thread: Thread, isLoggedIn: bool): VNode = + result = buildHtml(): + tdiv(class="post"): + tdiv(class="post-icon"): + render(post.author, "post-avatar") + tdiv(class="post-main"): + tdiv(class="post-title"): + tdiv(class="post-username"): + text post.author.name + tdiv(class="post-time"): + let title = post.info.creation.fromUnix().local. + format("MMM d, yyyy HH:mm") + a(href=renderPostUrl(post, thread), title=title): + text renderActivity(post.info.creation) + tdiv(class="post-content"): + p(text post.info.content) # TODO: RSTGEN + tdiv(class="post-buttons"): + tdiv(class="like-button"): + button(class="btn"): + span(class="like-count"): + if post.likes.len > 0: + text $post.likes.len + italic(class="far fa-heart") + if isLoggedIn: + tdiv(class="flag-button"): + button(class="btn"): + italic(class="far fa-flag") + tdiv(class="reply-button"): + button(class="btn"): + italic(class="fas fa-reply") + text " Reply" + + proc renderPostList*(threadId: int, isLoggedIn: bool): VNode = + if state.status != Http200: + return renderError("Couldn't retrieve posts.") + + if state.list.isNone: + ajaxGet(makeUri("posts.json?id=" & $threadId), @[], onPostList) + + return buildHtml(tdiv(class="loading loading-lg")) + + let list = state.list.get() + result = buildHtml(): + section(class="container grid-xl"): + tdiv(class="title"): + p(): text list.thread.topic + render(list.thread.category) + tdiv(class="posts"): + for post in list.posts: + genPost(post, list.thread, isLoggedIn) \ No newline at end of file diff --git a/redesign/threadlist.nim b/redesign/threadlist.nim index 3d8bb9f..3dfab50 100644 --- a/redesign/threadlist.nim +++ b/redesign/threadlist.nim @@ -1,4 +1,6 @@ -import strformat, times, options, json +import strformat, times, options, json, httpcore + +import category type User* = object @@ -6,10 +8,6 @@ type avatarUrl*: string isOnline*: bool - Category* = object - id*: string - color*: string - Thread* = object id*: int topic*: string @@ -31,17 +29,19 @@ when defined(js): include karax/prelude import karax / [vstyles, kajax, kdom] - import karaxutils + import karaxutils, error type State = ref object list: Option[ThreadList] loading: bool + status: HttpCode proc newState(): State = State( list: none[ThreadList](), - loading: false + loading: false, + status: Http200 ) var @@ -63,16 +63,20 @@ when defined(js): button(class="btn btn-link"): text "Categories" section(class="navbar-section") + proc render*(user: User, class: string): VNode = + result = buildHtml(): + figure(class=class): + img(src=user.avatarUrl, title=user.name) + if user.isOnline: + italic(class="avatar-presense online") + proc genUserAvatars(users: seq[User]): VNode = result = buildHtml(td): for user in users: - figure(class="avatar avatar-sm"): - img(src=user.avatarUrl, title=user.name) - if user.isOnline: - italic(class="avatar-presense online") + render(user, "avatar avatar-sm") text " " - proc renderActivity(activity: int64): string = + proc renderActivity*(activity: int64): string = let currentTime = getTime() let activityTime = fromUnix(activity) let duration = currentTime - activityTime @@ -97,12 +101,7 @@ when defined(js): italic(class="fas fa-lock fa-xs") a(href=makeUri("/t/" & $thread.id), onClick=anchorCB): text thread.topic td(): - if thread.category.id.len > 0: - tdiv(class="triangle", - style=style( - (StyleAttr.borderBottom, kstring"0.6rem solid " & thread.category.color) - )): - text thread.category.id + render(thread.category) genUserAvatars(thread.users) td(): text $thread.replies td(class=class({ @@ -119,6 +118,9 @@ when defined(js): proc onThreadList(httpStatus: int, response: kstring) = state.loading = false + state.status = httpStatus.HttpCode + if state.status != Http200: return + let parsed = parseJson($response) let list = to(parsed, ThreadList) @@ -132,11 +134,14 @@ when defined(js): proc onLoadMore(ev: Event, n: VNode) = state.loading = true let start = state.list.get().threads.len - ajaxGet("threads.json?start=" & $start, @[], onThreadList) + ajaxGet(makeUri("threads.json?start=" & $start), @[], onThreadList) proc genThreadList(): VNode = + if state.status != Http200: + return renderError("Couldn't retrieve threads.") + if state.list.isNone: - ajaxGet("threads.json", @[], onThreadList) + ajaxGet(makeUri("threads.json"), @[], onThreadList) return buildHtml(tdiv(class="loading loading-lg")) diff --git a/utils.nim b/utils.nim index f30ad60..ca0e992 100644 --- a/utils.nim +++ b/utils.nim @@ -15,6 +15,11 @@ proc parseInt*(s: string, value: var int, validRange: Slice[int]) {. discard if x in validRange: value = x +proc getInt*(s: string, default = 0): int = + ## Safely parses an int and returns it. + result = default + parseInt(s, result, 0..1_000_000_000) + type Config* = object smtpAddress: string From cf37fa34c406b2460ad92b96a9d325c131191177 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 10 May 2018 16:24:11 +0100 Subject: [PATCH 123/451] Small style and other fixes. --- forum.nim | 2 +- redesign/forum.nim | 2 +- redesign/nimforum.scss | 20 ++++++++++++++++++-- redesign/threadlist.nim | 2 +- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/forum.nim b/forum.nim index 4bd503d..ef97f85 100644 --- a/forum.nim +++ b/forum.nim @@ -1042,7 +1042,7 @@ proc selectThread(threadRow: seq[string]): Thread = topic: threadRow[1], category: Category(id: "", color: "#ff0000"), # TODO users: @[], - replies: posts[0].parseInt, + replies: posts[0].parseInt-1, views: threadRow[2].parseInt, activity: threadRow[3].parseInt, creation: posts[1].parseInt, diff --git a/redesign/forum.nim b/redesign/forum.nim index f24cfc7..8aaa425 100644 --- a/redesign/forum.nim +++ b/redesign/forum.nim @@ -28,7 +28,7 @@ proc genHeader(): VNode = result = buildHtml(header(id="main-navbar")): tdiv(class="navbar container grid-xl"): section(class="navbar-section"): - a(href="/"): + a(href=makeUri("/")): img(src="images/crown.png", id="img-logo") # TODO: Customisation. section(class="navbar-section"): tdiv(class="input-group input-inline"): diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index 7ed37ed..80fe4c1 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -94,6 +94,17 @@ $logo-height: $navbar-height - 20px; } // - Thread table +.thread-title { + a, a:visited, a:hover { + color: $body-font-color; + text-decoration: none; + } + + a.visited { + color: lighten($body-font-color, 40%); + } +} + $super-popular-color: #f86713; $popular-color: darken($super-popular-color, 25%); $views-color: #545d70; @@ -212,7 +223,12 @@ $views-color: #545d70; .post-title { margin-bottom: $control-padding-y*2; - color: lighten($body-font-color, 20%); + + &, a, a:visited, a:hover { + color: lighten($body-font-color, 20%); + text-decoration: none; + } + .post-username { font-weight: bold; display: inline-block; @@ -253,7 +269,7 @@ $views-color: #545d70; box-shadow: inset 0 0 .4rem .01rem darken($secondary-btn-color, 80%); } - .like-button i { + .like-button i:hover, .like-button i.fas { color: #f783ac; } } diff --git a/redesign/threadlist.nim b/redesign/threadlist.nim index 3dfab50..58094be 100644 --- a/redesign/threadlist.nim +++ b/redesign/threadlist.nim @@ -96,7 +96,7 @@ when defined(js): proc genThread(thread: Thread, isNew: bool, noBorder: bool): VNode = result = buildHtml(): tr(class=class({"no-border": noBorder})): - td(): + td(class="thread-title"): if thread.isLocked: italic(class="fas fa-lock fa-xs") a(href=makeUri("/t/" & $thread.id), onClick=anchorCB): text thread.topic From 4176bdee3cd199e0bec1f2146f6a5183cc7dbc4a Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 10 May 2018 18:21:12 +0100 Subject: [PATCH 124/451] Use Jester's pattern matcher for simple routing. --- forum.nim | 4 ++-- redesign/forum.nim | 31 +++++++++++++++++++++++++------ redesign/postlist.nim | 4 ++-- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/forum.nim b/forum.nim index ef97f85..025e109 100644 --- a/forum.nim +++ b/forum.nim @@ -1046,8 +1046,8 @@ proc selectThread(threadRow: seq[string]): Thread = views: threadRow[2].parseInt, activity: threadRow[3].parseInt, creation: posts[1].parseInt, - isLocked: false, - isSolved: false # TODO: ^ and this. Add a field to `post` to identify. + isLocked: false, # TODO: + isSolved: false # TODO: Add a field to `post` to identify the solution. ) # Gather the users list. diff --git a/redesign/forum.nim b/redesign/forum.nim index 8aaa425..efa4780 100644 --- a/redesign/forum.nim +++ b/redesign/forum.nim @@ -1,8 +1,8 @@ -import strformat, times, options, json +import strformat, times, options, json, tables, future from dom import window, Location include karax/prelude - +import jester/patterns import threadlist, postlist, karaxutils @@ -40,13 +40,32 @@ proc genHeader(): VNode = italic(class="fas fa-sign-in-alt") text " Log in" +const appName = "/karax" +type Params = Table[string, string] +type + Route = object + n: string + p: proc (params: Params): VNode + +proc r(n: string, p: proc (params: Params): VNode): Route = Route(n: n, p: p) +proc route(routes: openarray[Route]): VNode = + for route in routes: + let pattern = (appName & route.n).parsePattern() + let (matched, params) = pattern.match($state.url.pathname) + if matched: + return route.p(params) + proc render(): VNode = result = buildHtml(tdiv()): genHeader() - if "/t/" in state.url.pathname: - renderPostList(3806, false) - else: - renderThreadList() + route([ + r("/t/@id?", + (params: Params) => + (kout(params["id"].cstring); + renderPostList(params["id"].parseInt(), false)) + ), + r("/", (params: Params) => renderThreadList()) + ]) window.onPopState = onPopState setRenderer render \ No newline at end of file diff --git a/redesign/postlist.nim b/redesign/postlist.nim index 33ed677..52978bf 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -56,7 +56,7 @@ when defined(js): let parsed = parseJson($response) let list = to(parsed, PostList) - if state.list.isSome: + if state.list.isSome and state.list.get().thread.id == list.thread.id: state.list.get().posts.add(list.posts) # TODO: Incorporate other possible changes? else: @@ -101,7 +101,7 @@ when defined(js): if state.status != Http200: return renderError("Couldn't retrieve posts.") - if state.list.isNone: + if state.list.isNone or state.list.get().thread.id != threadId: ajaxGet(makeUri("posts.json?id=" & $threadId), @[], onPostList) return buildHtml(tdiv(class="loading loading-lg")) From 19a9f24d3d5de9ccb595f75ec8f7edabecf04780 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 10 May 2018 19:07:53 +0100 Subject: [PATCH 125/451] Implements loading of more posts. --- forum.nim | 13 +++++++++++- redesign/karaxutils.nim | 12 +++++++++++ redesign/nimforum.scss | 11 ++++++++++ redesign/postlist.nim | 46 +++++++++++++++++++++++++++++++++++------ 4 files changed, 75 insertions(+), 7 deletions(-) diff --git a/forum.nim b/forum.nim index 025e109..7f4929b 100644 --- a/forum.nim +++ b/forum.nim @@ -1126,7 +1126,18 @@ routes: order by p.id limit ?, ?""" % modClause ) - var list = PostList(posts: @[], history: @[], thread: thread) + let pstCount = getValue( + db, + sql"select count(*) from post where thread = ?;", + id + ).parseInt() + let moreCount = max(0, pstCount - (start + count)) + + var list = PostList( + posts: @[], + history: @[], + thread: thread, + moreCount: moreCount) for post in getAllRows(db, postsQuery, id, c.userId, c.userId, start, count): list.posts.add(Post( diff --git a/redesign/karaxutils.nim b/redesign/karaxutils.nim index 3949da0..c170a97 100644 --- a/redesign/karaxutils.nim +++ b/redesign/karaxutils.nim @@ -25,6 +25,18 @@ proc makeUri*(relative: string, appName=appName): string = $window.location.search & $window.location.hash +proc makeUri*(relative: string, params: varargs[(string, string)], + appName=appName): string = + var query = "" + for i in 0 ..< params.len: + let param = params[i] + if i != 0: query.add("&") + query.add(param[0] & "=" & param[1]) + + if query.len > 0: + makeUri(relative & "?" & query, appName) + else: + makeUri(relative, appName) proc anchorCB*(e: kdom.Event, n: VNode) = # TODO: Why does this need disamb? e.preventDefault() diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index 80fe4c1..00925d6 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -369,4 +369,15 @@ blockquote { text-transform: uppercase; font-weight: bold; cursor: pointer; + + .information-main { + width: 100%; + text-align: left; + } + + .more-post-count { + color: rgba(darken($label-color, 35%), 0.5); + margin-right: $control-padding-x*2; + float: right; + } } \ No newline at end of file diff --git a/redesign/postlist.nim b/redesign/postlist.nim index 52978bf..226af98 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -1,5 +1,5 @@ -import options, json, times, httpcore, strformat +import options, json, times, httpcore, strformat, sugar import threadlist, category type @@ -25,6 +25,7 @@ type ## older versions of the thread (title/category ## changes). posts*: seq[Post] + moreCount*: int when defined(js): include karax/prelude @@ -48,7 +49,7 @@ when defined(js): var state = newState() - proc onPostList(httpStatus: int, response: kstring) = + proc onPostList(httpStatus: int, response: kstring, start: int) = state.loading = false state.status = httpStatus.HttpCode if state.status != Http200: return @@ -57,8 +58,12 @@ when defined(js): let list = to(parsed, PostList) if state.list.isSome and state.list.get().thread.id == list.thread.id: - state.list.get().posts.add(list.posts) - # TODO: Incorporate other possible changes? + var old = state.list.get() + for i in 0.. onPostList(s, r, start)) + + proc genLoadMore(start: int): VNode = + result = buildHtml(): + tdiv(class="information load-more-posts", + onClick=onLoadMore, + "data-start" = $start): + tdiv(class="information-icon"): + italic(class="fas fa-comment-dots") + tdiv(class="information-main"): + if state.loading: + tdiv(class="loading loading-lg") + else: + tdiv(class="information-title"): + text "Load more posts " + span(class="more-post-count"): + text "(" & $state.list.get().moreCount & ")" + proc renderPostList*(threadId: int, isLoggedIn: bool): VNode = if state.status != Http200: return renderError("Couldn't retrieve posts.") if state.list.isNone or state.list.get().thread.id != threadId: - ajaxGet(makeUri("posts.json?id=" & $threadId), @[], onPostList) + let uri = makeUri("posts.json", ("id", $threadId)) + ajaxGet(uri, @[], (s: int, r: kstring) => onPostList(s, r, 0)) return buildHtml(tdiv(class="loading loading-lg")) @@ -114,4 +145,7 @@ when defined(js): render(list.thread.category) tdiv(class="posts"): for post in list.posts: - genPost(post, list.thread, isLoggedIn) \ No newline at end of file + genPost(post, list.thread, isLoggedIn) + + if list.moreCount > 0: + genLoadMore(list.posts.len) \ No newline at end of file From e790e8ac575823afccd8fec1cb0942393243f2af Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 10 May 2018 19:13:42 +0100 Subject: [PATCH 126/451] Allow refreshes on other URLs too. --- forum.nim | 7 ++++--- redesign/karax.html | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/forum.nim b/forum.nim index 7f4929b..e5e45f9 100644 --- a/forum.nim +++ b/forum.nim @@ -9,7 +9,7 @@ import os, strutils, times, md5, strtabs, cgi, math, db_sqlite, scgi, jester, asyncdispatch, asyncnet, cache, sequtils, - parseutils, utils, random, rst, ranks, recaptcha, json + parseutils, utils, random, rst, ranks, recaptcha, json, re import redesign/threadlist except User import redesign/[category, postlist] @@ -1068,8 +1068,6 @@ routes: additionalHeaders = genRSSHeaders(c), showRssLinks = true) resp data - get "/karax/": - resp readFile("redesign/karax.html") get "/karax/nimforum.css": resp readFile("redesign/nimforum.css"), "text/css" get "/karax/nimcache/forum.js": @@ -1077,6 +1075,7 @@ routes: get "/karax/images/crown.png": resp readFile("redesign/images/crown.png"), "image/png" + get "/karax/threads.json": var start = getInt(@"start", 0) @@ -1154,6 +1153,8 @@ routes: resp $(%list), "application/json" + get re"/karax/(.+)?": + resp readFile("redesign/karax.html") get "/threadActivity.xml": createTFD() diff --git a/redesign/karax.html b/redesign/karax.html index 054116d..24242d2 100644 --- a/redesign/karax.html +++ b/redesign/karax.html @@ -8,6 +8,8 @@ The Nim programming language forum + + From 4c0a88a1316174e02b16420f83401dc40a2db820 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 10 May 2018 23:33:48 +0100 Subject: [PATCH 127/451] Create login modal. --- redesign/forum.nim | 58 ++++++++++++++++++++++++++++++++---------- redesign/nimforum.scss | 6 +++-- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/redesign/forum.nim b/redesign/forum.nim index efa4780..219d75a 100644 --- a/redesign/forum.nim +++ b/redesign/forum.nim @@ -25,20 +25,50 @@ proc onPopState(event: dom.Event) = redraw() proc genHeader(): VNode = - result = buildHtml(header(id="main-navbar")): - tdiv(class="navbar container grid-xl"): - section(class="navbar-section"): - a(href=makeUri("/")): - img(src="images/crown.png", id="img-logo") # TODO: Customisation. - section(class="navbar-section"): - tdiv(class="input-group input-inline"): - input(class="form-input input-sm", `type`="text", placeholder="search") - button(class="btn btn-primary btn-sm"): - italic(class="fas fa-user-plus") - text " Sign up" - button(class="btn btn-primary btn-sm"): - italic(class="fas fa-sign-in-alt") - text " Log in" + result = buildHtml(tdiv()): + header(id="main-navbar"): + tdiv(class="navbar container grid-xl"): + section(class="navbar-section"): + a(href=makeUri("/")): + img(src="images/crown.png", id="img-logo") # TODO: Customisation. + section(class="navbar-section"): + tdiv(class="input-group input-inline"): + input(class="search-input input-sm", `type`="text", placeholder="search") + button(class="btn btn-primary btn-sm"): + italic(class="fas fa-user-plus") + text " Sign up" + a(href="#login-modal"): + button(class="btn btn-primary btn-sm"): + italic(class="fas fa-sign-in-alt") + text " Log in" + + # Modals + tdiv(class="modal modal-sm", id="login-modal"): + a(href="#", class="modal-overlay", "aria-label"="close") + tdiv(class="modal-container"): + tdiv(class="modal-header"): + a(href="#", class="btn btn-clear float-right", "aria-label"="close") + tdiv(class="modal-title h5"): + text "Log in" + tdiv(class="modal-body"): + tdiv(class="content"): + form(): + tdiv(class="form-group"): + label(class="form-label", `for`="username"): + text "Username" + input(class="form-input", `type`="text", id="username") + tdiv(class="form-group"): + label(class="form-label", `for`="password"): + text "Password" + input(class="form-input", `type`="password", id="password") + button(class="btn btn-link"): + text "Reset your password" + tdiv(class="modal-footer"): + button(class="btn btn-primary"): + text "Log in" + button(class="btn"): + text "Create account" + const appName = "/karax" type Params = Table[string, string] diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index 00925d6..a819796 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -60,12 +60,14 @@ $logo-height: $navbar-height - 20px; } // Unfortunately we must colour the controls in the navbar manually. - .form-input { + .search-input { + @extend .form-input; border-color: $navbar-border-color-dark; } - .form-input:focus { + .search-input:focus { box-shadow: none; + border-color: $navbar-border-color-dark; } .btn-primary { From 29eb22cf9c2a3018225151f440d460f3b251d2f9 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 11 May 2018 00:32:01 +0100 Subject: [PATCH 128/451] Implements sign up form. --- redesign/forum.nim | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/redesign/forum.nim b/redesign/forum.nim index 219d75a..f0fda2f 100644 --- a/redesign/forum.nim +++ b/redesign/forum.nim @@ -34,10 +34,11 @@ proc genHeader(): VNode = section(class="navbar-section"): tdiv(class="input-group input-inline"): input(class="search-input input-sm", `type`="text", placeholder="search") - button(class="btn btn-primary btn-sm"): - italic(class="fas fa-user-plus") - text " Sign up" - a(href="#login-modal"): + a(href="#signup-modal", id="signup-btn"): + button(class="btn btn-primary btn-sm"): + italic(class="fas fa-user-plus") + text " Sign up" + a(href="#login-modal", id="login-btn"): button(class="btn btn-primary btn-sm"): italic(class="fas fa-sign-in-alt") text " Log in" @@ -66,8 +67,38 @@ proc genHeader(): VNode = tdiv(class="modal-footer"): button(class="btn btn-primary"): text "Log in" - button(class="btn"): + a(href="#signup-modal"): + button(class="btn"): + text "Create account" + + tdiv(class="modal", id="signup-modal"): + a(href="#", class="modal-overlay", "aria-label"="close") + tdiv(class="modal-container"): + tdiv(class="modal-header"): + a(href="#", class="btn btn-clear float-right", "aria-label"="close") + tdiv(class="modal-title h5"): + text "Create a new account" + tdiv(class="modal-body"): + tdiv(class="content"): + form(): + tdiv(class="form-group"): + label(class="form-label", `for`="email"): + text "Email" + input(class="form-input", `type`="text", id="email") + tdiv(class="form-group"): + label(class="form-label", `for`="username"): + text "Username" + input(class="form-input", `type`="text", id="username") + tdiv(class="form-group"): + label(class="form-label", `for`="password"): + text "Password" + input(class="form-input", `type`="password", id="password") + tdiv(class="modal-footer"): + button(class="btn btn-primary"): text "Create account" + a(href="#login-modal"): + button(class="btn"): + text "Log in" const appName = "/karax" From c0bbce53e94f8dc71a127a949420942bb1afe5ae Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 11 May 2018 13:53:26 +0100 Subject: [PATCH 129/451] Implements logging in. --- forum.nim | 34 ++++++++- redesign/error.nim | 35 +++++---- redesign/forum.nim | 84 +-------------------- redesign/header.nim | 157 ++++++++++++++++++++++++++++++++++++++++ redesign/karaxutils.nim | 10 ++- utils.nim | 7 +- 6 files changed, 228 insertions(+), 99 deletions(-) create mode 100644 redesign/header.nim diff --git a/forum.nim b/forum.nim index e5e45f9..b353ddd 100644 --- a/forum.nim +++ b/forum.nim @@ -7,12 +7,14 @@ # import - os, strutils, times, md5, strtabs, cgi, math, db_sqlite, + os, strutils, times, md5, strtabs, math, db_sqlite, scgi, jester, asyncdispatch, asyncnet, cache, sequtils, parseutils, utils, random, rst, ranks, recaptcha, json, re +import cgi except setCookie +import options import redesign/threadlist except User -import redesign/[category, postlist] +import redesign/[category, postlist, error, header] when not defined(windows): import bcrypt # TODO @@ -1153,6 +1155,34 @@ routes: resp $(%list), "application/json" + post "/karax/login": + createTFD() + let formData = request.formData + if login(c, formData["username"].body, formData["password"].body): + setCookie("sid", c.userpass) + resp Http200, "{}", "application/json" + else: + let err = PostError( + errorFields: @["username", "password"], + message: "Invalid username or password" + ) + resp $(%err), "application/json" + + get "/karax/status.json": + createTFD() + let user = + if c.loggedIn(): + some(threadlist.User( + name: c.username, + avatarUrl: c.email.getGravatarUrl(), + isOnline: true + )) + else: + none[threadlist.User]() + + let status = UserStatus(user: user) + resp $(%status), "application/json" + get re"/karax/(.+)?": resp readFile("redesign/karax.html") diff --git a/redesign/error.nim b/redesign/error.nim index a670804..108adbe 100644 --- a/redesign/error.nim +++ b/redesign/error.nim @@ -1,17 +1,22 @@ -include karax/prelude -import karax / [vstyles, kajax, kdom] +type + PostError* = object + errorFields*: seq[string] ## IDs of the fields with an error. + message*: string +when defined(js): + include karax/prelude + import karax / [vstyles, kajax, kdom] -proc renderError*(message: string): VNode = - result = buildHtml(): - tdiv(class="empty error"): - tdiv(class="empty icon"): - italic(class="fas fa-bug fa-5x") - p(class="empty-title h5"): - text message - p(class="empty-subtitle"): - text "Please report this issue to us so we can fix it!" - tdiv(class="empty-action"): - a(href="https://github.com/nim-lang/nimforum/issues", target="_blank"): - button(class="btn btn-primary"): - text "Report issue" \ No newline at end of file + proc renderError*(message: string): VNode = + result = buildHtml(): + tdiv(class="empty error"): + tdiv(class="empty icon"): + italic(class="fas fa-bug fa-5x") + p(class="empty-title h5"): + text message + p(class="empty-subtitle"): + text "Please report this issue to us so we can fix it!" + tdiv(class="empty-action"): + a(href="https://github.com/nim-lang/nimforum/issues", target="_blank"): + button(class="btn btn-primary"): + text "Report issue" \ No newline at end of file diff --git a/redesign/forum.nim b/redesign/forum.nim index f0fda2f..637fcf4 100644 --- a/redesign/forum.nim +++ b/redesign/forum.nim @@ -1,10 +1,11 @@ -import strformat, times, options, json, tables, future +import strformat, times, options, json, tables, sugar from dom import window, Location include karax/prelude import jester/patterns -import threadlist, postlist, karaxutils +import threadlist, postlist, header +import karaxutils type State = ref object @@ -24,83 +25,6 @@ proc onPopState(event: dom.Event) = state.url = window.location redraw() -proc genHeader(): VNode = - result = buildHtml(tdiv()): - header(id="main-navbar"): - tdiv(class="navbar container grid-xl"): - section(class="navbar-section"): - a(href=makeUri("/")): - img(src="images/crown.png", id="img-logo") # TODO: Customisation. - section(class="navbar-section"): - tdiv(class="input-group input-inline"): - input(class="search-input input-sm", `type`="text", placeholder="search") - a(href="#signup-modal", id="signup-btn"): - button(class="btn btn-primary btn-sm"): - italic(class="fas fa-user-plus") - text " Sign up" - a(href="#login-modal", id="login-btn"): - button(class="btn btn-primary btn-sm"): - italic(class="fas fa-sign-in-alt") - text " Log in" - - # Modals - tdiv(class="modal modal-sm", id="login-modal"): - a(href="#", class="modal-overlay", "aria-label"="close") - tdiv(class="modal-container"): - tdiv(class="modal-header"): - a(href="#", class="btn btn-clear float-right", "aria-label"="close") - tdiv(class="modal-title h5"): - text "Log in" - tdiv(class="modal-body"): - tdiv(class="content"): - form(): - tdiv(class="form-group"): - label(class="form-label", `for`="username"): - text "Username" - input(class="form-input", `type`="text", id="username") - tdiv(class="form-group"): - label(class="form-label", `for`="password"): - text "Password" - input(class="form-input", `type`="password", id="password") - button(class="btn btn-link"): - text "Reset your password" - tdiv(class="modal-footer"): - button(class="btn btn-primary"): - text "Log in" - a(href="#signup-modal"): - button(class="btn"): - text "Create account" - - tdiv(class="modal", id="signup-modal"): - a(href="#", class="modal-overlay", "aria-label"="close") - tdiv(class="modal-container"): - tdiv(class="modal-header"): - a(href="#", class="btn btn-clear float-right", "aria-label"="close") - tdiv(class="modal-title h5"): - text "Create a new account" - tdiv(class="modal-body"): - tdiv(class="content"): - form(): - tdiv(class="form-group"): - label(class="form-label", `for`="email"): - text "Email" - input(class="form-input", `type`="text", id="email") - tdiv(class="form-group"): - label(class="form-label", `for`="username"): - text "Username" - input(class="form-input", `type`="text", id="username") - tdiv(class="form-group"): - label(class="form-label", `for`="password"): - text "Password" - input(class="form-input", `type`="password", id="password") - tdiv(class="modal-footer"): - button(class="btn btn-primary"): - text "Create account" - a(href="#login-modal"): - button(class="btn"): - text "Log in" - - const appName = "/karax" type Params = Table[string, string] type @@ -118,7 +42,7 @@ proc route(routes: openarray[Route]): VNode = proc render(): VNode = result = buildHtml(tdiv()): - genHeader() + renderHeader() route([ r("/t/@id?", (params: Params) => diff --git a/redesign/header.nim b/redesign/header.nim new file mode 100644 index 0000000..f9b9d05 --- /dev/null +++ b/redesign/header.nim @@ -0,0 +1,157 @@ +import options, times, httpcore, json, sugar + +import threadlist +type + UserStatus* = object + user*: Option[User] + +when defined(js): + include karax/prelude + import karax / [kajax] + + + import karaxutils + + from dom import setTimeout, window, document, getElementById + + type + State = ref object + data: Option[UserStatus] + loading: bool + status: HttpCode + lastUpdate: Time + + proc newState(): State = + State( + data: none[UserStatus](), + loading: false, + status: Http200 + ) + + var + state = newState() + + proc getStatus + proc onStatus(httpStatus: int, response: kstring) = + state.loading = false + state.status = httpStatus.HttpCode + if state.status != Http200: return + + let parsed = parseJson($response) + state.data = some(to(parsed, UserStatus)) + + state.lastUpdate = getTime() + + proc getStatus = + if state.loading: return + let diff = getTime() - state.lastUpdate + if diff.minutes < 5: + return + + state.loading = true + let uri = makeUri("status.json") + ajaxGet(uri, @[], onStatus) + + proc onLogInPost(httpStatus: int, response: kstring) = + kout(response) + + proc onLogInClick(ev: Event, n: VNode) = + let uri = makeUri("login") + let form = document.getElementById("login-form") + # TODO: This is a hack, karax should support this. + let formData = newFormData(form) + kout(formData.get("username")) + ajaxPost(uri, @[], cast[cstring](formData), onLogInPost) + + proc genLoginModal(): VNode = + result = buildHtml(): + tdiv(class="modal modal-sm", id="login-modal"): + a(href="#", class="modal-overlay", "aria-label"="close") + tdiv(class="modal-container"): + tdiv(class="modal-header"): + a(href="#", class="btn btn-clear float-right", "aria-label"="close") + tdiv(class="modal-title h5"): + text "Log in" + tdiv(class="modal-body"): + tdiv(class="content"): + form(id="login-form"): + tdiv(class="form-group"): + label(class="form-label", `for`="username"): + text "Username" + input(class="form-input", `type`="text", name="username") + tdiv(class="form-group"): + label(class="form-label", `for`="password"): + text "Password" + input(class="form-input", `type`="password", name="password") + a(href="#reset-password-modal"): + text "Reset your password" + tdiv(class="modal-footer"): + button(class="btn btn-primary", onClick=onLogInClick): + text "Log in" + a(href="#signup-modal"): + button(class="btn"): + text "Create account" + + proc genSignUpModal(): VNode = + result = buildHtml(): + tdiv(class="modal", id="signup-modal"): + a(href="#", class="modal-overlay", "aria-label"="close") + tdiv(class="modal-container"): + tdiv(class="modal-header"): + a(href="#", class="btn btn-clear float-right", "aria-label"="close") + tdiv(class="modal-title h5"): + text "Create a new account" + tdiv(class="modal-body"): + tdiv(class="content"): + form(): + tdiv(class="form-group"): + label(class="form-label", `for`="email"): + text "Email" + input(class="form-input", `type`="text", name="email") + tdiv(class="form-group"): + label(class="form-label", `for`="regusername"): + text "Username" + input(class="form-input", `type`="text", name="username") + tdiv(class="form-group"): + label(class="form-label", `for`="regpassword"): + text "Password" + input(class="form-input", `type`="password", name="password") + tdiv(class="modal-footer"): + button(class="btn btn-primary"): + text "Create account" + a(href="#login-modal"): + button(class="btn"): + text "Log in" + + proc renderHeader*(): VNode = + if state.data.isNone: + getStatus() + + let user = state.data.map(x => x.user).flatten + result = buildHtml(tdiv()): # TODO: Why do some buildHtml's need this? + header(id="main-navbar"): + tdiv(class="navbar container grid-xl"): + section(class="navbar-section"): + a(href=makeUri("/")): + img(src="images/crown.png", id="img-logo") # TODO: Customisation. + section(class="navbar-section"): + tdiv(class="input-group input-inline"): + input(class="search-input input-sm", `type`="text", placeholder="search") + if state.loading: + tdiv(class="loading") + elif user.isNone: + a(href="#signup-modal", id="signup-btn"): + button(class="btn btn-primary btn-sm"): + italic(class="fas fa-user-plus") + text " Sign up" + a(href="#login-modal", id="login-btn"): + button(class="btn btn-primary btn-sm"): + italic(class="fas fa-sign-in-alt") + text " Log in" + else: + render(user.get(), "avatar") + + # Modals + genLoginModal() + + genSignUpModal() \ No newline at end of file diff --git a/redesign/karaxutils.nim b/redesign/karaxutils.nim index c170a97..6830aa4 100644 --- a/redesign/karaxutils.nim +++ b/redesign/karaxutils.nim @@ -48,4 +48,12 @@ proc anchorCB*(e: kdom.Event, n: VNode) = # TODO: Why does this need disamb? dom.pushState(dom.window.history, 5, cstring"Thread", url) # Fire the popState event. - dom.window.dispatchEvent(newEvent("popstate")) \ No newline at end of file + dom.window.dispatchEvent(newEvent("popstate")) + + +type + FormData* = ref object +proc newFormData*(form: dom.Element): FormData + {.importcpp: "new FormData(@)", constructor.} +proc get*(form: FormData, key: cstring): cstring + {.importcpp: "#.get(@)".} \ No newline at end of file diff --git a/utils.nim b/utils.nim index ca0e992..11f51af 100644 --- a/utils.nim +++ b/utils.nim @@ -1,5 +1,5 @@ import asyncdispatch, smtp, strutils, json, os, rst, rstgen, xmltree, strtabs, - htmlparser, streams, parseutils + htmlparser, streams, parseutils, options from times import getTime, getGMTime, format proc parseInt*(s: string, value: var int, validRange: Slice[int]) {. @@ -20,6 +20,11 @@ proc getInt*(s: string, default = 0): int = result = default parseInt(s, result, 0..1_000_000_000) +proc `%`*[T](opt: Option[T]): JsonNode = + ## Generic constructor for JSON data. Creates a new ``JNull JsonNode`` + ## if ``opt`` is empty, otherwise it delegates to the underlying value. + if opt.isSome: %opt.get else: newJNull() + type Config* = object smtpAddress: string From f9efbe04d303e604bbcb6ce67eb3cc5372a34381 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 11 May 2018 15:49:22 +0100 Subject: [PATCH 130/451] Implements proper error handling for login form. --- forum.nim | 2 +- redesign/header.nim | 77 ++++++++-------------------- redesign/karaxutils.nim | 20 ++++---- redesign/login.nim | 109 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 143 insertions(+), 65 deletions(-) create mode 100644 redesign/login.nim diff --git a/forum.nim b/forum.nim index b353ddd..03b9390 100644 --- a/forum.nim +++ b/forum.nim @@ -1166,7 +1166,7 @@ routes: errorFields: @["username", "password"], message: "Invalid username or password" ) - resp $(%err), "application/json" + resp Http403, $(%err), "application/json" get "/karax/status.json": createTFD() diff --git a/redesign/header.nim b/redesign/header.nim index f9b9d05..1d7b3bb 100644 --- a/redesign/header.nim +++ b/redesign/header.nim @@ -9,10 +9,10 @@ when defined(js): include karax/prelude import karax / [kajax] - + import login import karaxutils - from dom import setTimeout, window, document, getElementById + from dom import setTimeout, window, document, getElementById, focus type State = ref object @@ -20,18 +20,23 @@ when defined(js): loading: bool status: HttpCode lastUpdate: Time + loginModal: LoginModal - proc newState(): State = - State( - data: none[UserStatus](), - loading: false, - status: Http200 - ) - + proc newState(): State var state = newState() proc getStatus + proc newState(): State = + State( + data: none[UserStatus](), + loading: false, + status: Http200, + loginModal: newLoginModal( + () => (state.lastUpdate = fromUnix(0); getStatus()) + ) + ) + proc onStatus(httpStatus: int, response: kstring) = state.loading = false state.status = httpStatus.HttpCode @@ -52,46 +57,6 @@ when defined(js): let uri = makeUri("status.json") ajaxGet(uri, @[], onStatus) - proc onLogInPost(httpStatus: int, response: kstring) = - kout(response) - - proc onLogInClick(ev: Event, n: VNode) = - let uri = makeUri("login") - let form = document.getElementById("login-form") - # TODO: This is a hack, karax should support this. - let formData = newFormData(form) - kout(formData.get("username")) - ajaxPost(uri, @[], cast[cstring](formData), onLogInPost) - - proc genLoginModal(): VNode = - result = buildHtml(): - tdiv(class="modal modal-sm", id="login-modal"): - a(href="#", class="modal-overlay", "aria-label"="close") - tdiv(class="modal-container"): - tdiv(class="modal-header"): - a(href="#", class="btn btn-clear float-right", "aria-label"="close") - tdiv(class="modal-title h5"): - text "Log in" - tdiv(class="modal-body"): - tdiv(class="content"): - form(id="login-form"): - tdiv(class="form-group"): - label(class="form-label", `for`="username"): - text "Username" - input(class="form-input", `type`="text", name="username") - tdiv(class="form-group"): - label(class="form-label", `for`="password"): - text "Password" - input(class="form-input", `type`="password", name="password") - a(href="#reset-password-modal"): - text "Reset your password" - tdiv(class="modal-footer"): - button(class="btn btn-primary", onClick=onLogInClick): - text "Log in" - a(href="#signup-modal"): - button(class="btn"): - text "Create account" - proc genSignUpModal(): VNode = result = buildHtml(): tdiv(class="modal", id="signup-modal"): @@ -136,7 +101,9 @@ when defined(js): img(src="images/crown.png", id="img-logo") # TODO: Customisation. section(class="navbar-section"): tdiv(class="input-group input-inline"): - input(class="search-input input-sm", `type`="text", placeholder="search") + input(class="search-input input-sm", + `type`="text", placeholder="search", + id="search-box") if state.loading: tdiv(class="loading") elif user.isNone: @@ -144,14 +111,14 @@ when defined(js): button(class="btn btn-primary btn-sm"): italic(class="fas fa-user-plus") text " Sign up" - a(href="#login-modal", id="login-btn"): - button(class="btn btn-primary btn-sm"): - italic(class="fas fa-sign-in-alt") - text " Log in" + button(class="btn btn-primary btn-sm", + onClick=(e: Event, n: VNode) => state.loginModal.show()): + italic(class="fas fa-sign-in-alt") + text " Log in" else: render(user.get(), "avatar") # Modals - genLoginModal() + render(state.loginModal) genSignUpModal() \ No newline at end of file diff --git a/redesign/karaxutils.nim b/redesign/karaxutils.nim index 6830aa4..2c7f7ad 100644 --- a/redesign/karaxutils.nim +++ b/redesign/karaxutils.nim @@ -12,7 +12,7 @@ proc class*(classes: varargs[tuple[name: string, present: bool]], for class in classes: if class.present: result.add(class.name & " ") -proc makeUri*(relative: string, appName=appName): string = +proc makeUri*(relative: string, appName=appName, includeHash=false): string = ## Concatenates ``relative`` to the current URL in a way that is sane. var relative = relative assert appName in $window.location.pathname @@ -23,10 +23,10 @@ proc makeUri*(relative: string, appName=appName): string = appName & relative & $window.location.search & - $window.location.hash + (if includeHash: $window.location.hash else: "") proc makeUri*(relative: string, params: varargs[(string, string)], - appName=appName): string = + appName=appName, includeHash=false): string = var query = "" for i in 0 ..< params.len: let param = params[i] @@ -38,18 +38,20 @@ proc makeUri*(relative: string, params: varargs[(string, string)], else: makeUri(relative, appName) +proc navigateTo*(uri: cstring) = + # TODO: This was annoying. Karax also shouldn't have its own `window`. + dom.pushState(dom.window.history, 0, cstring"", uri) + + # Fire the popState event. + dom.window.dispatchEvent(newEvent("popstate")) + proc anchorCB*(e: kdom.Event, n: VNode) = # TODO: Why does this need disamb? e.preventDefault() # TODO: Why does Karax have it's own Node type? That's just silly. let url = cast[dom.Node](n.dom).getAttribute(cstring"href") - # TODO: This was annoying. Karax also shouldn't have its own `window`. - dom.pushState(dom.window.history, 5, cstring"Thread", url) - - # Fire the popState event. - dom.window.dispatchEvent(newEvent("popstate")) - + navigateTo(url) type FormData* = ref object diff --git a/redesign/login.nim b/redesign/login.nim new file mode 100644 index 0000000..aa92fac --- /dev/null +++ b/redesign/login.nim @@ -0,0 +1,109 @@ +when defined(js): + import sugar, httpcore, options, json + import dom except Event + + include karax/prelude + import karax / [kajax, kdom] + + import error + import karaxutils + + type + LoginModal* = ref object + shown: bool + onLogIn: proc () + error: Option[PostError] + + proc onLogInPost(httpStatus: int, response: kstring, state: LoginModal) = + let status = httpStatus.HttpCode + if status == Http200: + state.shown = false + state.onLogIn() + else: + # TODO: Karax should pass the content-type... + try: + let parsed = parseJson($response) + let error = to(parsed, PostError) + + state.error = some(error) + except: + kout(getCurrentExceptionMsg().cstring) + state.error = some(PostError( + errorFields: @[], + message: "Unknown error occurred." + )) + + proc onLogInClick(ev: Event, n: VNode, state: LoginModal) = + state.error = none[PostError]() + + let uri = makeUri("login") + let form = dom.document.getElementById("login-form") + # TODO: This is a hack, karax should support this. + let formData = newFormData(form) + ajaxPost(uri, @[], cast[cstring](formData), + (s: int, r: kstring) => onLogInPost(s, r, state)) + + proc onClose(ev: Event, n: VNode, state: LoginModal) = + state.shown = false + ev.preventDefault() + + proc newLoginModal*(onLogIn: proc ()): LoginModal = + LoginModal( + shown: false, + onLogIn: onLogIn + ) + + proc show*(state: LoginModal) = + state.shown = true + + proc genFormField(error: Option[PostError], name, label, typ: string, + isLast: bool): VNode = + let hasError = + not error.isNone and ( + name in error.get().errorFields or + error.get().errorFields.len == 0) + result = buildHtml(): + tdiv(class=class({"has-error": hasError}, "form-group")): + label(class="form-label", `for`=name): + text "Username" + input(class="form-input", `type`="text", name=name) + + if not error.isNone: + let e = error.get() + if (e.errorFields.len == 1 and e.errorFields[0] == name) or isLast: + p(class="form-input-hint"): + text e.message + + proc render*(state: LoginModal): VNode = + result = buildHtml(): + tdiv(class=class({"active": state.shown}, "modal modal-sm"), + id="login-modal"): + a(href="", class="modal-overlay", "aria-label"="close", + onClick=(ev: Event, n: VNode) => onClose(ev, n, state)) + tdiv(class="modal-container"): + tdiv(class="modal-header"): + a(href="", class="btn btn-clear float-right", + "aria-label"="close", + onClick=(ev: Event, n: VNode) => onClose(ev, n, state)) + tdiv(class="modal-title h5"): + text "Log in" + tdiv(class="modal-body"): + tdiv(class="content"): + form(id="login-form"): + genFormField(state.error, "username", "Username", "text", false) + genFormField( + state.error, + "password", + "Password", + "password", + true + ) + a(href="#reset-password-modal"): + text "Reset your password" + tdiv(class="modal-footer"): + button(class="btn btn-primary", + onClick=(ev: Event, n: VNode) => onLogInClick(ev, n, state)): + text "Log in" + a(href="#signup-modal"): + button(class="btn"): + text "Create account" \ No newline at end of file From 01253244f7dff048e75338a88e900c65328820ac Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 11 May 2018 16:15:13 +0100 Subject: [PATCH 131/451] Reimplement sign up form. --- redesign/error.nim | 23 ++++++++++- redesign/header.nim | 51 ++++++----------------- redesign/karaxutils.nim | 2 +- redesign/login.nim | 31 ++++---------- redesign/signup.nim | 92 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 136 insertions(+), 63 deletions(-) create mode 100644 redesign/signup.nim diff --git a/redesign/error.nim b/redesign/error.nim index 108adbe..3d540d5 100644 --- a/redesign/error.nim +++ b/redesign/error.nim @@ -1,3 +1,4 @@ +import options type PostError* = object errorFields*: seq[string] ## IDs of the fields with an error. @@ -7,6 +8,8 @@ when defined(js): include karax/prelude import karax / [vstyles, kajax, kdom] + import karaxutils + proc renderError*(message: string): VNode = result = buildHtml(): tdiv(class="empty error"): @@ -19,4 +22,22 @@ when defined(js): tdiv(class="empty-action"): a(href="https://github.com/nim-lang/nimforum/issues", target="_blank"): button(class="btn btn-primary"): - text "Report issue" \ No newline at end of file + text "Report issue" + + proc genFormField*(error: Option[PostError], name, label, typ: string, + isLast: bool): VNode = + let hasError = + not error.isNone and ( + name in error.get().errorFields or + error.get().errorFields.len == 0) + result = buildHtml(): + tdiv(class=class({"has-error": hasError}, "form-group")): + label(class="form-label", `for`=name): + text label + input(class="form-input", `type`="text", name=name) + + if not error.isNone: + let e = error.get() + if (e.errorFields.len == 1 and e.errorFields[0] == name) or isLast: + p(class="form-input-hint"): + text e.message \ No newline at end of file diff --git a/redesign/header.nim b/redesign/header.nim index 1d7b3bb..cf07135 100644 --- a/redesign/header.nim +++ b/redesign/header.nim @@ -9,7 +9,7 @@ when defined(js): include karax/prelude import karax / [kajax] - import login + import login, signup import karaxutils from dom import setTimeout, window, document, getElementById, focus @@ -21,6 +21,7 @@ when defined(js): status: HttpCode lastUpdate: Time loginModal: LoginModal + signupModal: SignupModal proc newState(): State var @@ -33,7 +34,12 @@ when defined(js): loading: false, status: Http200, loginModal: newLoginModal( - () => (state.lastUpdate = fromUnix(0); getStatus()) + () => (state.lastUpdate = fromUnix(0); getStatus()), + () => state.signupModal.show() + ), + signupModal: newSignupModal( + () => (state.lastUpdate = fromUnix(0); getStatus()), + () => state.loginModal.show() ) ) @@ -57,37 +63,6 @@ when defined(js): let uri = makeUri("status.json") ajaxGet(uri, @[], onStatus) - proc genSignUpModal(): VNode = - result = buildHtml(): - tdiv(class="modal", id="signup-modal"): - a(href="#", class="modal-overlay", "aria-label"="close") - tdiv(class="modal-container"): - tdiv(class="modal-header"): - a(href="#", class="btn btn-clear float-right", "aria-label"="close") - tdiv(class="modal-title h5"): - text "Create a new account" - tdiv(class="modal-body"): - tdiv(class="content"): - form(): - tdiv(class="form-group"): - label(class="form-label", `for`="email"): - text "Email" - input(class="form-input", `type`="text", name="email") - tdiv(class="form-group"): - label(class="form-label", `for`="regusername"): - text "Username" - input(class="form-input", `type`="text", name="username") - tdiv(class="form-group"): - label(class="form-label", `for`="regpassword"): - text "Password" - input(class="form-input", `type`="password", name="password") - tdiv(class="modal-footer"): - button(class="btn btn-primary"): - text "Create account" - a(href="#login-modal"): - button(class="btn"): - text "Log in" - proc renderHeader*(): VNode = if state.data.isNone: getStatus() @@ -107,10 +82,10 @@ when defined(js): if state.loading: tdiv(class="loading") elif user.isNone: - a(href="#signup-modal", id="signup-btn"): - button(class="btn btn-primary btn-sm"): - italic(class="fas fa-user-plus") - text " Sign up" + button(class="btn btn-primary btn-sm", + onClick=(e: Event, n: VNode) => state.signupModal.show()): + italic(class="fas fa-user-plus") + text " Sign up" button(class="btn btn-primary btn-sm", onClick=(e: Event, n: VNode) => state.loginModal.show()): italic(class="fas fa-sign-in-alt") @@ -121,4 +96,4 @@ when defined(js): # Modals render(state.loginModal) - genSignUpModal() \ No newline at end of file + render(state.signupModal) \ No newline at end of file diff --git a/redesign/karaxutils.nim b/redesign/karaxutils.nim index 2c7f7ad..7ee5eaf 100644 --- a/redesign/karaxutils.nim +++ b/redesign/karaxutils.nim @@ -1,4 +1,4 @@ -import strutils +import strutils, options import dom except window include karax/prelude diff --git a/redesign/login.nim b/redesign/login.nim index aa92fac..006a610 100644 --- a/redesign/login.nim +++ b/redesign/login.nim @@ -12,6 +12,7 @@ when defined(js): LoginModal* = ref object shown: bool onLogIn: proc () + onSignUp: proc () error: Option[PostError] proc onLogInPost(httpStatus: int, response: kstring, state: LoginModal) = @@ -47,33 +48,16 @@ when defined(js): state.shown = false ev.preventDefault() - proc newLoginModal*(onLogIn: proc ()): LoginModal = + proc newLoginModal*(onLogIn, onSignUp: proc ()): LoginModal = LoginModal( shown: false, - onLogIn: onLogIn + onLogIn: onLogIn, + onSignUp: onSignUp ) proc show*(state: LoginModal) = state.shown = true - proc genFormField(error: Option[PostError], name, label, typ: string, - isLast: bool): VNode = - let hasError = - not error.isNone and ( - name in error.get().errorFields or - error.get().errorFields.len == 0) - result = buildHtml(): - tdiv(class=class({"has-error": hasError}, "form-group")): - label(class="form-label", `for`=name): - text "Username" - input(class="form-input", `type`="text", name=name) - - if not error.isNone: - let e = error.get() - if (e.errorFields.len == 1 and e.errorFields[0] == name) or isLast: - p(class="form-input-hint"): - text e.message - proc render*(state: LoginModal): VNode = result = buildHtml(): tdiv(class=class({"active": state.shown}, "modal modal-sm"), @@ -104,6 +88,7 @@ when defined(js): button(class="btn btn-primary", onClick=(ev: Event, n: VNode) => onLogInClick(ev, n, state)): text "Log in" - a(href="#signup-modal"): - button(class="btn"): - text "Create account" \ No newline at end of file + button(class="btn", + onClick=(ev: Event, n: VNode) => + (state.onSignUp(); state.shown = false)): + text "Create account" \ No newline at end of file diff --git a/redesign/signup.nim b/redesign/signup.nim new file mode 100644 index 0000000..1b0db46 --- /dev/null +++ b/redesign/signup.nim @@ -0,0 +1,92 @@ +when defined(js): + import sugar, httpcore, options, json + import dom except Event + + include karax/prelude + import karax / [kajax, kdom] + + import error + import karaxutils + + type + SignupModal* = ref object + shown: bool + onSignUp, onLogIn: proc () + error: Option[PostError] + + proc onSignUpPost(httpStatus: int, response: kstring, state: SignupModal) = + let status = httpStatus.HttpCode + if status == Http200: + state.shown = false + state.onSignUp() + else: + # TODO: Karax should pass the content-type... + try: + let parsed = parseJson($response) + let error = to(parsed, PostError) + + state.error = some(error) + except: + kout(getCurrentExceptionMsg().cstring) + state.error = some(PostError( + errorFields: @[], + message: "Unknown error occurred." + )) + + proc onSignUpClick(ev: Event, n: VNode, state: SignupModal) = + state.error = none[PostError]() + + let uri = makeUri("signup") + let form = dom.document.getElementById("signup-form") + # TODO: This is a hack, karax should support this. + let formData = newFormData(form) + ajaxPost(uri, @[], cast[cstring](formData), + (s: int, r: kstring) => onSignUpPost(s, r, state)) + + proc onClose(ev: Event, n: VNode, state: SignupModal) = + state.shown = false + ev.preventDefault() + + proc newSignupModal*(onSignUp, onLogIn: proc ()): SignupModal = + SignupModal( + shown: false, + onLogIn: onLogIn, + onSignUp: onSignUp + ) + + proc show*(state: SignupModal) = + state.shown = true + + proc render*(state: SignupModal): VNode = + result = buildHtml(): + tdiv(class=class({"active": state.shown}, "modal modal-sm"), + id="signup-modal"): + a(href="", class="modal-overlay", "aria-label"="close", + onClick=(ev: Event, n: VNode) => onClose(ev, n, state)) + tdiv(class="modal-container"): + tdiv(class="modal-header"): + a(href="", class="btn btn-clear float-right", + "aria-label"="close", + onClick=(ev: Event, n: VNode) => onClose(ev, n, state)) + tdiv(class="modal-title h5"): + text "Create a new account" + tdiv(class="modal-body"): + tdiv(class="content"): + form(id="signup-form"): + genFormField(state.error, "email", "Email", "email", false) + genFormField(state.error, "username", "Username", "text", false) + genFormField( + state.error, + "password", + "Password", + "password", + true + ) + tdiv(class="modal-footer"): + button(class="btn btn-primary", + onClick=(ev: Event, n: VNode) => onSignUpClick(ev, n, state)): + text "Create account" + button(class="btn", + onClick=(ev: Event, n: VNode) => + (state.onLogIn(); state.shown = false)): + text "Log in" \ No newline at end of file From 4d115622d60b8065f31eac23674314f9efdde364 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 11 May 2018 16:17:41 +0100 Subject: [PATCH 132/451] Small fix to genFormField. --- redesign/error.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redesign/error.nim b/redesign/error.nim index 3d540d5..fb96758 100644 --- a/redesign/error.nim +++ b/redesign/error.nim @@ -34,7 +34,7 @@ when defined(js): tdiv(class=class({"has-error": hasError}, "form-group")): label(class="form-label", `for`=name): text label - input(class="form-input", `type`="text", name=name) + input(class="form-input", `type`=typ, name=name) if not error.isNone: let e = error.get() From a7b616e63aff646b7e0fb5fac178480585bd9d2a Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 11 May 2018 17:33:10 +0100 Subject: [PATCH 133/451] Implements user dropdown and the ability to log out. --- forum.nim | 5 ++++- redesign/header.nim | 16 ++++++++------ redesign/nimforum.scss | 7 ++++++ redesign/threadlist.nim | 2 +- redesign/usermenu.nim | 48 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 redesign/usermenu.nim diff --git a/forum.nim b/forum.nim index 03b9390..ce1bc6f 100644 --- a/forum.nim +++ b/forum.nim @@ -1170,8 +1170,11 @@ routes: get "/karax/status.json": createTFD() + let user = - if c.loggedIn(): + if @"logout" == "true": + logout(c); none[threadlist.User]() + elif c.loggedIn(): some(threadlist.User( name: c.username, avatarUrl: c.email.getGravatarUrl(), diff --git a/redesign/header.nim b/redesign/header.nim index cf07135..9c08d16 100644 --- a/redesign/header.nim +++ b/redesign/header.nim @@ -9,7 +9,7 @@ when defined(js): include karax/prelude import karax / [kajax] - import login, signup + import login, signup, usermenu import karaxutils from dom import setTimeout, window, document, getElementById, focus @@ -22,12 +22,13 @@ when defined(js): lastUpdate: Time loginModal: LoginModal signupModal: SignupModal + userMenu: UserMenu proc newState(): State var state = newState() - proc getStatus + proc getStatus(logout: bool=false) proc newState(): State = State( data: none[UserStatus](), @@ -40,6 +41,9 @@ when defined(js): signupModal: newSignupModal( () => (state.lastUpdate = fromUnix(0); getStatus()), () => state.loginModal.show() + ), + userMenu: newUserMenu( + () => (state.lastUpdate = fromUnix(0); getStatus(logout=true)) ) ) @@ -53,19 +57,19 @@ when defined(js): state.lastUpdate = getTime() - proc getStatus = + proc getStatus(logout: bool=false) = if state.loading: return let diff = getTime() - state.lastUpdate if diff.minutes < 5: return state.loading = true - let uri = makeUri("status.json") + let uri = makeUri("status.json", [("logout", $logout)]) ajaxGet(uri, @[], onStatus) proc renderHeader*(): VNode = if state.data.isNone: - getStatus() + getStatus() # TODO: Call this every render? let user = state.data.map(x => x.user).flatten result = buildHtml(tdiv()): # TODO: Why do some buildHtml's need this? @@ -91,7 +95,7 @@ when defined(js): italic(class="fas fa-sign-in-alt") text " Log in" else: - render(user.get(), "avatar") + render(state.userMenu, user.get()) # Modals render(state.loginModal) diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index a819796..99cf68b 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -81,6 +81,13 @@ $logo-height: $navbar-height - 20px; height: $logo-height; } +.menu-right { + // To make sure the user menu doesn't move off the screen. + left: auto; + right: 0; + position: absolute; +} + // - Main buttons #main-buttons { margin-top: $control-padding-y*2; diff --git a/redesign/threadlist.nim b/redesign/threadlist.nim index 58094be..7ab611f 100644 --- a/redesign/threadlist.nim +++ b/redesign/threadlist.nim @@ -1,4 +1,4 @@ -import strformat, times, options, json, httpcore +import strformat, times, options, json, httpcore, sugar import category diff --git a/redesign/usermenu.nim b/redesign/usermenu.nim new file mode 100644 index 0000000..2bb35a1 --- /dev/null +++ b/redesign/usermenu.nim @@ -0,0 +1,48 @@ + +when defined(js): + import sugar + + include karax/prelude + import karax/[vstyles] + import karaxutils + + import threadlist + type + UserMenu* = ref object + shown: bool + user: User + onLogout: proc () + + proc newUserMenu*(onLogout: proc ()): UserMenu = + UserMenu( + shown: false, + onLogout: onLogout + ) + + proc render*(state: UserMenu, user: User): VNode = + result = buildHtml(): + tdiv(): + figure(class="avatar c-hand", + onClick=(e: Event, n: VNode) => (state.shown = true)): + img(src=user.avatarUrl, title=user.name) + if user.isOnline: + italic(class="avatar-presense online") + + ul(class="menu menu-right", style=style( + StyleAttr.display, if state.shown: "inherit" else: "none" + )): + li(class="menu-item"): + tdiv(class="tile tile-centered"): + tdiv(class="tile-icon"): + img(class="avatar", src=user.avatarUrl, + title=user.name) + tdiv(class="tile-content"): + text user.name + li(class="divider") + li(class="menu-item"): + a(href=makeUri("/profile/" & user.name)): + text "My profile" + li(class="menu-item c-hand"): + a(onClick = (e: Event, n: VNode) => + (state.shown=false; state.onLogout())): + text "Logout" \ No newline at end of file From 84069239150f945176069721cc59880964eb11c0 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 11 May 2018 17:37:52 +0100 Subject: [PATCH 134/451] Implements login via enter key. --- redesign/login.nim | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/redesign/login.nim b/redesign/login.nim index 006a610..060c7a3 100644 --- a/redesign/login.nim +++ b/redesign/login.nim @@ -58,6 +58,11 @@ when defined(js): proc show*(state: LoginModal) = state.shown = true + proc onKeyDown(e: Event, n: VNode, state: LoginModal) = + let event = cast[KeyboardEvent](e) + if event.key == "Enter": + onLogInClick(e, n, state) + proc render*(state: LoginModal): VNode = result = buildHtml(): tdiv(class=class({"active": state.shown}, "modal modal-sm"), @@ -73,7 +78,8 @@ when defined(js): text "Log in" tdiv(class="modal-body"): tdiv(class="content"): - form(id="login-form"): + form(id="login-form", + onKeyDown=(ev: Event, n: VNode) => onKeyDown(ev, n, state)): genFormField(state.error, "username", "Username", "text", false) genFormField( state.error, From 17436010a2cc4b41fb58e8e0e625f8689b609f19 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 11 May 2018 17:41:06 +0100 Subject: [PATCH 135/451] Show spinner on login. --- redesign/login.nim | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/redesign/login.nim b/redesign/login.nim index 060c7a3..94a9228 100644 --- a/redesign/login.nim +++ b/redesign/login.nim @@ -11,11 +11,13 @@ when defined(js): type LoginModal* = ref object shown: bool + loading: bool onLogIn: proc () onSignUp: proc () error: Option[PostError] proc onLogInPost(httpStatus: int, response: kstring, state: LoginModal) = + state.loading = false let status = httpStatus.HttpCode if status == Http200: state.shown = false @@ -35,6 +37,7 @@ when defined(js): )) proc onLogInClick(ev: Event, n: VNode, state: LoginModal) = + state.loading = true state.error = none[PostError]() let uri = makeUri("login") @@ -91,7 +94,10 @@ when defined(js): a(href="#reset-password-modal"): text "Reset your password" tdiv(class="modal-footer"): - button(class="btn btn-primary", + button(class=class( + {"loading": state.loading}, + "btn btn-primary" + ), onClick=(ev: Event, n: VNode) => onLogInClick(ev, n, state)): text "Log in" button(class="btn", From 594f230480131d28c581379daa8426938f634e7a Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 11 May 2018 17:42:19 +0100 Subject: [PATCH 136/451] Allow toggling of menu by clicking on the user icon. --- redesign/usermenu.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redesign/usermenu.nim b/redesign/usermenu.nim index 2bb35a1..9673dcf 100644 --- a/redesign/usermenu.nim +++ b/redesign/usermenu.nim @@ -23,7 +23,7 @@ when defined(js): result = buildHtml(): tdiv(): figure(class="avatar c-hand", - onClick=(e: Event, n: VNode) => (state.shown = true)): + onClick=(e: Event, n: VNode) => (state.shown = not state.shown)): img(src=user.avatarUrl, title=user.name) if user.isOnline: italic(class="avatar-presense online") From f1e2a68d86081861133293977d716ef08352bf50 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 11 May 2018 19:49:57 +0100 Subject: [PATCH 137/451] Pass user info to post renderer. --- redesign/forum.nim | 3 +-- redesign/header.nim | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/redesign/forum.nim b/redesign/forum.nim index 637fcf4..686fc2a 100644 --- a/redesign/forum.nim +++ b/redesign/forum.nim @@ -46,8 +46,7 @@ proc render(): VNode = route([ r("/t/@id?", (params: Params) => - (kout(params["id"].cstring); - renderPostList(params["id"].parseInt(), false)) + (renderPostList(params["id"].parseInt(), isLoggedIn())) ), r("/", (params: Params) => renderThreadList()) ]) diff --git a/redesign/header.nim b/redesign/header.nim index 9c08d16..3ab6845 100644 --- a/redesign/header.nim +++ b/redesign/header.nim @@ -67,6 +67,10 @@ when defined(js): let uri = makeUri("status.json", [("logout", $logout)]) ajaxGet(uri, @[], onStatus) + proc isLoggedIn*(): bool = + let user = state.data.map(x => x.user).flatten + not user.isNone + proc renderHeader*(): VNode = if state.data.isNone: getStatus() # TODO: Call this every render? From ea6ced889c3a50501f05931f014d8ec182741b08 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 11 May 2018 20:21:28 +0100 Subject: [PATCH 138/451] Implements rendering of `time-passed` divs. --- redesign/postlist.nim | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/redesign/postlist.nim b/redesign/postlist.nim index 226af98..80e0d3c 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -1,5 +1,5 @@ -import options, json, times, httpcore, strformat, sugar +import options, json, times, httpcore, strformat, sugar, math import threadlist, category type @@ -38,6 +38,7 @@ when defined(js): list: Option[PostList] loading: bool status: HttpCode + replyBoxShown: bool proc newState(): State = State( @@ -127,6 +128,33 @@ when defined(js): span(class="more-post-count"): text "(" & $state.list.get().moreCount & ")" + proc genTimePassed(prevPost: Post, post: Option[Post]): VNode = + var latestTime = + if post.isSome: post.get().info.creation.fromUnix() + else: getTime() + + # TODO: Use `between` once it's merged into stdlib. + var diffStr = "Some time later" + let diff = latestTime - prevPost.info.creation.fromUnix() + if diff.weeks > 48: + let years = diff.weeks div 48 + diffStr = $years & " years later" + elif diff.weeks > 4: + let months = diff.weeks div 4 + diffStr = $months & " months later" + else: + return buildHtml(tdiv()) + + # PROTIP: Good thread ID to test this with is: 1267. + result = buildHtml(): + tdiv(class="information time-passed"): + tdiv(class="information-icon"): + italic(class="fas fa-clock") + tdiv(class="information-main"): + tdiv(class="information-title"): + text diffStr + + proc renderPostList*(threadId: int, isLoggedIn: bool): VNode = if state.status != Http200: return renderError("Couldn't retrieve posts.") @@ -144,8 +172,15 @@ when defined(js): p(): text list.thread.topic render(list.thread.category) tdiv(class="posts"): + var prevPost: Option[Post] = none[Post]() for post in list.posts: + if prevPost.isSome: + genTimePassed(prevPost.get(), some(post)) genPost(post, list.thread, isLoggedIn) + prevPost = some(post) + + if state.replyBoxShown and prevPost.isSome: + genTimePassed(prevPost.get(), none[Post]()) if list.moreCount > 0: - genLoadMore(list.posts.len) \ No newline at end of file + genLoadMore(list.posts.len) From dc7e9fda31b4e439128b7b6d13218cd74663ddd2 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 11 May 2018 20:24:53 +0100 Subject: [PATCH 139/451] The plurals in the English language always get you --- redesign/postlist.nim | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/redesign/postlist.nim b/redesign/postlist.nim index 80e0d3c..37caf8e 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -138,10 +138,12 @@ when defined(js): let diff = latestTime - prevPost.info.creation.fromUnix() if diff.weeks > 48: let years = diff.weeks div 48 - diffStr = $years & " years later" + diffStr = $years + diffStr.add(if years == 1: " year later" else: " years later") elif diff.weeks > 4: let months = diff.weeks div 4 - diffStr = $months & " months later" + diffStr = $months + diffStr.add(if months == 1: " month later" else: " months later") else: return buildHtml(tdiv()) From aab10809a24a518185f05ebd302413491672181e Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 11 May 2018 20:52:07 +0100 Subject: [PATCH 140/451] Implements reply box UI. --- redesign/nimforum.scss | 6 ++++++ redesign/postlist.nim | 11 ++++++++--- redesign/replybox.nim | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 redesign/replybox.nim diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index 99cf68b..b255092 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -389,4 +389,10 @@ blockquote { margin-right: $control-padding-x*2; float: right; } +} + +.form-input { + // For reply text area. + margin-top: $control-padding-y*2; + resize: vertical; } \ No newline at end of file diff --git a/redesign/postlist.nim b/redesign/postlist.nim index 37caf8e..a437088 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -31,7 +31,7 @@ when defined(js): include karax/prelude import karax / [vstyles, kajax, kdom] - import karaxutils, error + import karaxutils, error, replybox type State = ref object @@ -39,12 +39,15 @@ when defined(js): loading: bool status: HttpCode replyBoxShown: bool + replyBox: ReplyBox proc newState(): State = State( list: none[PostList](), loading: false, - status: Http200 + status: Http200, + replyBoxShown: true, + replyBox: newReplyBox() ) var @@ -156,7 +159,6 @@ when defined(js): tdiv(class="information-title"): text diffStr - proc renderPostList*(threadId: int, isLoggedIn: bool): VNode = if state.status != Http200: return renderError("Couldn't retrieve posts.") @@ -186,3 +188,6 @@ when defined(js): if list.moreCount > 0: genLoadMore(list.posts.len) + + if state.replyBoxShown: + render(state.replyBox, list.thread) \ No newline at end of file diff --git a/redesign/replybox.nim b/redesign/replybox.nim new file mode 100644 index 0000000..00a69ed --- /dev/null +++ b/redesign/replybox.nim @@ -0,0 +1,40 @@ +when defined(js): + import strformat + + include karax/prelude + import karax / [vstyles, kajax, kdom] + + import karaxutils, threadlist + + type + ReplyBox* = ref object + preview: bool + + proc newReplyBox*(): ReplyBox = + ReplyBox() + + proc render*(state: ReplyBox, thread: Thread): VNode = + result = buildHtml(): + tdiv(class="information no-border"): + tdiv(class="information-icon"): + italic(class="fas fa-reply") + tdiv(class="information-main", style=style(StyleAttr.width, "100%")): + tdiv(class="information-title"): + # text fmt("Replying to \"{thread.topic}\"") + # tdiv(class="information-content"): + tdiv(class="panel"): + tdiv(class="panel-nav"): + ul(class="tab tab-block"): + li(class=class({"active": not state.preview}, "tab-item")): + a(href="#"): + text "Message" + li(class=class({"active": state.preview}, "tab-item")): + a(href="#"): + text "Preview" + tdiv(class="panel-body"): + textarea(class="form-input", rows="5") + tdiv(class="panel-footer"): + button(class="btn btn-primary float-right"): + text "Reply" + button(class="btn btn-link float-right"): + text "Cancel" \ No newline at end of file From fe49dd1e85991a8038392144770cd7a20c9ddeea Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 11 May 2018 22:41:11 +0100 Subject: [PATCH 141/451] Fixes small style regression. --- redesign/nimforum.scss | 2 +- redesign/replybox.nim | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index b255092..34d5cad 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -391,7 +391,7 @@ blockquote { } } -.form-input { +#reply-box .form-input { // For reply text area. margin-top: $control-padding-y*2; resize: vertical; diff --git a/redesign/replybox.nim b/redesign/replybox.nim index 00a69ed..35f3b81 100644 --- a/redesign/replybox.nim +++ b/redesign/replybox.nim @@ -22,7 +22,7 @@ when defined(js): tdiv(class="information-title"): # text fmt("Replying to \"{thread.topic}\"") # tdiv(class="information-content"): - tdiv(class="panel"): + tdiv(class="panel", id="reply-box"): tdiv(class="panel-nav"): ul(class="tab tab-block"): li(class=class({"active": not state.preview}, "tab-item")): From 96e78a5b8482516c69bfc9871cc218eda8b28098 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 12 May 2018 14:08:08 +0100 Subject: [PATCH 142/451] Show who you're replying to and scroll to reply box. --- redesign/header.nim | 2 +- redesign/karax.html | 8 +++----- redesign/karaxutils.nim | 3 ++- redesign/post.nim | 18 ++++++++++++++++++ redesign/postlist.nim | 37 +++++++++++++------------------------ redesign/replybox.nim | 39 ++++++++++++++++++++++++++++++++------- redesign/threadlist.nim | 6 ++++++ 7 files changed, 75 insertions(+), 38 deletions(-) create mode 100644 redesign/post.nim diff --git a/redesign/header.nim b/redesign/header.nim index 3ab6845..b049808 100644 --- a/redesign/header.nim +++ b/redesign/header.nim @@ -81,7 +81,7 @@ when defined(js): tdiv(class="navbar container grid-xl"): section(class="navbar-section"): a(href=makeUri("/")): - img(src="images/crown.png", id="img-logo") # TODO: Customisation. + img(src="/karax/images/crown.png", id="img-logo") # TODO: Customisation. section(class="navbar-section"): tdiv(class="input-group input-inline"): input(class="search-input input-sm", diff --git a/redesign/karax.html b/redesign/karax.html index 24242d2..aa4d440 100644 --- a/redesign/karax.html +++ b/redesign/karax.html @@ -8,17 +8,15 @@ The Nim programming language forum - - - + - +
- + diff --git a/redesign/karaxutils.nim b/redesign/karaxutils.nim index 7ee5eaf..28ada03 100644 --- a/redesign/karaxutils.nim +++ b/redesign/karaxutils.nim @@ -13,7 +13,8 @@ proc class*(classes: varargs[tuple[name: string, present: bool]], if class.present: result.add(class.name & " ") proc makeUri*(relative: string, appName=appName, includeHash=false): string = - ## Concatenates ``relative`` to the current URL in a way that is sane. + ## Concatenates ``relative`` to the current URL in a way that is + ## (possibly) sane. var relative = relative assert appName in $window.location.pathname if relative[0] == '/': relative = relative[1..^1] diff --git a/redesign/post.nim b/redesign/post.nim new file mode 100644 index 0000000..da0ac10 --- /dev/null +++ b/redesign/post.nim @@ -0,0 +1,18 @@ +import threadlist + +type + PostInfo* = object + creation*: int64 + content*: string + + Post* = object + id*: int + author*: User + likes*: seq[User] ## Users that liked this post. + seen*: bool ## Determines whether the current user saw this post. + ## I considered using a simple timestamp for each thread, + ## but that wouldn't work when a user navigates to the last + ## post in a thread for example. + history*: seq[PostInfo] ## If the post was edited this will contain the + ## older versions of the post. + info*: PostInfo \ No newline at end of file diff --git a/redesign/postlist.nim b/redesign/postlist.nim index a437088..78abd34 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -1,23 +1,8 @@ import options, json, times, httpcore, strformat, sugar, math -import threadlist, category +import threadlist, category, post type - PostInfo* = object - creation*: int64 - content*: string - - Post* = object - id*: int - author*: User - likes*: seq[User] ## Users that liked this post. - seen*: bool ## Determines whether the current user saw this post. - ## I considered using a simple timestamp for each thread, - ## but that wouldn't work when a user navigates to the last - ## post in a thread for example. - history*: seq[PostInfo] ## If the post was edited this will contain the - ## older versions of the post. - info*: PostInfo PostList* = ref object thread*: Thread @@ -38,7 +23,7 @@ when defined(js): list: Option[PostList] loading: bool status: HttpCode - replyBoxShown: bool + replyingTo: Option[Post] replyBox: ReplyBox proc newState(): State = @@ -46,7 +31,7 @@ when defined(js): list: none[PostList](), loading: false, status: Http200, - replyBoxShown: true, + replyingTo: none[Post](), replyBox: newReplyBox() ) @@ -74,7 +59,12 @@ when defined(js): proc renderPostUrl(post: Post, thread: Thread): string = makeUri(fmt"/t/{thread.id}/p/{post.id}") + proc onReplyClick(e: Event, n: VNode, p: Option[Post]) = + state.replyingTo = p + state.replyBox.show() + proc genPost(post: Post, thread: Thread, isLoggedIn: bool): VNode = + let postCopy = post # TODO: Another workaround here, closure capture :( result = buildHtml(): tdiv(class="post"): tdiv(class="post-icon"): @@ -102,7 +92,8 @@ when defined(js): button(class="btn"): italic(class="far fa-flag") tdiv(class="reply-button"): - button(class="btn"): + button(class="btn", onClick=(e: Event, n: VNode) => + onReplyClick(e, n, some(postCopy))): italic(class="fas fa-reply") text " Reply" @@ -183,11 +174,9 @@ when defined(js): genPost(post, list.thread, isLoggedIn) prevPost = some(post) - if state.replyBoxShown and prevPost.isSome: - genTimePassed(prevPost.get(), none[Post]()) - if list.moreCount > 0: genLoadMore(list.posts.len) + elif prevPost.isSome: + genTimePassed(prevPost.get(), none[Post]()) - if state.replyBoxShown: - render(state.replyBox, list.thread) \ No newline at end of file + render(state.replyBox, list.thread, state.replyingTo) \ No newline at end of file diff --git a/redesign/replybox.nim b/redesign/replybox.nim index 35f3b81..f8718a5 100644 --- a/redesign/replybox.nim +++ b/redesign/replybox.nim @@ -1,28 +1,53 @@ when defined(js): - import strformat + import strformat, options + + from dom import getElementById, scrollIntoView, setTimeout include karax/prelude import karax / [vstyles, kajax, kdom] - import karaxutils, threadlist + import karaxutils, threadlist, post type ReplyBox* = ref object + shown: bool preview: bool proc newReplyBox*(): ReplyBox = ReplyBox() - proc render*(state: ReplyBox, thread: Thread): VNode = + proc performScroll() = + let replyBox = dom.document.getElementById("reply-box") + replyBox.scrollIntoView(false) + + proc show*(state: ReplyBox) = + # Scroll to the reply box. + if not state.shown: + # TODO: It would be nice for Karax to give us an event when it renders + # things. That way we can remove this crappy hack. + discard dom.window.setTimeout(performScroll, 50) + else: + performScroll() + + state.shown = true + + proc render*(state: ReplyBox, thread: Thread, post: Option[Post]): VNode = + if not state.shown: + return buildHtml(tdiv(id="reply-box")) + result = buildHtml(): - tdiv(class="information no-border"): + tdiv(class="information no-border", id="reply-box"): tdiv(class="information-icon"): italic(class="fas fa-reply") tdiv(class="information-main", style=style(StyleAttr.width, "100%")): tdiv(class="information-title"): - # text fmt("Replying to \"{thread.topic}\"") - # tdiv(class="information-content"): - tdiv(class="panel", id="reply-box"): + if post.isNone: + text fmt("Replying to \"{thread.topic}\"") + else: + text "Replying to " + renderUserMention(post.get().author) + tdiv(class="information-content"): + tdiv(class="panel"): tdiv(class="panel-nav"): ul(class="tab tab-block"): li(class=class({"active": not state.preview}, "tab-item")): diff --git a/redesign/threadlist.nim b/redesign/threadlist.nim index 7ab611f..fb5dddf 100644 --- a/redesign/threadlist.nim +++ b/redesign/threadlist.nim @@ -70,6 +70,12 @@ when defined(js): if user.isOnline: italic(class="avatar-presense online") + proc renderUserMention*(user: User): VNode = + result = buildHtml(): + # TODO: Add URL to profile. + span(class="user-mention"): + text "@" & user.name + proc genUserAvatars(users: seq[User]): VNode = result = buildHtml(td): for user in users: From 7faa8a8dae9608cd38bdd9bf4e5d91bfb613b467 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 12 May 2018 14:12:57 +0100 Subject: [PATCH 143/451] Adds margin to reply box panel. --- redesign/nimforum.scss | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index 34d5cad..c62b9b3 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -391,8 +391,15 @@ blockquote { } } -#reply-box .form-input { - // For reply text area. - margin-top: $control-padding-y*2; - resize: vertical; +#reply-box { + + .form-input { + // For reply text area. + margin-top: $control-padding-y*2; + resize: vertical; + } + + .panel { + margin-top: $control-padding-y*2; + } } \ No newline at end of file From 9f059bfade54c708c5ec092126fa255de9ecc4b8 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 12 May 2018 14:13:04 +0100 Subject: [PATCH 144/451] Fixes misleading post time rendering. --- redesign/threadlist.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redesign/threadlist.nim b/redesign/threadlist.nim index fb5dddf..224069e 100644 --- a/redesign/threadlist.nim +++ b/redesign/threadlist.nim @@ -86,7 +86,7 @@ when defined(js): let currentTime = getTime() let activityTime = fromUnix(activity) let duration = currentTime - activityTime - if duration.days > 300: + if currentTime.local().year != activityTime.local().year: return activityTime.local().format("MMM yyyy") elif duration.days > 30 and duration.days < 300: return activityTime.local().format("MMM dd") From 6fe8286509f45b6d3c873ed9350f26bf5a0eb720 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 12 May 2018 14:16:23 +0100 Subject: [PATCH 145/451] Fixes reply box's border. --- redesign/postlist.nim | 5 +++-- redesign/replybox.nim | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/redesign/postlist.nim b/redesign/postlist.nim index 78abd34..a6c8755 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -174,9 +174,10 @@ when defined(js): genPost(post, list.thread, isLoggedIn) prevPost = some(post) - if list.moreCount > 0: + let hasMore = list.moreCount > 0 + if hasMore: genLoadMore(list.posts.len) elif prevPost.isSome: genTimePassed(prevPost.get(), none[Post]()) - render(state.replyBox, list.thread, state.replyingTo) \ No newline at end of file + render(state.replyBox, list.thread, state.replyingTo, hasMore) \ No newline at end of file diff --git a/redesign/replybox.nim b/redesign/replybox.nim index f8718a5..883c1a2 100644 --- a/redesign/replybox.nim +++ b/redesign/replybox.nim @@ -31,12 +31,13 @@ when defined(js): state.shown = true - proc render*(state: ReplyBox, thread: Thread, post: Option[Post]): VNode = + proc render*(state: ReplyBox, thread: Thread, post: Option[Post], + hasMore: bool): VNode = if not state.shown: return buildHtml(tdiv(id="reply-box")) result = buildHtml(): - tdiv(class="information no-border", id="reply-box"): + tdiv(class=class({"no-border": hasMore}, "information"), id="reply-box"): tdiv(class="information-icon"): italic(class="fas fa-reply") tdiv(class="information-main", style=style(StyleAttr.width, "100%")): From 53c0bd89b9c5d49a496078cd6d127d01399e521d Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 12 May 2018 14:46:13 +0100 Subject: [PATCH 146/451] Use old post anchors. Highlight anchored post. --- redesign/nimforum.scss | 13 +++++++++++++ redesign/post.nim | 8 +++++++- redesign/postlist.nim | 5 +---- redesign/replybox.nim | 5 +++++ 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index c62b9b3..a7fcdcb 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -213,6 +213,19 @@ $views-color: #545d70; @extend .tile; border-top: 1px solid $border-color; padding-top: $control-padding-y-lg; + + &:target .post-main { + animation: highlight 2000ms ease-out; + } +} + +@keyframes highlight { + 0% { + background-color: lighten($primary-color, 20%); + } + 100% { + background-color: inherit; + } } .post-icon { diff --git a/redesign/post.nim b/redesign/post.nim index da0ac10..8d7b8da 100644 --- a/redesign/post.nim +++ b/redesign/post.nim @@ -1,4 +1,7 @@ +import strformat + import threadlist +import karaxutils type PostInfo* = object @@ -15,4 +18,7 @@ type ## post in a thread for example. history*: seq[PostInfo] ## If the post was edited this will contain the ## older versions of the post. - info*: PostInfo \ No newline at end of file + info*: PostInfo + +proc renderPostUrl*(post: Post, thread: Thread): string = + makeUri(fmt"/t/{thread.id}#{post.id}") \ No newline at end of file diff --git a/redesign/postlist.nim b/redesign/postlist.nim index a6c8755..a28585b 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -56,9 +56,6 @@ when defined(js): else: state.list = some(list) - proc renderPostUrl(post: Post, thread: Thread): string = - makeUri(fmt"/t/{thread.id}/p/{post.id}") - proc onReplyClick(e: Event, n: VNode, p: Option[Post]) = state.replyingTo = p state.replyBox.show() @@ -66,7 +63,7 @@ when defined(js): proc genPost(post: Post, thread: Thread, isLoggedIn: bool): VNode = let postCopy = post # TODO: Another workaround here, closure capture :( result = buildHtml(): - tdiv(class="post"): + tdiv(class="post", id = $post.id): tdiv(class="post-icon"): render(post.author, "post-avatar") tdiv(class="post-main"): diff --git a/redesign/replybox.nim b/redesign/replybox.nim index 883c1a2..752e01f 100644 --- a/redesign/replybox.nim +++ b/redesign/replybox.nim @@ -47,6 +47,11 @@ when defined(js): else: text "Replying to " renderUserMention(post.get().author) + tdiv(class="post-buttons", + style=style(StyleAttr.marginTop, "-0.3rem")): + a(href=renderPostUrl(post.get(), thread)): + button(class="btn"): + italic(class="fas fa-arrow-up") tdiv(class="information-content"): tdiv(class="panel"): tdiv(class="panel-nav"): From 4ff5df6be26a4f533eaa7a3c22be21548bd6a8a0 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 12 May 2018 16:43:52 +0100 Subject: [PATCH 147/451] Working post preview! --- forum.nim | 25 ++++++++++++++- redesign/karaxutils.nim | 6 +++- redesign/post.nim | 9 ++++-- redesign/replybox.nim | 67 ++++++++++++++++++++++++++++++++++++----- 4 files changed, 94 insertions(+), 13 deletions(-) diff --git a/forum.nim b/forum.nim index ce1bc6f..4d75d85 100644 --- a/forum.nim +++ b/forum.nim @@ -14,7 +14,7 @@ import cgi except setCookie import options import redesign/threadlist except User -import redesign/[category, postlist, error, header] +import redesign/[category, postlist, error, header, post] when not defined(windows): import bcrypt # TODO @@ -1186,6 +1186,29 @@ routes: let status = UserStatus(user: user) resp $(%status), "application/json" + post "/karax/preview": + createTFD() + if not c.loggedIn(): + let err = PostError( + errorFields: @[], + message: "Not logged in." + ) + resp Http401, $(%err), "application/json" + + let formData = request.formData + cond "msg" in formData + + let msg = formData["msg"].body + try: + let rendered = msg.rstToHtml() + resp Http200, rendered + except EParseError: + let err = PostError( + errorFields: @[], + message: getCurrentExceptionMsg() + ) + resp Http400, $(%err), "application/json" + get re"/karax/(.+)?": resp readFile("redesign/karax.html") diff --git a/redesign/karaxutils.nim b/redesign/karaxutils.nim index 28ada03..8f6334f 100644 --- a/redesign/karaxutils.nim +++ b/redesign/karaxutils.nim @@ -56,7 +56,11 @@ proc anchorCB*(e: kdom.Event, n: VNode) = # TODO: Why does this need disamb? type FormData* = ref object +proc newFormData*(): FormData + {.importcpp: "new FormData()", constructor.} proc newFormData*(form: dom.Element): FormData {.importcpp: "new FormData(@)", constructor.} proc get*(form: FormData, key: cstring): cstring - {.importcpp: "#.get(@)".} \ No newline at end of file + {.importcpp: "#.get(@)".} +proc append*(form: FormData, key, val: cstring) + {.importcpp: "#.append(@)".} \ No newline at end of file diff --git a/redesign/post.nim b/redesign/post.nim index 8d7b8da..616fa1d 100644 --- a/redesign/post.nim +++ b/redesign/post.nim @@ -1,7 +1,7 @@ import strformat import threadlist -import karaxutils + type PostInfo* = object @@ -20,5 +20,8 @@ type ## older versions of the post. info*: PostInfo -proc renderPostUrl*(post: Post, thread: Thread): string = - makeUri(fmt"/t/{thread.id}#{post.id}") \ No newline at end of file + +when defined(js): + import karaxutils + proc renderPostUrl*(post: Post, thread: Thread): string = + makeUri(fmt"/t/{thread.id}#{post.id}") \ No newline at end of file diff --git a/redesign/replybox.nim b/redesign/replybox.nim index 752e01f..cd7d2d3 100644 --- a/redesign/replybox.nim +++ b/redesign/replybox.nim @@ -1,20 +1,26 @@ when defined(js): - import strformat, options + import strformat, options, httpcore, json, sugar from dom import getElementById, scrollIntoView, setTimeout include karax/prelude import karax / [vstyles, kajax, kdom] - import karaxutils, threadlist, post + import karaxutils, threadlist, post, error type ReplyBox* = ref object shown: bool + text: kstring preview: bool + loading: bool + error: Option[PostError] + rendering: Option[kstring] proc newReplyBox*(): ReplyBox = - ReplyBox() + ReplyBox( + text: "" + ) proc performScroll() = let replyBox = dom.document.getElementById("reply-box") @@ -31,6 +37,39 @@ when defined(js): state.shown = true + proc onPreviewPost(httpStatus: int, response: kstring, state: ReplyBox) = + let status = httpStatus.HttpCode + if status == Http200: + kout(response) + state.rendering = some[kstring](response) + else: + # TODO: login has similar code, abstract this. + try: + let parsed = parseJson($response) + let error = to(parsed, PostError) + + state.error = some(error) + except: + kout(getCurrentExceptionMsg().cstring) + state.error = some(PostError( + errorFields: @[], + message: "Unknown error occurred." + )) + + proc onPreviewClick(e: Event, n: VNode, state: ReplyBox) = + state.preview = true + + let formData = newFormData() + formData.append("msg", state.text) + let uri = makeUri("/preview") + ajaxPost(uri, @[], cast[cstring](formData), + (s: int, r: kstring) => onPreviewPost(s, r, state)) + + proc onChange(e: Event, n: VNode, state: ReplyBox) = + # TODO: There should be a karax-way to do this. I guess I can just call + # `value` on the node? We need to document this better :) + state.text = cast[dom.TextAreaElement](n.dom).value + proc render*(state: ReplyBox, thread: Thread, post: Option[Post], hasMore: bool): VNode = if not state.shown: @@ -56,14 +95,26 @@ when defined(js): tdiv(class="panel"): tdiv(class="panel-nav"): ul(class="tab tab-block"): - li(class=class({"active": not state.preview}, "tab-item")): - a(href="#"): + li(class=class({"active": not state.preview}, "tab-item"), + onClick=(e: Event, n: VNode) => (state.preview = false)): + a(class="c-hand"): text "Message" - li(class=class({"active": state.preview}, "tab-item")): - a(href="#"): + li(class=class({"active": state.preview}, "tab-item"), + onClick=(e: Event, n: VNode) => + onPreviewClick(e, n, state)): + a(class="c-hand"): text "Preview" tdiv(class="panel-body"): - textarea(class="form-input", rows="5") + if state.preview: + if state.loading: + tdiv(class="loading") + elif state.rendering.isSome(): + verbatim(state.rendering.get()) + else: + textarea(class="form-input", rows="5", + onChange=(e: Event, n: VNode) => + onChange(e, n, state), + value=state.text) tdiv(class="panel-footer"): button(class="btn btn-primary float-right"): text "Reply" From 67a5869c10624c56f498b037d2e5101d4413ec82 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 12 May 2018 18:36:51 +0100 Subject: [PATCH 148/451] Implements rendering of RST using server and verbatim node in karax. --- forum.nim | 2 +- redesign/postlist.nim | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/forum.nim b/forum.nim index 4d75d85..b7998e7 100644 --- a/forum.nim +++ b/forum.nim @@ -1149,7 +1149,7 @@ routes: history: @[], # TODO: info: PostInfo( creation: post[2].parseInt, - content: post[1] + content: post[1].rstToHtml() ) )) diff --git a/redesign/postlist.nim b/redesign/postlist.nim index a28585b..8ffc423 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -76,7 +76,7 @@ when defined(js): a(href=renderPostUrl(post, thread), title=title): text renderActivity(post.info.creation) tdiv(class="post-content"): - p(text post.info.content) # TODO: RSTGEN + verbatim(post.info.content) tdiv(class="post-buttons"): tdiv(class="like-button"): button(class="btn"): From e13a4425f721a245c192fb4bcced1a3b77911268 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 13 May 2018 19:26:36 +0100 Subject: [PATCH 149/451] Show proper error when preview fails. --- redesign/replybox.nim | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/redesign/replybox.nim b/redesign/replybox.nim index cd7d2d3..921579e 100644 --- a/redesign/replybox.nim +++ b/redesign/replybox.nim @@ -38,6 +38,7 @@ when defined(js): state.shown = true proc onPreviewPost(httpStatus: int, response: kstring, state: ReplyBox) = + state.loading = false let status = httpStatus.HttpCode if status == Http200: kout(response) @@ -58,6 +59,8 @@ when defined(js): proc onPreviewClick(e: Event, n: VNode, state: ReplyBox) = state.preview = true + state.loading = true + state.error = none[PostError]() let formData = newFormData() formData.append("msg", state.text) @@ -108,6 +111,10 @@ when defined(js): if state.preview: if state.loading: tdiv(class="loading") + elif state.error.isSome(): + tdiv(class="toast toast-error", + style=style(StyleAttr.marginTop, "0.4rem")): + text state.error.get().message elif state.rendering.isSome(): verbatim(state.rendering.get()) else: From 8b01e452b602d0d9ff8f1543e72d1a795ae71276 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 13 May 2018 22:59:29 +0100 Subject: [PATCH 150/451] Improves post rendering to support multiple load more buttons. --- forum.nim | 83 ++++++++++++++++++++++++------------ redesign/post.nim | 4 +- redesign/postlist.nim | 99 +++++++++++++++++++++++++------------------ 3 files changed, 116 insertions(+), 70 deletions(-) diff --git a/forum.nim b/forum.nim index b7998e7..9321e0f 100644 --- a/forum.nim +++ b/forum.nim @@ -9,7 +9,7 @@ import os, strutils, times, md5, strtabs, math, db_sqlite, scgi, jester, asyncdispatch, asyncnet, cache, sequtils, - parseutils, utils, random, rst, ranks, recaptcha, json, re + parseutils, utils, random, rst, ranks, recaptcha, json, re, sugar import cgi except setCookie import options @@ -1026,6 +1026,20 @@ proc selectUser(userRow: seq[string]): threadlist.User = isOnline: isOnline ) +proc selectPost(postRow: seq[string], skippedPosts: seq[int]): Post = + return Post( + id: postRow[0].parseInt, + author: selectUser(@[postRow[4], postRow[5], postRow[6]]), + likes: @[], # TODO: + seen: false, # TODO: + history: @[], # TODO: + info: PostInfo( + creation: postRow[2].parseInt, + content: postRow[1].rstToHtml() + ), + moreBefore: skippedPosts + ) + proc selectThread(threadRow: seq[string]): Thread = const postsQuery = sql"""select count(*), strftime('%s', creation) from post @@ -1101,9 +1115,10 @@ routes: createTFD() var id = getInt(@"id", -1) - start = getInt(@"start", 0) - count = getInt(@"count", 5) + anchor = getInt(@"anchor", -1) cond id != -1 + const + count = 10 const threadsQuery = sql"""select id, name, views, strftime('%s', modified) from thread @@ -1124,34 +1139,50 @@ routes: from post p, person u where u.id = p.author and p.thread = ? and $# and (u.status <> 'Spammer' or p.author = ?) - order by p.id limit ?, ?""" % modClause + order by p.id""" % modClause ) - let pstCount = getValue( - db, - sql"select count(*) from post where thread = ?;", - id - ).parseInt() - let moreCount = max(0, pstCount - (start + count)) - var list = PostList( posts: @[], history: @[], - thread: thread, - moreCount: moreCount) - for post in getAllRows(db, postsQuery, id, c.userId, c.userId, - start, count): - list.posts.add(Post( - id: post[0].parseInt, - author: selectUser(@[post[4], post[5], post[6]]), - likes: @[], # TODO: - seen: false, # TODO: - history: @[], # TODO: - info: PostInfo( - creation: post[2].parseInt, - content: post[1].rstToHtml() - ) - )) + thread: thread + ) + let rows = getAllRows(db, postsQuery, id, c.userId, c.userId) + + var skippedPosts: seq[int] = @[] + for i in 0 ..< rows.len: + let id = rows[i][0].parseInt + + let addDetail = i < count or rows.len-i < count or id == anchor + + if addDetail: + let post = selectPost(rows[i], skippedPosts) + list.posts.add(post) + skippedPosts = @[] + else: + skippedPosts.add(id) + + resp $(%list), "application/json" + + get "/karax/specific_posts.json": + createTFD() + var + ids = parseJson(@"ids") + + cond ids.kind == JArray + let intIDs = ids.elems.map(x => x.getInt()) + let postsQuery = sql(""" + select p.id, p.content, strftime('%s', p.creation), p.author, + u.name, u.email, strftime('%s', u.lastOnline) + from post p, person u + where u.id = p.author and p.id in ($#) + order by p.id; + """ % intIDs.join(",")) # TODO: It's horrible that I have to do this. + + var list: seq[Post] = @[] + + for row in db.getAllRows(postsQuery): + list.add(selectPost(row, @[])) resp $(%list), "application/json" diff --git a/redesign/post.nim b/redesign/post.nim index 616fa1d..9ddbbb7 100644 --- a/redesign/post.nim +++ b/redesign/post.nim @@ -8,7 +8,7 @@ type creation*: int64 content*: string - Post* = object + Post* = ref object id*: int author*: User likes*: seq[User] ## Users that liked this post. @@ -19,7 +19,7 @@ type history*: seq[PostInfo] ## If the post was edited this will contain the ## older versions of the post. info*: PostInfo - + moreBefore*: seq[int] when defined(js): import karaxutils diff --git a/redesign/postlist.nim b/redesign/postlist.nim index 8ffc423..c585e64 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -10,7 +10,6 @@ type ## older versions of the thread (title/category ## changes). posts*: seq[Post] - moreCount*: int when defined(js): include karax/prelude @@ -38,7 +37,7 @@ when defined(js): var state = newState() - proc onPostList(httpStatus: int, response: kstring, start: int) = + proc onPostList(httpStatus: int, response: kstring) = state.loading = false state.status = httpStatus.HttpCode if state.status != Http200: return @@ -46,20 +45,62 @@ when defined(js): let parsed = parseJson($response) let list = to(parsed, PostList) - if state.list.isSome and state.list.get().thread.id == list.thread.id: - var old = state.list.get() - for i in 0.. onMorePosts(s, r, start, post) + ) + + proc genLoadMore(post: Post, start: int): VNode = + result = buildHtml(): + tdiv(class="information load-more-posts", + onClick=(e: Event, n: VNode) => onLoadMore(e, n, start, post)): + tdiv(class="information-icon"): + italic(class="fas fa-comment-dots") + tdiv(class="information-main"): + if state.loading: + tdiv(class="loading loading-lg") + else: + tdiv(class="information-title"): + text "Load more posts " + span(class="more-post-count"): + text "(" & $post.moreBefore.len & ")" + proc genPost(post: Post, thread: Thread, isLoggedIn: bool): VNode = let postCopy = post # TODO: Another workaround here, closure capture :( result = buildHtml(): @@ -94,31 +135,6 @@ when defined(js): italic(class="fas fa-reply") text " Reply" - proc onLoadMore(ev: Event, n: VNode) = - if state.loading: return - - state.loading = true - let start = n.getAttr("data-start").parseInt() - let threadId = state.list.get().thread.id - let uri = makeUri("posts.json", [("start", $start), ("id", $threadId)]) - ajaxGet(uri, @[], (s: int, r: kstring) => onPostList(s, r, start)) - - proc genLoadMore(start: int): VNode = - result = buildHtml(): - tdiv(class="information load-more-posts", - onClick=onLoadMore, - "data-start" = $start): - tdiv(class="information-icon"): - italic(class="fas fa-comment-dots") - tdiv(class="information-main"): - if state.loading: - tdiv(class="loading loading-lg") - else: - tdiv(class="information-title"): - text "Load more posts " - span(class="more-post-count"): - text "(" & $state.list.get().moreCount & ")" - proc genTimePassed(prevPost: Post, post: Option[Post]): VNode = var latestTime = if post.isSome: post.get().info.creation.fromUnix() @@ -153,7 +169,7 @@ when defined(js): if state.list.isNone or state.list.get().thread.id != threadId: let uri = makeUri("posts.json", ("id", $threadId)) - ajaxGet(uri, @[], (s: int, r: kstring) => onPostList(s, r, 0)) + ajaxGet(uri, @[], (s: int, r: kstring) => onPostList(s, r)) return buildHtml(tdiv(class="loading loading-lg")) @@ -165,16 +181,15 @@ when defined(js): render(list.thread.category) tdiv(class="posts"): var prevPost: Option[Post] = none[Post]() - for post in list.posts: + for i, post in list.posts: if prevPost.isSome: genTimePassed(prevPost.get(), some(post)) + if post.moreBefore.len > 0: + genLoadMore(post, i) genPost(post, list.thread, isLoggedIn) prevPost = some(post) - let hasMore = list.moreCount > 0 - if hasMore: - genLoadMore(list.posts.len) - elif prevPost.isSome: + if prevPost.isSome: genTimePassed(prevPost.get(), none[Post]()) - render(state.replyBox, list.thread, state.replyingTo, hasMore) \ No newline at end of file + render(state.replyBox, list.thread, state.replyingTo, false) \ No newline at end of file From ed5f715ae55a693444547bcc437019eff3c1be29 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 13 May 2018 23:15:07 +0100 Subject: [PATCH 151/451] Fixes styles so that content of post doesn't overflow its container. --- redesign/nimforum.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index a7fcdcb..1352f6d 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -241,6 +241,9 @@ $views-color: #545d70; @extend .tile-content; margin-bottom: $control-padding-y-lg*2; + // https://stackoverflow.com/a/41675912/492186 + flex: 1; + min-width: 0; } .post-title { From e1b72ed5662f1b6e9f625ec660a0e3ac5eaedb1f Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 14 May 2018 10:53:53 +0100 Subject: [PATCH 152/451] Normalize and adjust and styles. --- redesign/nimforum.scss | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index 1352f6d..a511334 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -3,7 +3,7 @@ // Define variables to override default ones $primary-color: #f99c19; $body-font-color: #292929; -$dark-color: #525252; +$dark-color: #505050; $label-color: #7cd2ff; $secondary-btn-color: #f1f1f1; @@ -418,4 +418,13 @@ blockquote { .panel { margin-top: $control-padding-y*2; } +} + +code { + color: $body-font-color; + background-color: $bg-color; +} + +tt { + @extend code; } \ No newline at end of file From 52f1e9c3651e02ce57098770884c2a128d8e73b5 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 14 May 2018 12:28:45 +0100 Subject: [PATCH 153/451] Implements syntax highlighting and
style. --- redesign/nimforum.scss | 11 ++++++++++- utils.nim | 9 +++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index a511334..5e87f3e 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -427,4 +427,13 @@ code { tt { @extend code; -} \ No newline at end of file +} + +hr { + background: $border-color; + height: $border-width; + margin: $unit-2 0; + border: 0; +} + +@import "syntax.scss"; \ No newline at end of file diff --git a/utils.nim b/utils.nim index 11f51af..b1c7552 100644 --- a/utils.nim +++ b/utils.nim @@ -38,8 +38,8 @@ type var docConfig: StringTableRef docConfig = rstgen.defaultConfig() -docConfig["doc.listing_start"] = "
"
-docConfig["doc.smiley_format"] = "/images/smilieys/$1.png"
+docConfig["doc.listing_start"] = "
"
+docConfig["doc.listing_end"] = "
" proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config = result = Config(smtpAddress: "", smtpPort: 25, smtpUser: "", @@ -81,7 +81,7 @@ proc blockquoteFinish(currentBlockquote, newNode: var XmlNode, n: XmlNode) = newNode.add(n) proc rstToHtml*(content: string): string = - result = rstgen.rstToHtml(content, {roSupportSmilies, roSupportMarkdown}, + result = rstgen.rstToHtml(content, {roSupportMarkdown}, docConfig) # Bolt on quotes. # TODO: Yes, this is ugly. I wrote it quickly. PRs welcome ;) @@ -116,7 +116,8 @@ proc rstToHtml*(content: string): string = blockquoteFinish(currentBlockquote, newNode, n) else: blockquoteFinish(currentBlockquote, newNode, n) - result = $newNode + result = "" + add(result, newNode, indWidth=0, addNewLines=false) except: echo("[WARNING] Could not parse rst html.") From 53ed3717b889d99073650b24e8752dbaa3de4c7d Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 14 May 2018 12:37:15 +0100 Subject: [PATCH 154/451] Hide "Run" button when appropriate and "none" language caption. --- redesign/nimforum.scss | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index 5e87f3e..8bbc407 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -356,6 +356,17 @@ blockquote { @extend .toast-success; } +.code { + // Don't show the "none". + &[data-lang="none"]::before { + content: ""; + } + + &:not([data-lang="Nim"]) > .code-buttons { + display: none; + } +} + .information { @extend .tile; border-top: 1px solid $border-color; From eeda9270852c7ec9ca4c3a9d66f15f96526d5fc3 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 14 May 2018 14:41:12 +0100 Subject: [PATCH 155/451] Implements mention highlighting in posts. --- forum.nim | 5 +-- utils.nim | 121 +++++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 90 insertions(+), 36 deletions(-) diff --git a/forum.nim b/forum.nim index 9321e0f..2752ac6 100644 --- a/forum.nim +++ b/forum.nim @@ -281,9 +281,6 @@ proc validThreadId(c: TForumData): bool = result = getValue(db, sql"select id from thread where id = ?", $c.threadId).len > 0 -const - SecureChars = {'A'..'Z', 'a'..'z', '0'..'9', '_', '\128'..'\255'} - proc setError(c: TForumData, field, msg: string): bool {.inline.} = c.invalidField = field c.errorMsg = "Error: " & msg @@ -292,7 +289,7 @@ proc setError(c: TForumData, field, msg: string): bool {.inline.} = proc register(c: TForumData, name, pass, antibot, userIp, email: string): Future[bool] {.async.} = # Username validation: - if name.len == 0 or not allCharsInSet(name, SecureChars): + if name.len == 0 or not allCharsInSet(name, UsernameIdent): return setError(c, "name", "Invalid username!") if getValue(db, sql"select name from person where name = ?", name).len > 0: return setError(c, "name", "Username already exists!") diff --git a/utils.nim b/utils.nim index b1c7552..690a007 100644 --- a/utils.nim +++ b/utils.nim @@ -2,6 +2,12 @@ import asyncdispatch, smtp, strutils, json, os, rst, rstgen, xmltree, strtabs, htmlparser, streams, parseutils, options from times import getTime, getGMTime, format +# Used to be: +# {'A'..'Z', 'a'..'z', '0'..'9', '_', '\128'..'\255'} +let + UsernameIdent* = IdentChars # TODO: Double check that everyone follows this. + + proc parseInt*(s: string, value: var int, validRange: Slice[int]) {. noSideEffect.} = ## parses `s` into an integer in the range `validRange`. If successful, @@ -80,46 +86,97 @@ proc blockquoteFinish(currentBlockquote, newNode: var XmlNode, n: XmlNode) = currentBlockquote = newElement("blockquote") newNode.add(n) +proc processQuotes(node: XmlNode): XmlNode = + # Bolt on quotes. + # TODO: Yes, this is ugly. I wrote it quickly. PRs welcome ;) + result = newElement("div") + var currentBlockquote = newElement("blockquote") + for n in items(node): + case n.kind + of xnElement: + case n.tag + of "p": + let (nesting, contentNode, _) = processGT(n, "p") + if nesting > 0: + var bq = currentBlockquote + for i in 1 ..< nesting: + var newBq = bq.child("blockquote") + if newBq.isNil: + newBq = newElement("blockquote") + bq.add(newBq) + bq = newBq + bq.insert(contentNode, if bq.len == 0: 0 else: bq.len) + else: + blockquoteFinish(currentBlockquote, result, n) + else: + blockquoteFinish(currentBlockquote, result, n) + of xnText: + if n.text[0] == '\10': + result.add(n) + else: + blockquoteFinish(currentBlockquote, result, n) + else: + blockquoteFinish(currentBlockquote, result, n) + +proc replaceMentions(node: XmlNode): seq[XmlNode] = + assert node.kind == xnText + result = @[] + + var current = "" + var i = 0 + while i < len(node.text): + i += parseUntil(node.text, current, {'@'}, i) + if i >= len(node.text): break + if node.text[i] == '@': + i.inc # Skip @ + var username = "" + i += parseWhile(node.text, username, UsernameIdent, i) + + let el = <>span( + class="user-mention", + data-username=username, + newText("@" & username) + ) + + result.add(newText(current)) + current = "" + result.add(el) + + result.add(newText(current)) + +proc processMentions(node: XmlNode): XmlNode = + case node.kind + of xnText: + result = newElement("span") + for child in replaceMentions(node): + result.add(child) + of xnElement: + case node.tag + of "pre", "code", "tt": + return node + else: + result = newElement(node.tag) + for n in items(node): + result.add(processMentions(n)) + else: + return node + + + proc rstToHtml*(content: string): string = result = rstgen.rstToHtml(content, {roSupportMarkdown}, docConfig) - # Bolt on quotes. - # TODO: Yes, this is ugly. I wrote it quickly. PRs welcome ;) try: var node = parseHtml(newStringStream(result)) - var newNode = newElement("div") if node.kind == xnElement: - var currentBlockquote = newElement("blockquote") - for n in items(node): - case n.kind - of xnElement: - case n.tag - of "p": - let (nesting, contentNode, tag) = processGT(n, "p") - if nesting > 0: - var bq = currentBlockquote - for i in 1 .. Date: Mon, 14 May 2018 18:28:55 +0100 Subject: [PATCH 156/451] Fixes "mention" transformation and parsing quirks. --- utils.nim | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/utils.nim b/utils.nim index 690a007..543626f 100644 --- a/utils.nim +++ b/utils.nim @@ -132,15 +132,18 @@ proc replaceMentions(node: XmlNode): seq[XmlNode] = var username = "" i += parseWhile(node.text, username, UsernameIdent, i) - let el = <>span( - class="user-mention", - data-username=username, - newText("@" & username) - ) + if username.len == 0: + result.add(newText(current & "@")) + else: + let el = <>span( + class="user-mention", + data-username=username, + newText("@" & username) + ) - result.add(newText(current)) - current = "" - result.add(el) + result.add(newText(current)) + current = "" + result.add(el) result.add(newText(current)) @@ -156,27 +159,25 @@ proc processMentions(node: XmlNode): XmlNode = return node else: result = newElement(node.tag) + result.attrs = node.attrs for n in items(node): result.add(processMentions(n)) else: return node - - proc rstToHtml*(content: string): string = result = rstgen.rstToHtml(content, {roSupportMarkdown}, docConfig) try: var node = parseHtml(newStringStream(result)) if node.kind == xnElement: - let quotedNode = processQuotes(node) - let mentionedNode = processMentions(quotedNode) + node = processQuotes(node) - result = "" - add(result, mentionedNode, indWidth=0, addNewLines=false) + node = processMentions(node) + result = "" + add(result, node, indWidth=0, addNewLines=false) except: - raise - # echo("[WARNING] Could not parse rst html.") + echo("[WARNING] Could not parse rst html.") proc sendMail(config: Config, subject, message, recipient: string, from_addr = "forum@nim-lang.org", otherHeaders:seq[(string, string)] = @[]) {.async.} = if config.smtpAddress.len == 0: From 01315d2b34041942bbe7831230fb6199f4b0df42 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 14 May 2018 18:29:12 +0100 Subject: [PATCH 157/451] Adds missing syntax.scss syntax highlighting styles file. --- redesign/syntax.scss | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 redesign/syntax.scss diff --git a/redesign/syntax.scss b/redesign/syntax.scss new file mode 100644 index 0000000..14dfa49 --- /dev/null +++ b/redesign/syntax.scss @@ -0,0 +1,13 @@ +pre .Comment { color:#618f0b; font-style:italic; } +pre .Keyword { color:rgb(39, 141, 182); font-weight:bold; } +pre .Type { color:#128B7D; font-weight:bold; } +pre .Operator { font-weight: bold; } +pre .atr { color:#128B7D; font-weight:bold; font-style:italic; } +pre .def { color:#CAD6E4; font-weight:bold; font-style:italic; } +pre .StringLit { color:rgb(190, 15, 15); font-weight:bold; } +pre .DecNumber, pre .FloatNumber { color:#8AB647; } +pre .tab { border-left:1px dotted rgba(67,168,207,0.4); } +pre .EscapeSequence +{ + color: #C08D12; +} \ No newline at end of file From 073f274e0471cb6c6b90d8cf901d73959886457b Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 14 May 2018 19:14:04 +0100 Subject: [PATCH 158/451] Small fix to blockquote parser. Decided to leave the blockquote parser alone. It works well enough and the small bugs it contains aren't critical. --- utils.nim | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/utils.nim b/utils.nim index 543626f..8f4d9a8 100644 --- a/utils.nim +++ b/utils.nim @@ -105,7 +105,7 @@ proc processQuotes(node: XmlNode): XmlNode = newBq = newElement("blockquote") bq.add(newBq) bq = newBq - bq.insert(contentNode, if bq.len == 0: 0 else: bq.len) + bq.add(contentNode) else: blockquoteFinish(currentBlockquote, result, n) else: @@ -172,7 +172,6 @@ proc rstToHtml*(content: string): string = var node = parseHtml(newStringStream(result)) if node.kind == xnElement: node = processQuotes(node) - node = processMentions(node) result = "" add(result, node, indWidth=0, addNewLines=false) From 7635478b34ca6167269a247e2217c8163612b39c Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 14 May 2018 21:11:18 +0100 Subject: [PATCH 159/451] Implements posting of replies. --- forum.nim | 59 ++++++++++++++++++++++++++++++++++++++ redesign/postlist.nim | 39 ++++++++++++++++--------- redesign/replybox.nim | 66 +++++++++++++++++++++++++++++++++++++------ 3 files changed, 143 insertions(+), 21 deletions(-) diff --git a/forum.nim b/forum.nim index 2752ac6..581510e 100644 --- a/forum.nim +++ b/forum.nim @@ -1069,6 +1069,32 @@ proc selectThread(threadRow: seq[string]): Thread = return thread +proc executeReply(c: TForumData, threadId: int, content: string, + replyingTo: int): int64 = + # TODO: Refactor TForumData. + assert c.loggedIn() + + let subject = "" # TODO: Remove this redundant field. + if rateLimitCheck(c): + raise newException(ForumError, "You're posting too fast!") + + # TODO: Replying to. + let retID = insertID( + db, + crud(crCreate, "post", "author", "ip", "header", "content", "thread"), + c.userId, c.req.ip, subject, content, $threadId, "" + ) + discard tryExec( + db, + crud(crCreate, "post_fts", "id", "header", "content"), + retID.int, subject, content + ) + + exec(db, sql"update thread set modified = DATETIME('now') where id = ?", + $c.threadId) + + return retID + initialise() routes: @@ -1237,6 +1263,39 @@ routes: ) resp Http400, $(%err), "application/json" + post "/karax/createPost": + createTFD() + if not c.loggedIn(): + let err = PostError( + errorFields: @[], + message: "Not logged in." + ) + resp Http401, $(%err), "application/json" + + let formData = request.formData + cond "msg" in formData + cond "threadId" in formData + + let msg = formData["msg"].body + let threadId = getInt(formData["threadId"].body, -1) + cond threadId != -1 + + let replyingTo = + if "replyingTo" in formData: + getInt(formData["replyingTo"].body, -1) + else: + -1 + + try: + let id = executeReply(c, threadId, msg, replyingTo) + resp Http200, $(%id), "application/json" + except ForumError: + let err = PostError( + errorFields: @[], + message: getCurrentExceptionMsg() + ) + resp Http400, $(%err), "application/json" + get re"/karax/(.+)?": resp readFile("redesign/karax.html") diff --git a/redesign/postlist.nim b/redesign/postlist.nim index c585e64..2b667ea 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -25,13 +25,14 @@ when defined(js): replyingTo: Option[Post] replyBox: ReplyBox + proc onReplyPosted(id: int) proc newState(): State = State( list: none[PostList](), loading: false, status: Http200, replyingTo: none[Post](), - replyBox: newReplyBox() + replyBox: newReplyBox(onReplyPosted) ) var @@ -47,7 +48,7 @@ when defined(js): state.list = some(list) - proc onMorePosts(httpStatus: int, response: kstring, start: int, post: Post) = + proc onMorePosts(httpStatus: int, response: kstring, start: int) = state.loading = false state.status = httpStatus.HttpCode if state.status != Http200: return @@ -62,20 +63,21 @@ when defined(js): # Save a list of the IDs which have not yet been loaded into the top-most # post. - for id in post.moreBefore: - if id notin idsLoaded: - state.list.get().posts[start].moreBefore.add(id) - post.moreBefore = @[] + let postIndex = start+list.len + # The following check is necessary because we reuse this proc to load + # a newly created post. + if postIndex < state.list.get().posts.len: + let post = state.list.get().posts[postIndex] + var newPostIds: seq[int] = @[] + for id in post.moreBefore: + if id notin idsLoaded: + newPostIds.add(id) + post.moreBefore = newPostIds - proc onReplyClick(e: Event, n: VNode, p: Option[Post]) = - state.replyingTo = p - state.replyBox.show() - - proc onLoadMore(ev: Event, n: VNode, start: int, post: Post) = + proc loadMore(start: int, ids: seq[int]) = if state.loading: return state.loading = true - let ids = post.moreBefore # TODO: Don't load all! let uri = makeUri( "specific_posts.json", [("ids", $(%ids))] @@ -83,9 +85,20 @@ when defined(js): ajaxGet( uri, @[], - (s: int, r: kstring) => onMorePosts(s, r, start, post) + (s: int, r: kstring) => onMorePosts(s, r, start) ) + proc onReplyPosted(id: int) = + ## Executed when a reply has been successfully posted. + loadMore(state.list.get().posts.len, @[id]) + + proc onReplyClick(e: Event, n: VNode, p: Option[Post]) = + state.replyingTo = p + state.replyBox.show() + + proc onLoadMore(ev: Event, n: VNode, start: int, post: Post) = + loadMore(start, post.moreBefore) # TODO: Don't load all! + proc genLoadMore(post: Post, start: int): VNode = result = buildHtml(): tdiv(class="information load-more-posts", diff --git a/redesign/replybox.nim b/redesign/replybox.nim index 921579e..b92dc88 100644 --- a/redesign/replybox.nim +++ b/redesign/replybox.nim @@ -16,10 +16,12 @@ when defined(js): loading: bool error: Option[PostError] rendering: Option[kstring] + onPost: proc (id: int) - proc newReplyBox*(): ReplyBox = + proc newReplyBox*(onPost: proc (id: int)): ReplyBox = ReplyBox( - text: "" + text: "", + onPost: onPost ) proc performScroll() = @@ -68,6 +70,45 @@ when defined(js): ajaxPost(uri, @[], cast[cstring](formData), (s: int, r: kstring) => onPreviewPost(s, r, state)) + proc onReplyPost(httpStatus: int, response: kstring, state: ReplyBox) = + state.loading = false + let status = httpStatus.HttpCode + if status == Http200: + state.text = "" + state.shown = false + state.onPost(parseJson($response).getInt()) + else: + # TODO: login has similar code, abstract this. + try: + let parsed = parseJson($response) + let error = to(parsed, PostError) + + state.error = some(error) + except: + kout(getCurrentExceptionMsg().cstring) + state.error = some(PostError( + errorFields: @[], + message: "Unknown error occurred." + )) + + proc onReplyClick(e: Event, n: VNode, state: ReplyBox, + thread: Thread, replyingTo: Option[Post]) = + state.loading = true + state.error = none[PostError]() + + let formData = newFormData() + formData.append("msg", state.text) + formData.append("threadId", $thread.id) + if replyingTo.isSome: + formData.append("replyingTo", $replyingTo.get().id) + let uri = makeUri("/createPost") + ajaxPost(uri, @[], cast[cstring](formData), + (s: int, r: kstring) => onReplyPost(s, r, state)) + + proc onCancelClick(e: Event, n: VNode, state: ReplyBox) = + # TODO: Double check reply box contents and ask user whether to discard. + state.shown = false + proc onChange(e: Event, n: VNode, state: ReplyBox) = # TODO: There should be a karax-way to do this. I guess I can just call # `value` on the node? We need to document this better :) @@ -111,10 +152,6 @@ when defined(js): if state.preview: if state.loading: tdiv(class="loading") - elif state.error.isSome(): - tdiv(class="toast toast-error", - style=style(StyleAttr.marginTop, "0.4rem")): - text state.error.get().message elif state.rendering.isSome(): verbatim(state.rendering.get()) else: @@ -122,8 +159,21 @@ when defined(js): onChange=(e: Event, n: VNode) => onChange(e, n, state), value=state.text) + + if state.error.isSome(): + tdiv(class="toast toast-error", + style=style(StyleAttr.marginTop, "0.4rem")): + text state.error.get().message + tdiv(class="panel-footer"): - button(class="btn btn-primary float-right"): + button(class=class( + {"loading": state.loading}, + "btn btn-primary float-right" + ), + onClick=(e: Event, n: VNode) => + onReplyClick(e, n, state, thread, post)): text "Reply" - button(class="btn btn-link float-right"): + button(class="btn btn-link float-right", + onClick=(e: Event, n: VNode) => + onCancelClick(e, n, state)): text "Cancel" \ No newline at end of file From 7f895123f96140626fc227e66daf5e93f1dbe1ec Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 14 May 2018 21:23:44 +0100 Subject: [PATCH 160/451] Refactor on*Post procedures. --- redesign/error.nim | 21 ++++++++++++++++++++- redesign/login.nim | 17 +---------------- redesign/replybox.nim | 34 ++-------------------------------- 3 files changed, 23 insertions(+), 49 deletions(-) diff --git a/redesign/error.nim b/redesign/error.nim index fb96758..af04e73 100644 --- a/redesign/error.nim +++ b/redesign/error.nim @@ -40,4 +40,23 @@ when defined(js): let e = error.get() if (e.errorFields.len == 1 and e.errorFields[0] == name) or isLast: p(class="form-input-hint"): - text e.message \ No newline at end of file + text e.message + + template postFinished*(onSuccess: untyped): untyped = + state.loading = false + let status = httpStatus.HttpCode + if status == Http200: + onSuccess + else: + # TODO: Karax should pass the content-type... + try: + let parsed = parseJson($response) + let error = to(parsed, PostError) + + state.error = some(error) + except: + kout(getCurrentExceptionMsg().cstring) + state.error = some(PostError( + errorFields: @[], + message: "Unknown error occurred." + )) \ No newline at end of file diff --git a/redesign/login.nim b/redesign/login.nim index 94a9228..6485906 100644 --- a/redesign/login.nim +++ b/redesign/login.nim @@ -17,24 +17,9 @@ when defined(js): error: Option[PostError] proc onLogInPost(httpStatus: int, response: kstring, state: LoginModal) = - state.loading = false - let status = httpStatus.HttpCode - if status == Http200: + postFinished: state.shown = false state.onLogIn() - else: - # TODO: Karax should pass the content-type... - try: - let parsed = parseJson($response) - let error = to(parsed, PostError) - - state.error = some(error) - except: - kout(getCurrentExceptionMsg().cstring) - state.error = some(PostError( - errorFields: @[], - message: "Unknown error occurred." - )) proc onLogInClick(ev: Event, n: VNode, state: LoginModal) = state.loading = true diff --git a/redesign/replybox.nim b/redesign/replybox.nim index b92dc88..04852f2 100644 --- a/redesign/replybox.nim +++ b/redesign/replybox.nim @@ -40,24 +40,9 @@ when defined(js): state.shown = true proc onPreviewPost(httpStatus: int, response: kstring, state: ReplyBox) = - state.loading = false - let status = httpStatus.HttpCode - if status == Http200: + postFinished: kout(response) state.rendering = some[kstring](response) - else: - # TODO: login has similar code, abstract this. - try: - let parsed = parseJson($response) - let error = to(parsed, PostError) - - state.error = some(error) - except: - kout(getCurrentExceptionMsg().cstring) - state.error = some(PostError( - errorFields: @[], - message: "Unknown error occurred." - )) proc onPreviewClick(e: Event, n: VNode, state: ReplyBox) = state.preview = true @@ -71,25 +56,10 @@ when defined(js): (s: int, r: kstring) => onPreviewPost(s, r, state)) proc onReplyPost(httpStatus: int, response: kstring, state: ReplyBox) = - state.loading = false - let status = httpStatus.HttpCode - if status == Http200: + postFinished: state.text = "" state.shown = false state.onPost(parseJson($response).getInt()) - else: - # TODO: login has similar code, abstract this. - try: - let parsed = parseJson($response) - let error = to(parsed, PostError) - - state.error = some(error) - except: - kout(getCurrentExceptionMsg().cstring) - state.error = some(PostError( - errorFields: @[], - message: "Unknown error occurred." - )) proc onReplyClick(e: Event, n: VNode, state: ReplyBox, thread: Thread, replyingTo: Option[Post]) = From 681c3ef19dc2fd8a76716304f026e0745b3a6f84 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 14 May 2018 21:40:49 +0100 Subject: [PATCH 161/451] Fixes user menu on larger screens. --- redesign/nimforum.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index 8bbc407..84ee7f9 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -83,8 +83,10 @@ $logo-height: $navbar-height - 20px; .menu-right { // To make sure the user menu doesn't move off the screen. - left: auto; - right: 0; + @media (max-width: 1600px) { + left: auto; + right: 0; + } position: absolute; } From 6380ea699d9c07c48cf456de62b954ab17dc27a0 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 14 May 2018 21:53:13 +0100 Subject: [PATCH 162/451] Use big
to hide user menu in a more intuitive fashion. --- redesign/usermenu.nim | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/redesign/usermenu.nim b/redesign/usermenu.nim index 9673dcf..6d0d8c7 100644 --- a/redesign/usermenu.nim +++ b/redesign/usermenu.nim @@ -19,15 +19,31 @@ when defined(js): onLogout: onLogout ) + proc onClick(e: Event, n: VNode, state: UserMenu) = + state.shown = not state.shown + proc render*(state: UserMenu, user: User): VNode = result = buildHtml(): tdiv(): figure(class="avatar c-hand", - onClick=(e: Event, n: VNode) => (state.shown = not state.shown)): + onClick=(e: Event, n: VNode) => onClick(e, n, state)): img(src=user.avatarUrl, title=user.name) if user.isOnline: italic(class="avatar-presense online") + tdiv(style=style([ + (StyleAttr.width, kstring"999999px"), + (StyleAttr.height, kstring"999999px"), + (StyleAttr.position, kstring"absolute"), + (StyleAttr.left, kstring"0"), + (StyleAttr.top, kstring"0"), + ( + StyleAttr.display, + if state.shown: kstring"block" else: kstring"none" + ) + ]), + onClick=(e: Event, n: VNode) => (state.shown = false)) + ul(class="menu menu-right", style=style( StyleAttr.display, if state.shown: "inherit" else: "none" )): From d345bf76ae62bbd5bbea94f1259e036b31625069 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 14 May 2018 22:45:27 +0100 Subject: [PATCH 163/451] Implements naive /profile.json endpoint. --- forum.nim | 64 +++++++++++++++++++++++++++++++++++------ ranks.nim | 11 ------- redesign/profile.nim | 11 +++++++ redesign/threadlist.nim | 16 ++++++++++- 4 files changed, 81 insertions(+), 21 deletions(-) delete mode 100644 ranks.nim create mode 100644 redesign/profile.nim diff --git a/forum.nim b/forum.nim index 581510e..716f060 100644 --- a/forum.nim +++ b/forum.nim @@ -9,12 +9,12 @@ import os, strutils, times, md5, strtabs, math, db_sqlite, scgi, jester, asyncdispatch, asyncnet, cache, sequtils, - parseutils, utils, random, rst, ranks, recaptcha, json, re, sugar + parseutils, utils, random, rst, recaptcha, json, re, sugar import cgi except setCookie import options import redesign/threadlist except User -import redesign/[category, postlist, error, header, post] +import redesign/[category, postlist, error, header, post, profile] when not defined(windows): import bcrypt # TODO @@ -1016,17 +1016,17 @@ template createTFD() = #[ DB functions. TODO: Move to another module? ]# proc selectUser(userRow: seq[string]): threadlist.User = - let isOnline = getTime().toUnix() - userRow[2].parseInt > (60*5) return threadlist.User( name: userRow[0], avatarUrl: userRow[1].getGravatarUrl(), - isOnline: isOnline + lastOnline: userRow[2].parseInt, + rank: parseEnum[Rank](userRow[3]) ) proc selectPost(postRow: seq[string], skippedPosts: seq[int]): Post = return Post( id: postRow[0].parseInt, - author: selectUser(@[postRow[4], postRow[5], postRow[6]]), + author: selectUser(@[postRow[4], postRow[5], postRow[6], postRow[7]]), likes: @[], # TODO: seen: false, # TODO: history: @[], # TODO: @@ -1043,7 +1043,7 @@ proc selectThread(threadRow: seq[string]): Thread = where thread = ? order by creation asc limit 1;""" const usersListQuery = - sql"""select distinct name, email, strftime('%s', lastOnline) + sql"""select distinct name, email, strftime('%s', lastOnline), status from person where id in (select author from post where thread = ?) limit 5;""" # TODO: Order by most posts. @@ -1158,7 +1158,7 @@ routes: let postsQuery = sql( """select p.id, p.content, strftime('%s', p.creation), p.author, - u.name, u.email, strftime('%s', u.lastOnline) + u.name, u.email, strftime('%s', u.lastOnline), u.status from post p, person u where u.id = p.author and p.thread = ? and $# and (u.status <> 'Spammer' or p.author = ?) @@ -1196,7 +1196,7 @@ routes: let intIDs = ids.elems.map(x => x.getInt()) let postsQuery = sql(""" select p.id, p.content, strftime('%s', p.creation), p.author, - u.name, u.email, strftime('%s', u.lastOnline) + u.name, u.email, strftime('%s', u.lastOnline), u.status from post p, person u where u.id = p.author and p.id in ($#) order by p.id; @@ -1209,6 +1209,51 @@ routes: resp $(%list), "application/json" + get "/karax/profile.json": + createTFD() + var + username = @"username" + + let postsQuery = sql(""" + select p.id, p.content, strftime('%s', p.creation), p.author, + u.name, u.email, strftime('%s', u.lastOnline), u.status + from post p, person u + where u.id = p.author and u.name = ? + order by p.id desc; + """) + + var profile = Profile( + threads: @[], + posts: @[] + ) + + let rows = db.getAllRows(postsQuery, username) + profile.user = selectUser(@[ + rows[0][4], rows[0][5], rows[0][6], rows[0][7] + ]) + + if c.rank >= Admin: + profile.email = some(rows[0][5]) + + for row in rows: + let firstPostForThread = getRow(db, + sql"""select t.id, t.name, t.views, t.modified, p.id + from thread t, post p + where p.thread = t.id + order by p.id limit 1""", row[0]) + + # Check if the thread that contains this post was created by the user. + if firstPostForThread[4] == row[0]: + profile.threads.add( + selectThread(firstPostForThread) + ) + + profile.posts.add( + selectPost(row, @[]) + ) + + resp $(%profile), "application/json" + post "/karax/login": createTFD() let formData = request.formData @@ -1232,7 +1277,8 @@ routes: some(threadlist.User( name: c.username, avatarUrl: c.email.getGravatarUrl(), - isOnline: true + lastOnline: getTime().toUnix(), + rank: c.rank )) else: none[threadlist.User]() diff --git a/ranks.nim b/ranks.nim deleted file mode 100644 index 3b518f4..0000000 --- a/ranks.nim +++ /dev/null @@ -1,11 +0,0 @@ - -type - Rank* = enum ## serialized as 'status' - Spammer ## spammer: every post is invisible - Troll ## troll: cannot write new posts - EmailUnconfirmed ## member with unconfirmed email address - Moderated ## new member: posts manually reviewed before everybody - ## can see them - User ## Ordinary user - Moderator ## Moderator: can ban/moderate users - Admin ## Admin: can do everything diff --git a/redesign/profile.nim b/redesign/profile.nim new file mode 100644 index 0000000..b2fc8c7 --- /dev/null +++ b/redesign/profile.nim @@ -0,0 +1,11 @@ +import options +import threadlist, post +type + Profile* = object + user*: User + joinTime*: int64 + threads*: seq[Thread] + posts*: seq[Post] + # Information that only admins should see. + email*: Option[string] + diff --git a/redesign/threadlist.nim b/redesign/threadlist.nim index 224069e..35b7d58 100644 --- a/redesign/threadlist.nim +++ b/redesign/threadlist.nim @@ -3,10 +3,21 @@ import strformat, times, options, json, httpcore, sugar import category type + Rank* {.pure.} = enum ## serialized as 'status' + Spammer ## spammer: every post is invisible + Troll ## troll: cannot write new posts + EmailUnconfirmed ## member with unconfirmed email address + Moderated ## new member: posts manually reviewed before everybody + ## can see them + User ## Ordinary user + Moderator ## Moderator: can ban/moderate users + Admin ## Admin: can do everything + User* = object name*: string avatarUrl*: string - isOnline*: bool + lastOnline*: int64 + rank*: Rank Thread* = object id*: int @@ -25,6 +36,9 @@ type lastVisit*: int64 ## Unix timestamp moreCount*: int ## How many more threads are left +proc isOnline*(user: User): bool = + return getTime().toUnix() - user.lastOnline > (60*5) + when defined(js): include karax/prelude import karax / [vstyles, kajax, kdom] From 2dd9dd52a2cafc5240a15e773e2785d8800e2abe Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 15 May 2018 14:14:37 +0100 Subject: [PATCH 164/451] Implements simple stats in profile view. --- forum.nim | 10 +++--- redesign/forum.nim | 12 +++++-- redesign/nimforum.scss | 52 ++++++++++++++++++++++++++++++- redesign/profile.nim | 71 ++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 135 insertions(+), 10 deletions(-) diff --git a/forum.nim b/forum.nim index 716f060..49c85bb 100644 --- a/forum.nim +++ b/forum.nim @@ -1015,10 +1015,10 @@ template createTFD() = #[ DB functions. TODO: Move to another module? ]# -proc selectUser(userRow: seq[string]): threadlist.User = +proc selectUser(userRow: seq[string], avatarSize: int=80): threadlist.User = return threadlist.User( name: userRow[0], - avatarUrl: userRow[1].getGravatarUrl(), + avatarUrl: userRow[1].getGravatarUrl(avatarSize), lastOnline: userRow[2].parseInt, rank: parseEnum[Rank](userRow[3]) ) @@ -1216,7 +1216,8 @@ routes: let postsQuery = sql(""" select p.id, p.content, strftime('%s', p.creation), p.author, - u.name, u.email, strftime('%s', u.lastOnline), u.status + u.name, u.email, strftime('%s', u.lastOnline), u.status, + strftime('%s', u.creation) from post p, person u where u.id = p.author and u.name = ? order by p.id desc; @@ -1230,7 +1231,8 @@ routes: let rows = db.getAllRows(postsQuery, username) profile.user = selectUser(@[ rows[0][4], rows[0][5], rows[0][6], rows[0][7] - ]) + ], avatarSize=200) + profile.joinTime = rows[0][8].parseInt() if c.rank >= Admin: profile.email = some(rows[0][5]) diff --git a/redesign/forum.nim b/redesign/forum.nim index 686fc2a..9de7c75 100644 --- a/redesign/forum.nim +++ b/redesign/forum.nim @@ -4,16 +4,18 @@ from dom import window, Location include karax/prelude import jester/patterns -import threadlist, postlist, header +import threadlist, postlist, header, profile import karaxutils type State = ref object url: Location + profile: ProfileState proc newState(): State = State( - url: window.location + url: window.location, + profile: newProfileState() ) var state = newState() @@ -44,7 +46,11 @@ proc render(): VNode = result = buildHtml(tdiv()): renderHeader() route([ - r("/t/@id?", + r("/profile/@username", + (params: Params) => + (render(state.profile, params["username"])) + ), + r("/t/@id", (params: Params) => (renderPostList(params["id"].parseInt(), isLoggedIn())) ), diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index 84ee7f9..dcab208 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -449,4 +449,54 @@ hr { border: 0; } -@import "syntax.scss"; \ No newline at end of file +@import "syntax.scss"; + +// - Profile view + +.profile { + @extend .tile; + margin-top: $control-padding-y*5; +} + +.profile-icon { + @extend .tile-icon; +} + +.profile-avatar { + @extend .avatar; + @extend .avatar-xl; + + height: 8.2rem; + width: 8.2rem; +} + +.profile-content { + @extend .tile-content; + padding: $control-padding-x $control-padding-y; +} + +.profile-title { + @extend .tile-title; +} + +.profile-stats { + dl { + border-top: 1px solid $border-color; + border-bottom: 1px solid $border-color; + } + + dt { + font-weight: normal; + color: lighten($dark-color, 15%); + } + + dt, dd { + display: inline-block; + margin: 0; + margin-right: $control-padding-x; + } + + dd { + margin-right: $control-padding-x-lg; + } +} \ No newline at end of file diff --git a/redesign/profile.nim b/redesign/profile.nim index b2fc8c7..67cffee 100644 --- a/redesign/profile.nim +++ b/redesign/profile.nim @@ -1,5 +1,6 @@ -import options -import threadlist, post +import options, httpcore, json, sugar + +import threadlist, post, category, error type Profile* = object user*: User @@ -9,3 +10,69 @@ type # Information that only admins should see. email*: Option[string] +when defined(js): + include karax/prelude + import karax/[kajax] + import karaxutils + + type + ProfileState* = ref object + profile: Option[Profile] + loading: bool + status: HttpCode + + proc newProfileState*(): ProfileState = + ProfileState( + loading: false, + status: Http200 + ) + + proc onProfile(httpStatus: int, response: kstring, state: ProfileState) = + # TODO: Try to abstract these. + state.loading = false + state.status = httpStatus.HttpCode + if state.status != Http200: return + + let parsed = parseJson($response) + let profile = to(parsed, Profile) + + state.profile = some(profile) + + proc render*(state: ProfileState, username: string): VNode = + if state.status != Http200: + return renderError("Couldn't retrieve profile.") + + if state.profile.isNone: + let uri = makeUri("profile.json", ("username", username)) + ajaxGet(uri, @[], (s: int, r: kstring) => onProfile(s, r, state)) + + return buildHtml(tdiv(class="loading loading-lg")) + + let profile = state.profile.get() + result = buildHtml(): + section(class="container grid-xl"): + tdiv(class="profile"): + tdiv(class="profile-icon"): + render(profile.user, "profile-avatar") + tdiv(class="profile-content"): + h2(class="profile-title"): + text profile.user.name + + tdiv(class="profile-stats"): + dl(): + dt(text "Joined") + dd(text threadlist.renderActivity(profile.joinTime)) + dt(text "Last Post") + dd(text renderActivity(profile.posts[0].info.creation)) + dt(text "Last Online") + dd(text renderActivity(profile.user.lastOnline)) + dt(text "Rank") + dd(text $profile.user.rank) + + tdiv(class="columns"): + tdiv(class="column col-6"): + h4(text "Latest Posts") + tdiv(class="column col-6"): + h4(text "Latest Threads") + + From 30721d53d623c365df430eb8fc40321af49c935a Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 15 May 2018 15:52:01 +0100 Subject: [PATCH 165/451] Implements posts and threads list in profile page. --- forum.nim | 77 ++++++++++++++++++++++++++++++------------ redesign/nimforum.scss | 14 +++++++- redesign/post.nim | 14 +++++++- redesign/profile.nim | 42 ++++++++++++++++++++--- 4 files changed, 119 insertions(+), 28 deletions(-) diff --git a/forum.nim b/forum.nim index 49c85bb..8cd3311 100644 --- a/forum.nim +++ b/forum.nim @@ -1214,14 +1214,33 @@ routes: var username = @"username" + # Have to do this because SQLITE doesn't support `in` queries with + # multiple columns :/ + # TODO: Figure out a better way. This is horrible. + let creatorSubquery = """ + (select $1 from post p + where p.thread = t.id + order by p.id asc limit 1) + """ + + let threadsFrom = """ + from thread t, post p + where ? in $1 and p.id in $2 + """ % [creatorSubquery % "author", creatorSubquery % "id"] + + let postsFrom = """ + from post p, person u, thread t + where u.id = p.author and p.thread = t.id and u.name = ? + """ + let postsQuery = sql(""" - select p.id, p.content, strftime('%s', p.creation), p.author, + select p.id, strftime('%s', p.creation), u.name, u.email, strftime('%s', u.lastOnline), u.status, - strftime('%s', u.creation) - from post p, person u - where u.id = p.author and u.name = ? - order by p.id desc; - """) + strftime('%s', u.creation), u.id, + t.name, t.id + $1 + order by p.id desc limit 10; + """ % postsFrom) var profile = Profile( threads: @[], @@ -1229,29 +1248,43 @@ routes: ) let rows = db.getAllRows(postsQuery, username) + let userID = rows[0][7] profile.user = selectUser(@[ - rows[0][4], rows[0][5], rows[0][6], rows[0][7] + rows[0][2], rows[0][3], rows[0][4], rows[0][5] ], avatarSize=200) - profile.joinTime = rows[0][8].parseInt() + profile.joinTime = rows[0][7].parseInt() + profile.postCount = + getValue(db, sql("select count(*) " & postsFrom), username).parseInt() + profile.threadCount = + getValue(db, sql("select count(*) " & threadsFrom), userID).parseInt() if c.rank >= Admin: - profile.email = some(rows[0][5]) + profile.email = some(rows[0][3]) for row in rows: - let firstPostForThread = getRow(db, - sql"""select t.id, t.name, t.views, t.modified, p.id - from thread t, post p - where p.thread = t.id - order by p.id limit 1""", row[0]) - - # Check if the thread that contains this post was created by the user. - if firstPostForThread[4] == row[0]: - profile.threads.add( - selectThread(firstPostForThread) - ) - profile.posts.add( - selectPost(row, @[]) + PostLink( + creation: row[1].parseInt(), + topic: row[8], + threadId: row[9].parseInt(), + postId: row[0].parseInt() + ) + ) + + let threadsQuery = sql(""" + select t.id, t.name, strftime('%s', p.creation), p.id + $1 + order by t.id desc + limit 10; + """ % threadsFrom) + for row in db.getAllRows(threadsQuery, userID): + profile.threads.add( + PostLink( + creation: row[2].parseInt(), + topic: row[1], + threadId: row[0].parseInt(), + postId: row[3].parseInt() + ) ) resp $(%profile), "application/json" diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index dcab208..c617c52 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -499,4 +499,16 @@ hr { dd { margin-right: $control-padding-x-lg; } -} \ No newline at end of file +} + +.profile-post { + @extend .post; + + .profile-post-main { + flex: 1; + } + + .profile-post-time { + float: right; + } +} diff --git a/redesign/post.nim b/redesign/post.nim index 9ddbbb7..6a6f10d 100644 --- a/redesign/post.nim +++ b/redesign/post.nim @@ -21,7 +21,19 @@ type info*: PostInfo moreBefore*: seq[int] + PostLink* = object ## Used by profile + creation*: int64 + topic*: string + threadId*: int + postId*: int + when defined(js): import karaxutils + proc renderPostUrl*(threadId, postId: int): string = + makeUri(fmt"/t/{threadId}#{postId}") + proc renderPostUrl*(post: Post, thread: Thread): string = - makeUri(fmt"/t/{thread.id}#{post.id}") \ No newline at end of file + renderPostUrl(thread.id, post.id) + + proc renderPostUrl*(link: PostLink): string = + renderPostUrl(link.threadId, link.postId) \ No newline at end of file diff --git a/redesign/profile.nim b/redesign/profile.nim index 67cffee..91f3a96 100644 --- a/redesign/profile.nim +++ b/redesign/profile.nim @@ -1,12 +1,14 @@ -import options, httpcore, json, sugar +import options, httpcore, json, sugar, times import threadlist, post, category, error type Profile* = object user*: User joinTime*: int64 - threads*: seq[Thread] - posts*: seq[Post] + threads*: seq[PostLink] + posts*: seq[PostLink] + postCount*: int + threadCount*: int # Information that only admins should see. email*: Option[string] @@ -38,6 +40,20 @@ when defined(js): state.profile = some(profile) + proc genPostLink(link: PostLink): VNode = + let url = renderPostUrl(link) + result = buildHtml(): + tdiv(class="profile-post"): + tdiv(class="profile-post-main"): + tdiv(class="profile-post-title"): + a(href=url): + text link.topic + tdiv(class="profile-post-time"): + let title = link.creation.fromUnix().local. + format("MMM d, yyyy HH:mm") + p(title=title): + text renderActivity(link.creation) + proc render*(state: ProfileState, username: string): VNode = if state.status != Http200: return renderError("Couldn't retrieve profile.") @@ -63,16 +79,34 @@ when defined(js): dt(text "Joined") dd(text threadlist.renderActivity(profile.joinTime)) dt(text "Last Post") - dd(text renderActivity(profile.posts[0].info.creation)) + dd(text renderActivity(profile.posts[0].creation)) dt(text "Last Online") dd(text renderActivity(profile.user.lastOnline)) + dt(text "Posts") + dd(): + if profile.postCount > 999: + text $(profile.postCount / 1000) & "k" + else: + text $profile.postCount + dt(text "Threads") + dd(): + if profile.threadCount > 999: + text $(profile.threadCount / 1000) & "k" + else: + text $profile.threadCount dt(text "Rank") dd(text $profile.user.rank) tdiv(class="columns"): tdiv(class="column col-6"): h4(text "Latest Posts") + tdiv(class="posts"): + for post in profile.posts: + genPostLink(post) tdiv(class="column col-6"): h4(text "Latest Threads") + tdiv(class="posts"): + for thread in profile.threads: + genPostLink(thread) From 87d94397e594286c624d231ec8ee5697551cf8f5 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 15 May 2018 16:06:02 +0100 Subject: [PATCH 166/451] Show user emails to Admins only under spoiler class. --- redesign/nimforum.scss | 12 ++++++++++++ redesign/profile.nim | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index c617c52..2c485cf 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -512,3 +512,15 @@ hr { float: right; } } + +.spoiler { + text-shadow: gray 0px 0px 15px; + color: transparent; + -moz-user-select: none; + user-select: none; + cursor: normal; + + &:hover, &:focus { + text-shadow: $body-font-color 0px 0px 0px; + } +} \ No newline at end of file diff --git a/redesign/profile.nim b/redesign/profile.nim index 91f3a96..0ac7b72 100644 --- a/redesign/profile.nim +++ b/redesign/profile.nim @@ -96,6 +96,10 @@ when defined(js): text $profile.threadCount dt(text "Rank") dd(text $profile.user.rank) + if profile.email.isSome(): + dt(text "Email") + dd(class="spoiler"): + text profile.email.get() tdiv(class="columns"): tdiv(class="column col-6"): From 091d21b50f7e873ff081f1eb96112780ea86fcfc Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 15 May 2018 16:27:22 +0100 Subject: [PATCH 167/451] Add links to all user avatars and refactor user things into user module. --- forum.nim | 20 ++++++++++---------- redesign/header.nim | 2 +- redesign/karaxutils.nim | 10 ++++++++-- redesign/post.nim | 5 +---- redesign/postlist.nim | 2 +- redesign/profile.nim | 3 ++- redesign/replybox.nim | 2 +- redesign/threadlist.nim | 34 +--------------------------------- redesign/user.nim | 39 +++++++++++++++++++++++++++++++++++++++ redesign/usermenu.nim | 2 +- 10 files changed, 65 insertions(+), 54 deletions(-) create mode 100644 redesign/user.nim diff --git a/forum.nim b/forum.nim index 8cd3311..9db780e 100644 --- a/forum.nim +++ b/forum.nim @@ -14,7 +14,7 @@ import cgi except setCookie import options import redesign/threadlist except User -import redesign/[category, postlist, error, header, post, profile] +import redesign/[category, postlist, error, header, post, profile, user] when not defined(windows): import bcrypt # TODO @@ -394,7 +394,7 @@ proc getBanErrorMsg(banValue: string; rank: Rank): string = of Troll: return "You have been banned." of EmailUnconfirmed: return "You need to confirm your email first." - of Moderated, User, Moderator, Admin: + of Moderated, Rank.User, Moderator, Admin: return "" proc checkLoggedIn(c: TForumData) = @@ -638,7 +638,7 @@ proc reply(c: TForumData): bool = exec(db, sql"update thread set modified = DATETIME('now') where id = ?", $c.threadId) - if c.rank >= User: + if c.rank >= Rank.User: asyncCheck sendMailToMailingList(c.config, c.username, c.email, subject, content, threadId=c.threadId, postId=c.postID, is_reply=true, threadUrl=c.makeThreadURL()) @@ -661,7 +661,7 @@ proc newThread(c: TForumData): bool = writeToDb(c, crCreate, false) discard tryExec(db, sql"insert into post_fts(post_fts) values('optimize')") discard tryExec(db, sql"insert into post_fts(thread_fts) values('optimize')") - if c.rank >= User: + if c.rank >= Rank.User: asyncCheck sendMailToMailingList(c.config, c.username, c.email, subject, content, threadId=c.threadID, postId=c.postID, is_reply=false, threadUrl=c.makeThreadURL()) @@ -1015,8 +1015,8 @@ template createTFD() = #[ DB functions. TODO: Move to another module? ]# -proc selectUser(userRow: seq[string], avatarSize: int=80): threadlist.User = - return threadlist.User( +proc selectUser(userRow: seq[string], avatarSize: int=80): User = + return User( name: userRow[0], avatarUrl: userRow[1].getGravatarUrl(avatarSize), lastOnline: userRow[2].parseInt, @@ -1252,7 +1252,7 @@ routes: profile.user = selectUser(@[ rows[0][2], rows[0][3], rows[0][4], rows[0][5] ], avatarSize=200) - profile.joinTime = rows[0][7].parseInt() + profile.joinTime = rows[0][6].parseInt() profile.postCount = getValue(db, sql("select count(*) " & postsFrom), username).parseInt() profile.threadCount = @@ -1307,16 +1307,16 @@ routes: let user = if @"logout" == "true": - logout(c); none[threadlist.User]() + logout(c); none[User]() elif c.loggedIn(): - some(threadlist.User( + some(User( name: c.username, avatarUrl: c.email.getGravatarUrl(), lastOnline: getTime().toUnix(), rank: c.rank )) else: - none[threadlist.User]() + none[User]() let status = UserStatus(user: user) resp $(%status), "application/json" diff --git a/redesign/header.nim b/redesign/header.nim index b049808..d594431 100644 --- a/redesign/header.nim +++ b/redesign/header.nim @@ -1,6 +1,6 @@ import options, times, httpcore, json, sugar -import threadlist +import threadlist, user type UserStatus* = object user*: Option[User] diff --git a/redesign/karaxutils.nim b/redesign/karaxutils.nim index 8f6334f..e783b4d 100644 --- a/redesign/karaxutils.nim +++ b/redesign/karaxutils.nim @@ -1,4 +1,4 @@ -import strutils, options +import strutils, options, strformat import dom except window include karax/prelude @@ -63,4 +63,10 @@ proc newFormData*(form: dom.Element): FormData proc get*(form: FormData, key: cstring): cstring {.importcpp: "#.get(@)".} proc append*(form: FormData, key, val: cstring) - {.importcpp: "#.append(@)".} \ No newline at end of file + {.importcpp: "#.append(@)".} + +proc renderProfileUrl*(username: string): string = + makeUri(fmt"/profile/{username}") + +proc renderPostUrl*(threadId, postId: int): string = + makeUri(fmt"/t/{threadId}#{postId}") \ No newline at end of file diff --git a/redesign/post.nim b/redesign/post.nim index 6a6f10d..1e70da4 100644 --- a/redesign/post.nim +++ b/redesign/post.nim @@ -1,7 +1,6 @@ import strformat -import threadlist - +import user, threadlist type PostInfo* = object @@ -29,8 +28,6 @@ type when defined(js): import karaxutils - proc renderPostUrl*(threadId, postId: int): string = - makeUri(fmt"/t/{threadId}#{postId}") proc renderPostUrl*(post: Post, thread: Thread): string = renderPostUrl(thread.id, post.id) diff --git a/redesign/postlist.nim b/redesign/postlist.nim index 2b667ea..1b7cc1b 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -1,7 +1,7 @@ import options, json, times, httpcore, strformat, sugar, math -import threadlist, category, post +import threadlist, category, post, user type PostList* = ref object diff --git a/redesign/profile.nim b/redesign/profile.nim index 0ac7b72..c0769dd 100644 --- a/redesign/profile.nim +++ b/redesign/profile.nim @@ -1,6 +1,7 @@ import options, httpcore, json, sugar, times -import threadlist, post, category, error +import threadlist, post, category, error, user + type Profile* = object user*: User diff --git a/redesign/replybox.nim b/redesign/replybox.nim index 04852f2..bbc40c6 100644 --- a/redesign/replybox.nim +++ b/redesign/replybox.nim @@ -6,7 +6,7 @@ when defined(js): include karax/prelude import karax / [vstyles, kajax, kdom] - import karaxutils, threadlist, post, error + import karaxutils, threadlist, post, error, user type ReplyBox* = ref object diff --git a/redesign/threadlist.nim b/redesign/threadlist.nim index 35b7d58..385a0c0 100644 --- a/redesign/threadlist.nim +++ b/redesign/threadlist.nim @@ -1,24 +1,8 @@ import strformat, times, options, json, httpcore, sugar -import category +import category, user type - Rank* {.pure.} = enum ## serialized as 'status' - Spammer ## spammer: every post is invisible - Troll ## troll: cannot write new posts - EmailUnconfirmed ## member with unconfirmed email address - Moderated ## new member: posts manually reviewed before everybody - ## can see them - User ## Ordinary user - Moderator ## Moderator: can ban/moderate users - Admin ## Admin: can do everything - - User* = object - name*: string - avatarUrl*: string - lastOnline*: int64 - rank*: Rank - Thread* = object id*: int topic*: string @@ -36,9 +20,6 @@ type lastVisit*: int64 ## Unix timestamp moreCount*: int ## How many more threads are left -proc isOnline*(user: User): bool = - return getTime().toUnix() - user.lastOnline > (60*5) - when defined(js): include karax/prelude import karax / [vstyles, kajax, kdom] @@ -77,19 +58,6 @@ when defined(js): button(class="btn btn-link"): text "Categories" section(class="navbar-section") - proc render*(user: User, class: string): VNode = - result = buildHtml(): - figure(class=class): - img(src=user.avatarUrl, title=user.name) - if user.isOnline: - italic(class="avatar-presense online") - - proc renderUserMention*(user: User): VNode = - result = buildHtml(): - # TODO: Add URL to profile. - span(class="user-mention"): - text "@" & user.name - proc genUserAvatars(users: seq[User]): VNode = result = buildHtml(td): for user in users: diff --git a/redesign/user.nim b/redesign/user.nim new file mode 100644 index 0000000..480599a --- /dev/null +++ b/redesign/user.nim @@ -0,0 +1,39 @@ +import times + +type + Rank* {.pure.} = enum ## serialized as 'status' + Spammer ## spammer: every post is invisible + Troll ## troll: cannot write new posts + EmailUnconfirmed ## member with unconfirmed email address + Moderated ## new member: posts manually reviewed before everybody + ## can see them + User ## Ordinary user + Moderator ## Moderator: can ban/moderate users + Admin ## Admin: can do everything + + User* = object + name*: string + avatarUrl*: string + lastOnline*: int64 + rank*: Rank + +proc isOnline*(user: User): bool = + return getTime().toUnix() - user.lastOnline > (60*5) + +when defined(js): + include karax/prelude + import karaxutils + + proc render*(user: User, class: string): VNode = + result = buildHtml(): + a(href=renderProfileUrl(user.name), onClick=anchorCB): + figure(class=class): + img(src=user.avatarUrl, title=user.name) + if user.isOnline: + italic(class="avatar-presense online") + + proc renderUserMention*(user: User): VNode = + result = buildHtml(): + # TODO: Add URL to profile. + span(class="user-mention"): + text "@" & user.name \ No newline at end of file diff --git a/redesign/usermenu.nim b/redesign/usermenu.nim index 6d0d8c7..65ea45c 100644 --- a/redesign/usermenu.nim +++ b/redesign/usermenu.nim @@ -6,7 +6,7 @@ when defined(js): import karax/[vstyles] import karaxutils - import threadlist + import user type UserMenu* = ref object shown: bool From 9ce1ad94c70b0d8be98b103164bf96a199fe19a6 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 15 May 2018 16:30:08 +0100 Subject: [PATCH 168/451] Fixes profile view not reloading on new username. --- redesign/profile.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redesign/profile.nim b/redesign/profile.nim index c0769dd..7c78249 100644 --- a/redesign/profile.nim +++ b/redesign/profile.nim @@ -59,7 +59,7 @@ when defined(js): if state.status != Http200: return renderError("Couldn't retrieve profile.") - if state.profile.isNone: + if state.profile.isNone or state.profile.get().user.name != username: let uri = makeUri("profile.json", ("username", username)) ajaxGet(uri, @[], (s: int, r: kstring) => onProfile(s, r, state)) From b30bedd65ec189873b55c5fdeecdc81a12ef15b4 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 15 May 2018 18:17:46 +0100 Subject: [PATCH 169/451] Improves time passed messages. --- redesign/postlist.nim | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/redesign/postlist.nim b/redesign/postlist.nim index 1b7cc1b..314ddfa 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -1,5 +1,5 @@ -import options, json, times, httpcore, strformat, sugar, math +import options, json, times, httpcore, strformat, sugar, math, strutils import threadlist, category, post, user type @@ -148,22 +148,36 @@ when defined(js): italic(class="fas fa-reply") text " Reply" - proc genTimePassed(prevPost: Post, post: Option[Post]): VNode = + proc genTimePassed(prevPost: Post, post: Option[Post], last: bool): VNode = var latestTime = if post.isSome: post.get().info.creation.fromUnix() else: getTime() # TODO: Use `between` once it's merged into stdlib. - var diffStr = "Some time later" + let + tmpl = + if last: [ + "A long time since last reply", + "$1 year since last reply", + "$1 years since last reply", + "$1 month since last reply", + "$1 months since last reply", + ] + else: [ + "Some time later", + "$1 year later", "$1 years later", + "$1 month later", "$1 months later" + ] + var diffStr = tmpl[0] let diff = latestTime - prevPost.info.creation.fromUnix() if diff.weeks > 48: let years = diff.weeks div 48 - diffStr = $years - diffStr.add(if years == 1: " year later" else: " years later") + diffStr = + (if years == 1: tmpl[1] else: tmpl[2]) % $years elif diff.weeks > 4: let months = diff.weeks div 4 - diffStr = $months - diffStr.add(if months == 1: " month later" else: " months later") + diffStr = + (if months == 1: tmpl[3] else: tmpl[4]) % $months else: return buildHtml(tdiv()) @@ -196,13 +210,13 @@ when defined(js): var prevPost: Option[Post] = none[Post]() for i, post in list.posts: if prevPost.isSome: - genTimePassed(prevPost.get(), some(post)) + genTimePassed(prevPost.get(), some(post), false) if post.moreBefore.len > 0: genLoadMore(post, i) genPost(post, list.thread, isLoggedIn) prevPost = some(post) if prevPost.isSome: - genTimePassed(prevPost.get(), none[Post]()) + genTimePassed(prevPost.get(), none[Post](), true) render(state.replyBox, list.thread, state.replyingTo, false) \ No newline at end of file From 35930799a97b758e335f6ab8b06c8944f95aafc0 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 15 May 2018 18:35:19 +0100 Subject: [PATCH 170/451] Adds "New Thread" button. --- redesign/nimforum.scss | 11 ++++++++++- redesign/threadlist.nim | 5 ++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index 2c485cf..fff7094 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -95,12 +95,21 @@ $logo-height: $navbar-height - 20px; margin-top: $control-padding-y*2; margin-bottom: $control-padding-y*2; - .dropdown > .btn { + .dropdown > .btn, .btn-secondary { background: $secondary-btn-color; border-color: darken($secondary-btn-color, 5%); color: invert($secondary-btn-color); margin-right: $control-padding-x*2; + + &:hover, &:focus { + background: darken($secondary-btn-color, 5%); + border-color: darken($secondary-btn-color, 10%); + } + + &:focus { + @include control-shadow(darken($secondary-btn-color, 40%)); + } } } diff --git a/redesign/threadlist.nim b/redesign/threadlist.nim index 385a0c0..7095a53 100644 --- a/redesign/threadlist.nim +++ b/redesign/threadlist.nim @@ -56,7 +56,10 @@ when defined(js): button(class="btn btn-primary"): text "Latest" button(class="btn btn-link"): text "Most Active" button(class="btn btn-link"): text "Categories" - section(class="navbar-section") + section(class="navbar-section"): + button(class="btn btn-secondary"): + italic(class="fas fa-plus") + text " New Thread" proc genUserAvatars(users: seq[User]): VNode = result = buildHtml(td): From c966ec8f92ee94b3e18043a8a517a0984dadefe9 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 15 May 2018 19:40:07 +0100 Subject: [PATCH 171/451] Implements new thread modal. --- redesign/newthread.nim | 81 +++++++++++++++++++++++++++++++++++ redesign/nimforum.scss | 17 ++++++++ redesign/replybox.nim | 94 +++++++++++++++++++++++------------------ redesign/threadlist.nim | 16 +++++-- 4 files changed, 163 insertions(+), 45 deletions(-) create mode 100644 redesign/newthread.nim diff --git a/redesign/newthread.nim b/redesign/newthread.nim new file mode 100644 index 0000000..0b3751e --- /dev/null +++ b/redesign/newthread.nim @@ -0,0 +1,81 @@ +when defined(js): + import sugar, httpcore, options, json + import dom except Event + + include karax/prelude + import karax / [kajax, kdom] + + import error, replybox, threadlist, post + import karaxutils + + type + NewThreadModal* = ref object + shown: bool + loading: bool + onNewThread: proc (threadId, postId: int) + error: Option[PostError] + replyBox: ReplyBox + + proc onCreatePost(httpStatus: int, response: kstring, state: NewThreadModal) = + postFinished: + state.shown = false + state.onNewThread(0, 0) # TODO + + proc onCreateClick(ev: Event, n: VNode, state: NewThreadModal) = + state.loading = true + state.error = none[PostError]() + + let uri = makeUri("login") + # TODO: This is a hack, karax should support this. + let formData = newFormData() + #formData.append("" TODO + ajaxPost(uri, @[], cast[cstring](formData), + (s: int, r: kstring) => onCreatePost(s, r, state)) + + proc onClose(ev: Event, n: VNode, state: NewThreadModal) = + state.shown = false + ev.preventDefault() + + proc newNewThreadModal*( + onNewThread: proc (threadId, postId: int) + ): NewThreadModal = + NewThreadModal( + shown: false, + onNewThread: onNewThread, + replyBox: newReplyBox(nil) + ) + + proc show*(state: NewThreadModal) = + state.shown = true + + proc render*(state: NewThreadModal): VNode = + result = buildHtml(): + tdiv(class=class({"active": state.shown}, "modal modal-lg"), + id="new-thread-modal"): + a(href="", class="modal-overlay", "aria-label"="close", + onClick=(ev: Event, n: VNode) => onClose(ev, n, state)) + tdiv(class="modal-container"): + tdiv(class="modal-header"): + a(href="", class="btn btn-clear float-right", + "aria-label"="close", + onClick=(ev: Event, n: VNode) => onClose(ev, n, state)) + tdiv(class="modal-title h5"): + text "New Thread" + tdiv(class="modal-body"): + tdiv(class="content"): + input(class="form-input", `type`="text", name="username", + placeholder="Type the title here") + renderContent(state.replyBox, none[Thread](), none[Post]()) + + tdiv(class="modal-footer"): + button(class="btn", + onClick=(ev: Event, n: VNode) => + onClose(ev, n, state)): + text "Cancel" + button(class=class( + {"loading": state.loading}, + "btn btn-primary" + ), + onClick=(ev: Event, n: VNode) => + (onCreateClick(ev, n, state))): + text "Create thread" \ No newline at end of file diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index fff7094..b15b579 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -113,6 +113,23 @@ $logo-height: $navbar-height - 20px; } } +#new-thread-modal { + .modal-container .modal-body { + max-height: none; + } + + .form-input { + margin-bottom: $control-padding-y*2; + } + + textarea.form-input, .panel-body > div { + margin-top: $control-padding-y*2; + resize: none; + min-height: 40vh; + max-height: 45vh; + } +} + // - Thread table .thread-title { a, a:visited, a:hover { diff --git a/redesign/replybox.nim b/redesign/replybox.nim index bbc40c6..7b9d235 100644 --- a/redesign/replybox.nim +++ b/redesign/replybox.nim @@ -48,6 +48,7 @@ when defined(js): state.preview = true state.loading = true state.error = none[PostError]() + state.rendering = none[kstring]() let formData = newFormData() formData.append("msg", state.text) @@ -55,6 +56,10 @@ when defined(js): ajaxPost(uri, @[], cast[cstring](formData), (s: int, r: kstring) => onPreviewPost(s, r, state)) + proc onMessageClick(e: Event, n: VNode, state: ReplyBox) = + state.preview = false + state.error = none[PostError]() + proc onReplyPost(httpStatus: int, response: kstring, state: ReplyBox) = postFinished: state.text = "" @@ -84,6 +89,53 @@ when defined(js): # `value` on the node? We need to document this better :) state.text = cast[dom.TextAreaElement](n.dom).value + proc renderContent*(state: ReplyBox, thread: Option[Thread], + post: Option[Post]): VNode = + result = buildHtml(): + tdiv(class="panel"): + tdiv(class="panel-nav"): + ul(class="tab tab-block"): + li(class=class({"active": not state.preview}, "tab-item"), + onClick=(e: Event, n: VNode) => + onMessageClick(e, n, state)): + a(class="c-hand"): + text "Message" + li(class=class({"active": state.preview}, "tab-item"), + onClick=(e: Event, n: VNode) => + onPreviewClick(e, n, state)): + a(class="c-hand"): + text "Preview" + tdiv(class="panel-body"): + if state.preview: + if state.loading: + tdiv(class="loading") + elif state.rendering.isSome(): + verbatim(state.rendering.get()) + else: + textarea(class="form-input", rows="5", + onChange=(e: Event, n: VNode) => + onChange(e, n, state), + value=state.text) + + if state.error.isSome(): + p(class="text-error", + style=style(StyleAttr.marginTop, "0.4rem")): + text state.error.get().message + + if thread.isSome: + tdiv(class="panel-footer"): + button(class=class( + {"loading": state.loading}, + "btn btn-primary float-right" + ), + onClick=(e: Event, n: VNode) => + onReplyClick(e, n, state, thread.get(), post)): + text "Reply" + button(class="btn btn-link float-right", + onClick=(e: Event, n: VNode) => + onCancelClick(e, n, state)): + text "Cancel" + proc render*(state: ReplyBox, thread: Thread, post: Option[Post], hasMore: bool): VNode = if not state.shown: @@ -106,44 +158,4 @@ when defined(js): button(class="btn"): italic(class="fas fa-arrow-up") tdiv(class="information-content"): - tdiv(class="panel"): - tdiv(class="panel-nav"): - ul(class="tab tab-block"): - li(class=class({"active": not state.preview}, "tab-item"), - onClick=(e: Event, n: VNode) => (state.preview = false)): - a(class="c-hand"): - text "Message" - li(class=class({"active": state.preview}, "tab-item"), - onClick=(e: Event, n: VNode) => - onPreviewClick(e, n, state)): - a(class="c-hand"): - text "Preview" - tdiv(class="panel-body"): - if state.preview: - if state.loading: - tdiv(class="loading") - elif state.rendering.isSome(): - verbatim(state.rendering.get()) - else: - textarea(class="form-input", rows="5", - onChange=(e: Event, n: VNode) => - onChange(e, n, state), - value=state.text) - - if state.error.isSome(): - tdiv(class="toast toast-error", - style=style(StyleAttr.marginTop, "0.4rem")): - text state.error.get().message - - tdiv(class="panel-footer"): - button(class=class( - {"loading": state.loading}, - "btn btn-primary float-right" - ), - onClick=(e: Event, n: VNode) => - onReplyClick(e, n, state, thread, post)): - text "Reply" - button(class="btn btn-link float-right", - onClick=(e: Event, n: VNode) => - onCancelClick(e, n, state)): - text "Cancel" \ No newline at end of file + renderContent(state, some(thread), post) \ No newline at end of file diff --git a/redesign/threadlist.nim b/redesign/threadlist.nim index 7095a53..837c994 100644 --- a/redesign/threadlist.nim +++ b/redesign/threadlist.nim @@ -24,24 +24,30 @@ when defined(js): include karax/prelude import karax / [vstyles, kajax, kdom] - import karaxutils, error + import karaxutils, error, newthread type State = ref object list: Option[ThreadList] loading: bool status: HttpCode + newThread: NewThreadModal + proc onNewThread(threadId, postId: int) proc newState(): State = State( list: none[ThreadList](), loading: false, - status: Http200 + status: Http200, + newThread: newNewThreadModal(onNewThread) ) var state = newState() + proc onNewThread(threadId, postId: int) = + discard + proc genTopButtons(): VNode = result = buildHtml(): section(class="navbar container grid-xl", id="main-buttons"): @@ -57,7 +63,8 @@ when defined(js): button(class="btn btn-link"): text "Most Active" button(class="btn btn-link"): text "Categories" section(class="navbar-section"): - button(class="btn btn-secondary"): + button(class="btn btn-secondary", + onClick=(e: Event, n: VNode) => state.newThread.show()): italic(class="fas fa-plus") text " New Thread" @@ -174,4 +181,5 @@ when defined(js): proc renderThreadList*(): VNode = result = buildHtml(tdiv): genTopButtons() - genThreadList() \ No newline at end of file + genThreadList() + render(state.newThread) \ No newline at end of file From 76c7f43079b30d0e0a64e4c67daa51e2d1541e49 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 15 May 2018 19:52:21 +0100 Subject: [PATCH 172/451] Create separate page for new thread modal. --- redesign/forum.nim | 10 +++++-- redesign/newthread.nim | 60 ++++++++++++----------------------------- redesign/nimforum.scss | 10 ++++--- redesign/threadlist.nim | 17 +++++------- 4 files changed, 39 insertions(+), 58 deletions(-) diff --git a/redesign/forum.nim b/redesign/forum.nim index 9de7c75..8f216d4 100644 --- a/redesign/forum.nim +++ b/redesign/forum.nim @@ -4,18 +4,20 @@ from dom import window, Location include karax/prelude import jester/patterns -import threadlist, postlist, header, profile +import threadlist, postlist, header, profile, newthread import karaxutils type State = ref object url: Location profile: ProfileState + newThread: NewThread proc newState(): State = State( url: window.location, - profile: newProfileState() + profile: newProfileState(), + newThread: newNewThread() ) var state = newState() @@ -46,6 +48,10 @@ proc render(): VNode = result = buildHtml(tdiv()): renderHeader() route([ + r("/newthread", + (params: Params) => + (render(state.newThread)) + ), r("/profile/@username", (params: Params) => (render(state.profile, params["username"])) diff --git a/redesign/newthread.nim b/redesign/newthread.nim index 0b3751e..315caa4 100644 --- a/redesign/newthread.nim +++ b/redesign/newthread.nim @@ -9,19 +9,17 @@ when defined(js): import karaxutils type - NewThreadModal* = ref object - shown: bool + NewThread* = ref object loading: bool - onNewThread: proc (threadId, postId: int) error: Option[PostError] replyBox: ReplyBox - proc onCreatePost(httpStatus: int, response: kstring, state: NewThreadModal) = + proc onCreatePost(httpStatus: int, response: kstring, state: NewThread) = postFinished: - state.shown = false - state.onNewThread(0, 0) # TODO + # TODO + discard - proc onCreateClick(ev: Event, n: VNode, state: NewThreadModal) = + proc onCreateClick(ev: Event, n: VNode, state: NewThread) = state.loading = true state.error = none[PostError]() @@ -32,46 +30,22 @@ when defined(js): ajaxPost(uri, @[], cast[cstring](formData), (s: int, r: kstring) => onCreatePost(s, r, state)) - proc onClose(ev: Event, n: VNode, state: NewThreadModal) = - state.shown = false - ev.preventDefault() - - proc newNewThreadModal*( - onNewThread: proc (threadId, postId: int) - ): NewThreadModal = - NewThreadModal( - shown: false, - onNewThread: onNewThread, + proc newNewThread*(): NewThread = + NewThread( replyBox: newReplyBox(nil) ) - proc show*(state: NewThreadModal) = - state.shown = true - - proc render*(state: NewThreadModal): VNode = + proc render*(state: NewThread): VNode = result = buildHtml(): - tdiv(class=class({"active": state.shown}, "modal modal-lg"), - id="new-thread-modal"): - a(href="", class="modal-overlay", "aria-label"="close", - onClick=(ev: Event, n: VNode) => onClose(ev, n, state)) - tdiv(class="modal-container"): - tdiv(class="modal-header"): - a(href="", class="btn btn-clear float-right", - "aria-label"="close", - onClick=(ev: Event, n: VNode) => onClose(ev, n, state)) - tdiv(class="modal-title h5"): - text "New Thread" - tdiv(class="modal-body"): - tdiv(class="content"): - input(class="form-input", `type`="text", name="username", - placeholder="Type the title here") - renderContent(state.replyBox, none[Thread](), none[Post]()) - - tdiv(class="modal-footer"): - button(class="btn", - onClick=(ev: Event, n: VNode) => - onClose(ev, n, state)): - text "Cancel" + section(class="container grid-xl"): + tdiv(id="new-thread"): + tdiv(class="title"): + p(): text "New Thread" + tdiv(class="content"): + input(class="form-input", `type`="text", name="username", + placeholder="Type the title here") + renderContent(state.replyBox, none[Thread](), none[Post]()) + tdiv(class="footer"): button(class=class( {"loading": state.loading}, "btn btn-primary" diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index b15b579..169e2b9 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -113,7 +113,7 @@ $logo-height: $navbar-height - 20px; } } -#new-thread-modal { +#new-thread { .modal-container .modal-body { max-height: none; } @@ -124,9 +124,13 @@ $logo-height: $navbar-height - 20px; textarea.form-input, .panel-body > div { margin-top: $control-padding-y*2; - resize: none; + resize: vertical; min-height: 40vh; - max-height: 45vh; + } + + .footer { + float: right; + margin-top: $control-padding-y*2; } } diff --git a/redesign/threadlist.nim b/redesign/threadlist.nim index 837c994..0ef3aa2 100644 --- a/redesign/threadlist.nim +++ b/redesign/threadlist.nim @@ -24,22 +24,20 @@ when defined(js): include karax/prelude import karax / [vstyles, kajax, kdom] - import karaxutils, error, newthread + import karaxutils, error type State = ref object list: Option[ThreadList] loading: bool status: HttpCode - newThread: NewThreadModal proc onNewThread(threadId, postId: int) proc newState(): State = State( list: none[ThreadList](), loading: false, - status: Http200, - newThread: newNewThreadModal(onNewThread) + status: Http200 ) var @@ -63,10 +61,10 @@ when defined(js): button(class="btn btn-link"): text "Most Active" button(class="btn btn-link"): text "Categories" section(class="navbar-section"): - button(class="btn btn-secondary", - onClick=(e: Event, n: VNode) => state.newThread.show()): - italic(class="fas fa-plus") - text " New Thread" + a(href=makeUri("/newthread"), onClick=anchorCB): + button(class="btn btn-secondary"): + italic(class="fas fa-plus") + text " New Thread" proc genUserAvatars(users: seq[User]): VNode = result = buildHtml(td): @@ -181,5 +179,4 @@ when defined(js): proc renderThreadList*(): VNode = result = buildHtml(tdiv): genTopButtons() - genThreadList() - render(state.newThread) \ No newline at end of file + genThreadList() \ No newline at end of file From 702967f62433d886b6bc603ddbaaab9288fd1fcb Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 15 May 2018 20:40:59 +0100 Subject: [PATCH 173/451] Implements newthread logic on backend and marries it all together. --- forum.nim | 68 +++++++++++++++++++++++++++++++++++++++--- redesign/newthread.nim | 28 ++++++++++------- redesign/replybox.nim | 2 ++ 3 files changed, 84 insertions(+), 14 deletions(-) diff --git a/forum.nim b/forum.nim index 9db780e..a15b6e8 100644 --- a/forum.nim +++ b/forum.nim @@ -77,6 +77,7 @@ type lastIp: string ForumError = object of Exception + data: PostError var db: DbConn @@ -85,6 +86,16 @@ var useCaptcha: bool captcha: ReCaptcha +proc newForumError(message: string, + fields: seq[string] = @[]): ref ForumError = + new(result) + result.msg = message + result.data = + PostError( + errorFields: fields, + message: message + ) + proc init(c: TForumData) = c.userPass = "" c.userName = "" @@ -1076,9 +1087,10 @@ proc executeReply(c: TForumData, threadId: int, content: string, let subject = "" # TODO: Remove this redundant field. if rateLimitCheck(c): - raise newException(ForumError, "You're posting too fast!") + raise newForumError("You're posting too fast!") # TODO: Replying to. + # Verify that content can be parsed as RST. let retID = insertID( db, crud(crCreate, "post", "author", "ip", "header", "content", "thread"), @@ -1095,6 +1107,35 @@ proc executeReply(c: TForumData, threadId: int, content: string, return retID +proc executeNewThread(c: TForumData, subject, msg: string): (int64, int64) = + const + query = sql""" + insert into thread(name, views, modified) values (?, 0, DATETIME('now')) + """ + + assert c.loggedIn() + + if subject.len <= 2: + raise newForumError("Subject is too short", @["subject"]) + if subject.len > 100: + raise newForumError("Subject is too long", @["subject"]) + + if not validateRst(c, msg): + raise newForumError("Message needs to be valid RST", @["msg"]) + + if rateLimitCheck(c): + raise newForumError("You're posting too fast!") + + result[0] = tryInsertID(db, query, subject).int + if result[0] < 0: + raise newForumError("Subject already exists", @["subject"]) + + discard tryExec(db, crud(crCreate, "thread_fts", "id", "name"), + c.threadID, subject) + result[1] = executeReply(c, result[0].int, msg, -1) + discard tryExec(db, sql"insert into post_fts(post_fts) values('optimize')") + discard tryExec(db, sql"insert into post_fts(thread_fts) values('optimize')") + initialise() routes: @@ -1370,12 +1411,31 @@ routes: try: let id = executeReply(c, threadId, msg, replyingTo) resp Http200, $(%id), "application/json" - except ForumError: + except ForumError as exc: + resp Http400, $(%exc.data), "application/json" + + post "/karax/newthread": + createTFD() + if not c.loggedIn(): let err = PostError( errorFields: @[], - message: getCurrentExceptionMsg() + message: "Not logged in." ) - resp Http400, $(%err), "application/json" + resp Http401, $(%err), "application/json" + + let formData = request.formData + cond "msg" in formData + cond "subject" in formData + + let msg = formData["msg"].body + let subject = formData["subject"].body + # TODO: category + + try: + let res = executeNewThread(c, subject, msg) + resp Http200, $(%[res[0], res[1]]), "application/json" + except ForumError as exc: + resp Http400, $(%exc.data), "application/json" get re"/karax/(.+)?": resp readFile("redesign/karax.html") diff --git a/redesign/newthread.nim b/redesign/newthread.nim index 315caa4..c81f3cf 100644 --- a/redesign/newthread.nim +++ b/redesign/newthread.nim @@ -13,28 +13,35 @@ when defined(js): loading: bool error: Option[PostError] replyBox: ReplyBox + subject: kstring + + proc newNewThread*(): NewThread = + NewThread( + replyBox: newReplyBox(nil), + subject: "" + ) + + proc onSubjectChange(e: Event, n: VNode, state: NewThread) = + state.subject = n.value proc onCreatePost(httpStatus: int, response: kstring, state: NewThread) = postFinished: - # TODO - discard + let j = parseJson($response) + let response = to(j, array[2, int]) + navigateTo(renderPostUrl(response[0], response[1])) proc onCreateClick(ev: Event, n: VNode, state: NewThread) = state.loading = true state.error = none[PostError]() - let uri = makeUri("login") + let uri = makeUri("newthread") # TODO: This is a hack, karax should support this. let formData = newFormData() - #formData.append("" TODO + formData.append("subject", state.subject) + formData.append("msg", state.replyBox.getText()) ajaxPost(uri, @[], cast[cstring](formData), (s: int, r: kstring) => onCreatePost(s, r, state)) - proc newNewThread*(): NewThread = - NewThread( - replyBox: newReplyBox(nil) - ) - proc render*(state: NewThread): VNode = result = buildHtml(): section(class="container grid-xl"): @@ -43,7 +50,8 @@ when defined(js): p(): text "New Thread" tdiv(class="content"): input(class="form-input", `type`="text", name="username", - placeholder="Type the title here") + placeholder="Type the title here", + onChange=(e: Event, n: VNode) => onSubjectChange(e, n, state)) renderContent(state.replyBox, none[Thread](), none[Post]()) tdiv(class="footer"): button(class=class( diff --git a/redesign/replybox.nim b/redesign/replybox.nim index 7b9d235..99400f8 100644 --- a/redesign/replybox.nim +++ b/redesign/replybox.nim @@ -39,6 +39,8 @@ when defined(js): state.shown = true + proc getText*(state: ReplyBox): kstring = state.text + proc onPreviewPost(httpStatus: int, response: kstring, state: ReplyBox) = postFinished: kout(response) From f6e6929c25e97ed8907bfa73156b1e9d72029527 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 15 May 2018 20:46:42 +0100 Subject: [PATCH 174/451] Show newthread error underneath subject. --- forum.nim | 3 +++ redesign/newthread.nim | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/forum.nim b/forum.nim index a15b6e8..8881498 100644 --- a/forum.nim +++ b/forum.nim @@ -1120,6 +1120,9 @@ proc executeNewThread(c: TForumData, subject, msg: string): (int64, int64) = if subject.len > 100: raise newForumError("Subject is too long", @["subject"]) + if msg.len == 0: + raise newForumError("Message is empty", @["msg"]) + if not validateRst(c, msg): raise newForumError("Message needs to be valid RST", @["msg"]) diff --git a/redesign/newthread.nim b/redesign/newthread.nim index c81f3cf..8ce4623 100644 --- a/redesign/newthread.nim +++ b/redesign/newthread.nim @@ -52,8 +52,12 @@ when defined(js): input(class="form-input", `type`="text", name="username", placeholder="Type the title here", onChange=(e: Event, n: VNode) => onSubjectChange(e, n, state)) + if state.error.isSome(): + p(class="text-error"): + text state.error.get().message renderContent(state.replyBox, none[Thread](), none[Post]()) tdiv(class="footer"): + button(class=class( {"loading": state.loading}, "btn btn-primary" From bd150d04de85acf4c7f1ad39223d6bb8ccd19a3f Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 15 May 2018 21:28:51 +0100 Subject: [PATCH 175/451] Improve users list query and get thread authors as well. --- forum.nim | 25 ++++++++++++++++++++----- redesign/threadlist.nim | 10 ++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/forum.nim b/forum.nim index 8881498..df5b5eb 100644 --- a/forum.nim +++ b/forum.nim @@ -1054,10 +1054,21 @@ proc selectThread(threadRow: seq[string]): Thread = where thread = ? order by creation asc limit 1;""" const usersListQuery = - sql"""select distinct name, email, strftime('%s', lastOnline), status - from person where id in - (select author from post where thread = ?) - limit 5;""" # TODO: Order by most posts. + sql""" + select name, email, strftime('%s', lastOnline), status, count(*) + from person u, post p where p.author = u.id and p.thread = ? + group by name order by count(*) desc limit 5; + """ + const authorQuery = + sql""" + select name, email, strftime('%s', lastOnline), status + from person where id in ( + select author from post + where thread = ? + order by id + limit 1 + ) + """ let posts = getRow(db, postsQuery, threadRow[0]) @@ -1071,13 +1082,17 @@ proc selectThread(threadRow: seq[string]): Thread = activity: threadRow[3].parseInt, creation: posts[1].parseInt, isLocked: false, # TODO: - isSolved: false # TODO: Add a field to `post` to identify the solution. + isSolved: false, # TODO: Add a field to `post` to identify the solution. + isDeleted: false # TODO: ) # Gather the users list. for user in getAllRows(db, usersListQuery, thread.id): thread.users.add(selectUser(user)) + # Grab the author. + thread.author = selectUser(getRow(db, authorQuery, thread.id)) + return thread proc executeReply(c: TForumData, threadId: int, content: string, diff --git a/redesign/threadlist.nim b/redesign/threadlist.nim index 0ef3aa2..3895cab 100644 --- a/redesign/threadlist.nim +++ b/redesign/threadlist.nim @@ -7,6 +7,7 @@ type id*: int topic*: string category*: Category + author*: User users*: seq[User] replies*: int views*: int @@ -14,12 +15,17 @@ type creation*: int64 ## Unix timestamp isLocked*: bool isSolved*: bool + isDeleted*: bool ThreadList* = ref object threads*: seq[Thread] lastVisit*: int64 ## Unix timestamp moreCount*: int ## How many more threads are left +proc isInvisible*(thread: Thread): bool = + ## Determines whether the specified thread is under moderation. + thread.author.rank <= Moderated + when defined(js): include karax/prelude import karax / [vstyles, kajax, kdom] @@ -95,6 +101,10 @@ when defined(js): td(class="thread-title"): if thread.isLocked: italic(class="fas fa-lock fa-xs") + if thread.isInvisible: + italic(class="fas fa-eye-slash fa-xs") + if thread.isSolved: + italic(class="fas fa-check-square fa-xs") a(href=makeUri("/t/" & $thread.id), onClick=anchorCB): text thread.topic td(): render(thread.category) From bc104ff41da1c9d0a13e52eb935e92e0d6aed380 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 15 May 2018 22:13:53 +0100 Subject: [PATCH 176/451] Implements anchor awareness. --- redesign/forum.nim | 9 ++- redesign/karaxutils.nim | 131 +++++++++++++++++++++++----------------- redesign/postlist.nim | 26 ++++++-- utils.nim | 20 +----- 4 files changed, 107 insertions(+), 79 deletions(-) diff --git a/redesign/forum.nim b/redesign/forum.nim index 8f216d4..1c08ec4 100644 --- a/redesign/forum.nim +++ b/redesign/forum.nim @@ -58,7 +58,14 @@ proc render(): VNode = ), r("/t/@id", (params: Params) => - (renderPostList(params["id"].parseInt(), isLoggedIn())) + ( + let postId = getInt(($state.url.hash).substr(1), 0); + renderPostList( + params["id"].parseInt(), + if postId == 0: none[int]() else: some[int](postId), + isLoggedIn() + ) + ) ), r("/", (params: Params) => renderThreadList()) ]) diff --git a/redesign/karaxutils.nim b/redesign/karaxutils.nim index e783b4d..eeb0e92 100644 --- a/redesign/karaxutils.nim +++ b/redesign/karaxutils.nim @@ -1,72 +1,91 @@ -import strutils, options, strformat +import strutils, options, strformat, parseutils import dom except window -include karax/prelude -import karax / [kdom] +proc parseInt*(s: string, value: var int, validRange: Slice[int]) {. + noSideEffect.} = + ## parses `s` into an integer in the range `validRange`. If successful, + ## `value` is modified to contain the result. Otherwise no exception is + ## raised and `value` is not touched; this way a reasonable default value + ## won't be overwritten. + var x = value + try: + discard parseutils.parseInt(s, x, 0) + except OverflowError: + discard + if x in validRange: value = x -const appName = "/karax/" +proc getInt*(s: string, default = 0): int = + ## Safely parses an int and returns it. + result = default + parseInt(s, result, 0..1_000_000_000) -proc class*(classes: varargs[tuple[name: string, present: bool]], - defaultClasses: string = ""): string = - result = defaultClasses & " " - for class in classes: - if class.present: result.add(class.name & " ") +when defined(js): + include karax/prelude + import karax / [kdom] -proc makeUri*(relative: string, appName=appName, includeHash=false): string = - ## Concatenates ``relative`` to the current URL in a way that is - ## (possibly) sane. - var relative = relative - assert appName in $window.location.pathname - if relative[0] == '/': relative = relative[1..^1] + const appName = "/karax/" - return $window.location.protocol & "//" & - $window.location.host & - appName & - relative & - $window.location.search & - (if includeHash: $window.location.hash else: "") + proc class*(classes: varargs[tuple[name: string, present: bool]], + defaultClasses: string = ""): string = + result = defaultClasses & " " + for class in classes: + if class.present: result.add(class.name & " ") -proc makeUri*(relative: string, params: varargs[(string, string)], - appName=appName, includeHash=false): string = - var query = "" - for i in 0 ..< params.len: - let param = params[i] - if i != 0: query.add("&") - query.add(param[0] & "=" & param[1]) + proc makeUri*(relative: string, appName=appName, includeHash=false): string = + ## Concatenates ``relative`` to the current URL in a way that is + ## (possibly) sane. + var relative = relative + assert appName in $window.location.pathname + if relative[0] == '/': relative = relative[1..^1] - if query.len > 0: - makeUri(relative & "?" & query, appName) - else: - makeUri(relative, appName) + return $window.location.protocol & "//" & + $window.location.host & + appName & + relative & + $window.location.search & + (if includeHash: $window.location.hash else: "") -proc navigateTo*(uri: cstring) = - # TODO: This was annoying. Karax also shouldn't have its own `window`. - dom.pushState(dom.window.history, 0, cstring"", uri) + proc makeUri*(relative: string, params: varargs[(string, string)], + appName=appName, includeHash=false): string = + var query = "" + for i in 0 ..< params.len: + let param = params[i] + if i != 0: query.add("&") + query.add(param[0] & "=" & param[1]) - # Fire the popState event. - dom.window.dispatchEvent(newEvent("popstate")) + if query.len > 0: + makeUri(relative & "?" & query, appName) + else: + makeUri(relative, appName) -proc anchorCB*(e: kdom.Event, n: VNode) = # TODO: Why does this need disamb? - e.preventDefault() + proc navigateTo*(uri: cstring) = + # TODO: This was annoying. Karax also shouldn't have its own `window`. + dom.pushState(dom.window.history, 0, cstring"", uri) - # TODO: Why does Karax have it's own Node type? That's just silly. - let url = cast[dom.Node](n.dom).getAttribute(cstring"href") + # Fire the popState event. + dom.window.dispatchEvent(newEvent("popstate")) - navigateTo(url) + proc anchorCB*(e: kdom.Event, n: VNode) = # TODO: Why does this need disamb? + e.preventDefault() -type - FormData* = ref object -proc newFormData*(): FormData - {.importcpp: "new FormData()", constructor.} -proc newFormData*(form: dom.Element): FormData - {.importcpp: "new FormData(@)", constructor.} -proc get*(form: FormData, key: cstring): cstring - {.importcpp: "#.get(@)".} -proc append*(form: FormData, key, val: cstring) - {.importcpp: "#.append(@)".} + # TODO: Why does Karax have it's own Node type? That's just silly. + let url = cast[dom.Node](n.dom).getAttribute(cstring"href") -proc renderProfileUrl*(username: string): string = - makeUri(fmt"/profile/{username}") + navigateTo(url) -proc renderPostUrl*(threadId, postId: int): string = - makeUri(fmt"/t/{threadId}#{postId}") \ No newline at end of file + type + FormData* = ref object + proc newFormData*(): FormData + {.importcpp: "new FormData()", constructor.} + proc newFormData*(form: dom.Element): FormData + {.importcpp: "new FormData(@)", constructor.} + proc get*(form: FormData, key: cstring): cstring + {.importcpp: "#.get(@)".} + proc append*(form: FormData, key, val: cstring) + {.importcpp: "#.append(@)".} + + proc renderProfileUrl*(username: string): string = + makeUri(fmt"/profile/{username}") + + proc renderPostUrl*(threadId, postId: int): string = + makeUri(fmt"/t/{threadId}#{postId}") \ No newline at end of file diff --git a/redesign/postlist.nim b/redesign/postlist.nim index 314ddfa..ee41d12 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -12,6 +12,8 @@ type posts*: seq[Post] when defined(js): + from dom import nil + include karax/prelude import karax / [vstyles, kajax, kdom] @@ -38,7 +40,7 @@ when defined(js): var state = newState() - proc onPostList(httpStatus: int, response: kstring) = + proc onPostList(httpStatus: int, response: kstring, postId: Option[int]) = state.loading = false state.status = httpStatus.HttpCode if state.status != Http200: return @@ -48,6 +50,18 @@ when defined(js): state.list = some(list) + # The anchor should be jumped to once all the posts have been loaded. + if postId.isSome(): + discard setTimeout( + () => ( + # Would have used scrollIntoView but then the `:target` selector + # isn't activated. + window.location.hash = ""; + window.location.hash = "#" & $postId.get() + ), + 100 + ) + proc onMorePosts(httpStatus: int, response: kstring, start: int) = state.loading = false state.status = httpStatus.HttpCode @@ -190,13 +204,17 @@ when defined(js): tdiv(class="information-title"): text diffStr - proc renderPostList*(threadId: int, isLoggedIn: bool): VNode = + proc renderPostList*(threadId: int, postId: Option[int], + isLoggedIn: bool): VNode = if state.status != Http200: return renderError("Couldn't retrieve posts.") if state.list.isNone or state.list.get().thread.id != threadId: - let uri = makeUri("posts.json", ("id", $threadId)) - ajaxGet(uri, @[], (s: int, r: kstring) => onPostList(s, r)) + var params = @[("id", $threadId)] + if postId.isSome(): + params.add(("anchor", $postId.get())) + let uri = makeUri("posts.json", params) + ajaxGet(uri, @[], (s: int, r: kstring) => onPostList(s, r, postId)) return buildHtml(tdiv(class="loading loading-lg")) diff --git a/utils.nim b/utils.nim index 8f4d9a8..2d6e97e 100644 --- a/utils.nim +++ b/utils.nim @@ -7,24 +7,8 @@ from times import getTime, getGMTime, format let UsernameIdent* = IdentChars # TODO: Double check that everyone follows this. - -proc parseInt*(s: string, value: var int, validRange: Slice[int]) {. - noSideEffect.} = - ## parses `s` into an integer in the range `validRange`. If successful, - ## `value` is modified to contain the result. Otherwise no exception is - ## raised and `value` is not touched; this way a reasonable default value - ## won't be overwritten. - var x = value - try: - discard parseutils.parseInt(s, x, 0) - except OverflowError: - discard - if x in validRange: value = x - -proc getInt*(s: string, default = 0): int = - ## Safely parses an int and returns it. - result = default - parseInt(s, result, 0..1_000_000_000) +import redesign/karaxutils +export parseInt proc `%`*[T](opt: Option[T]): JsonNode = ## Generic constructor for JSON data. Creates a new ``JNull JsonNode`` From fe7c39b538462d228b144b525961d479fb89c8ac Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 16 May 2018 12:53:07 +0100 Subject: [PATCH 177/451] Fixes user presence on threadlist. --- redesign/threadlist.nim | 2 +- redesign/user.nim | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/redesign/threadlist.nim b/redesign/threadlist.nim index 3895cab..7036fe6 100644 --- a/redesign/threadlist.nim +++ b/redesign/threadlist.nim @@ -75,7 +75,7 @@ when defined(js): proc genUserAvatars(users: seq[User]): VNode = result = buildHtml(td): for user in users: - render(user, "avatar avatar-sm") + render(user, "avatar avatar-sm", showStatus=true) text " " proc renderActivity*(activity: int64): string = diff --git a/redesign/user.nim b/redesign/user.nim index 480599a..545a4a6 100644 --- a/redesign/user.nim +++ b/redesign/user.nim @@ -18,19 +18,19 @@ type rank*: Rank proc isOnline*(user: User): bool = - return getTime().toUnix() - user.lastOnline > (60*5) + return getTime().toUnix() - user.lastOnline < (60*5) when defined(js): include karax/prelude import karaxutils - proc render*(user: User, class: string): VNode = + proc render*(user: User, class: string, showStatus=false): VNode = result = buildHtml(): a(href=renderProfileUrl(user.name), onClick=anchorCB): figure(class=class): img(src=user.avatarUrl, title=user.name) - if user.isOnline: - italic(class="avatar-presense online") + if user.isOnline and showStatus: + italic(class="avatar-presence online") proc renderUserMention*(user: User): VNode = result = buildHtml(): From 3a394386d4e3a34031ba7644860ee41d9a587d1c Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 16 May 2018 14:29:01 +0100 Subject: [PATCH 178/451] Implements registration. Refactors login backend code. --- forum.nim | 234 ++++++++++++++++++++-------------------- redesign/error.nim | 3 +- redesign/header.nim | 4 +- redesign/karax.html | 1 - redesign/karaxutils.nim | 3 +- redesign/nimforum.scss | 8 ++ redesign/signup.nim | 31 +++--- 7 files changed, 146 insertions(+), 138 deletions(-) diff --git a/forum.nim b/forum.nim index df5b5eb..3f951bc 100644 --- a/forum.nim +++ b/forum.nim @@ -14,7 +14,9 @@ import cgi except setCookie import options import redesign/threadlist except User -import redesign/[category, postlist, error, header, post, profile, user] +import redesign/[ + category, postlist, error, header, post, profile, user, karaxutils +] when not defined(windows): import bcrypt # TODO @@ -297,63 +299,6 @@ proc setError(c: TForumData, field, msg: string): bool {.inline.} = c.errorMsg = "Error: " & msg return false -proc register(c: TForumData, name, pass, antibot, userIp, - email: string): Future[bool] {.async.} = - # Username validation: - if name.len == 0 or not allCharsInSet(name, UsernameIdent): - return setError(c, "name", "Invalid username!") - if getValue(db, sql"select name from person where name = ?", name).len > 0: - return setError(c, "name", "Username already exists!") - - # Password validation: - if pass.len < 4: - return setError(c, "new_password", "Invalid password!") - - # captcha validation: - if useCaptcha: - var captchaValid: bool = false - try: - captchaValid = await captcha.verify(antibot, userIp) - except: - echo("[ERROR] Error checking captcha: " & getCurrentExceptionMsg()) - captchaValid = false - - if not captchaValid: - return setError(c, "g-recaptcha-response", "Answer to captcha incorrect!") - - # email validation - if not ('@' in email and '.' in email): - return setError(c, "email", "Invalid email address") - - # perform registration: - var salt = makeSalt() - let password = makePassword(pass, salt) - - # Send activation email. - let epoch = $int(epochTime()) - let activateUrl = c.req.makeUri("/activateEmail?nick=$1&epoch=$2&ident=$3" % - [encodeUrl(name), encodeUrl(epoch), - encodeUrl(makeIdentHash(name, password, epoch, salt))]) - - let emailSentFut = sendEmailActivation(c.config, email, name, activateUrl) - # Block until we send the email. - # TODO: This is a workaround for 'var T' not being usable in async procs. - while not emailSentFut.finished: - poll() - when not defined(dev): - if emailSentFut.failed: - echo("[WARNING] Couldn't send activation email: ", emailSentFut.error.msg) - return setError(c, "email", "Couldn't send activation email") - - # add account to person table - exec(db, - sql("INSERT INTO person(name, password, email, salt, status, lastOnline) " & - "VALUES (?, ?, ?, ?, ?, DATETIME('now'))"), name, - password, email, salt, - when defined(dev): $Moderated else: $EmailUnconfirmed) - - return true - proc resetPassword(c: TForumData, nick, antibot, userIp: string): Future[bool] {.async.} = # captcha validation: if useCaptcha: @@ -678,34 +623,6 @@ proc newThread(c: TForumData): bool = threadUrl=c.makeThreadURL()) result = true -proc login(c: TForumData, name, pass: string): bool = - # get form data: - const query = - sql"select id, name, password, email, salt, status, ban from person where name = ?" - if name.len == 0: - return c.setError("name", "Username cannot be nil.") - var success = false - for row in fastRows(db, query, name): - if row[2] == makePassword(pass, row[4], row[2]): - c.rank = parseEnum[Rank](row[5]) - let ban = getBanErrorMsg(row[6], c.rank) - if ban.len > 0: - return c.setError("name", ban) - c.userid = row[0] - c.username = row[1] - c.userpass = row[2] - c.email = row[3] - success = true - break - if success: - # create session: - exec(db, - sql"insert into session (ip, password, userid) values (?, ?, ?)", - c.req.ip, c.userpass, c.userid) - return true - else: - return c.setError("password", "Login failed!") - proc verifyIdentHash(c: TForumData, name, epoch, ident: string): bool = const query = sql"select password, salt, strftime('%s', lastOnline) from person where name = ?" @@ -1154,6 +1071,81 @@ proc executeNewThread(c: TForumData, subject, msg: string): (int64, int64) = discard tryExec(db, sql"insert into post_fts(post_fts) values('optimize')") discard tryExec(db, sql"insert into post_fts(thread_fts) values('optimize')") +proc executeLogin(c: TForumData, username, password: string): string = + ## Performs a login with the specified details. + ## + ## Optionally, `username` may contain the email of the user instead. + const query = + sql""" + select id, name, password, email, salt + from person where name = ? or email = ? + """ + if username.len == 0: + raise newForumError("Username cannot be empty", @["username"]) + + for row in fastRows(db, query, username, username): + if row[2] == makePassword(password, row[4], row[2]): + exec( + db, + sql"insert into session (ip, password, userid) values (?, ?, ?)", + c.req.ip, row[2], row[0] + ) + return row[2] + + raise newForumError("Invalid username or password") + +proc executeRegister(c: TForumData, name, pass, antibot, userIp, + email: string): Future[string] {.async.} = + ## Registers a new user and returns a new session key for that user's + ## session if registration was successful. Exceptions are raised otherwise. + + # Username validation: + if name.len == 0 or not allCharsInSet(name, UsernameIdent): + raise newForumError("Invalid username", @["username"]) + if getValue(db, sql"select name from person where name = ?", name).len > 0: + raise newForumError("Username already exists", @["username"]) + + # Password validation: + if pass.len < 4: + raise newForumError("Please choose a longer password", @["password"]) + + # captcha validation: + if useCaptcha: + var verifyFut = captcha.verify(antibot, userIp) + yield verifyFut + if verifyFut.failed: + raise newForumError( + "Invalid recaptcha answer", @[] + ) + + # email validation + if not ('@' in email and '.' in email): + raise newForumError("Invalid email", @["email"]) + + # perform registration: + var salt = makeSalt() + let password = makePassword(pass, salt) + + # Send activation email. + let epoch = $int(epochTime()) + let activateUrl = c.req.makeUri("/activateEmail?nick=$1&epoch=$2&ident=$3" % + [encodeUrl(name), encodeUrl(epoch), + encodeUrl(makeIdentHash(name, password, epoch, salt))]) + + let emailSentFut = sendEmailActivation(c.config, email, name, activateUrl) + yield emailSentFut + if emailSentFut.failed: + echo("[WARNING] Couldn't send activation email: ", emailSentFut.error.msg) + raise newForumError("Couldn't send activation email", @["email"]) + + # Add account to person table + exec(db, + sql("INSERT INTO person(name, password, email, salt, status, lastOnline) " & + "VALUES (?, ?, ?, ?, ?, DATETIME('now'))"), name, + password, email, salt, $Moderated) + + return password + initialise() routes: @@ -1351,15 +1343,39 @@ routes: post "/karax/login": createTFD() let formData = request.formData - if login(c, formData["username"].body, formData["password"].body): - setCookie("sid", c.userpass) - resp Http200, "{}", "application/json" - else: - let err = PostError( - errorFields: @["username", "password"], - message: "Invalid username or password" + cond "username" in formData + cond "password" in formData + try: + let session = executeLogin( + c, + formData["username"].body, + formData["password"].body ) - resp Http403, $(%err), "application/json" + setCookie("sid", session) + resp Http200, "{}", "application/json" + except ForumError as exc: + resp Http400, $(%exc.data), "application/json" + + post "/karax/signup": + createTFD() + let formData = request.formData + let username = formData["username"].body + let password = formData["password"].body + try: + discard await executeRegister( + c, + username, + password, + formData["g-recaptcha-response"].body, + request.host, + formData["email"].body + ) + let session = executeLogin(c, username, password) + setCookie("sid", session) + resp Http200, "{}", "application/json" + except ForumError: + let exc = (ref ForumError)(getCurrentException()) + resp Http400, $(%exc.data), "application/json" get "/karax/status.json": createTFD() @@ -1377,7 +1393,14 @@ routes: else: none[User]() - let status = UserStatus(user: user) + let status = UserStatus( + user: user, + recaptchaSiteKey: + if useCaptcha: + some(config.recaptchaSiteKey) + else: + none[string]() + ) resp $(%status), "application/json" post "/karax/preview": @@ -1566,27 +1589,6 @@ routes: resp genMain(c, body(), "Nim Forum - " & (if c.isPreview: "Preview" else: "Error")) - post "/dologin": - createTFD() - if login(c, @"name", @"password"): - finishLogin() - else: - c.isThreadsList = true - var count = 0 - let threadList = genThreadsList(c, count) - let data = genMain(c, threadList, - additionalHeaders = genRSSHeaders(c), showRssLinks = true) - resp data - - post "/doregister": - createTFD() - if await c.register(@"name", @"new_password", @"g-recaptcha-response", request.host, @"email"): - resp genMain(c, "You are now registered. You must now confirm your" & - " email address by clicking the link sent to " & @"email", - "Registration successful - Nim Forum") - else: - resp c.genMain(genFormRegister(c)) - post "/donewthread": createTFD() if newThread(c): diff --git a/redesign/error.nim b/redesign/error.nim index af04e73..e921a1a 100644 --- a/redesign/error.nim +++ b/redesign/error.nim @@ -38,7 +38,8 @@ when defined(js): if not error.isNone: let e = error.get() - if (e.errorFields.len == 1 and e.errorFields[0] == name) or isLast: + if (e.errorFields.len == 1 and e.errorFields[0] == name) or + (isLast and e.errorFields.len == 0): p(class="form-input-hint"): text e.message diff --git a/redesign/header.nim b/redesign/header.nim index d594431..d4d764f 100644 --- a/redesign/header.nim +++ b/redesign/header.nim @@ -4,6 +4,7 @@ import threadlist, user type UserStatus* = object user*: Option[User] + recaptchaSiteKey*: Option[string] when defined(js): include karax/prelude @@ -104,4 +105,5 @@ when defined(js): # Modals render(state.loginModal) - render(state.signupModal) \ No newline at end of file + if state.data.isSome(): + render(state.signupModal, state.data.get().recaptchaSiteKey) \ No newline at end of file diff --git a/redesign/karax.html b/redesign/karax.html index aa4d440..8c35343 100644 --- a/redesign/karax.html +++ b/redesign/karax.html @@ -14,7 +14,6 @@ -
diff --git a/redesign/karaxutils.nim b/redesign/karaxutils.nim index eeb0e92..e34b7a2 100644 --- a/redesign/karaxutils.nim +++ b/redesign/karaxutils.nim @@ -1,5 +1,4 @@ import strutils, options, strformat, parseutils -import dom except window proc parseInt*(s: string, value: var int, validRange: Slice[int]) {. noSideEffect.} = @@ -23,6 +22,8 @@ when defined(js): include karax/prelude import karax / [kdom] + import dom except window + const appName = "/karax/" proc class*(classes: varargs[tuple[name: string, present: bool]], diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index 169e2b9..cc823b5 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -553,4 +553,12 @@ hr { &:hover, &:focus { text-shadow: $body-font-color 0px 0px 0px; } +} + +// - Sign up modal + +#signup-modal { + .modal-container .modal-body { + max-height: 60vh; + } } \ No newline at end of file diff --git a/redesign/signup.nim b/redesign/signup.nim index 1b0db46..93ac84f 100644 --- a/redesign/signup.nim +++ b/redesign/signup.nim @@ -11,29 +11,17 @@ when defined(js): type SignupModal* = ref object shown: bool + loading: bool onSignUp, onLogIn: proc () error: Option[PostError] proc onSignUpPost(httpStatus: int, response: kstring, state: SignupModal) = - let status = httpStatus.HttpCode - if status == Http200: + postFinished: state.shown = false state.onSignUp() - else: - # TODO: Karax should pass the content-type... - try: - let parsed = parseJson($response) - let error = to(parsed, PostError) - - state.error = some(error) - except: - kout(getCurrentExceptionMsg().cstring) - state.error = some(PostError( - errorFields: @[], - message: "Unknown error occurred." - )) proc onSignUpClick(ev: Event, n: VNode, state: SignupModal) = + state.loading = true state.error = none[PostError]() let uri = makeUri("signup") @@ -57,9 +45,11 @@ when defined(js): proc show*(state: SignupModal) = state.shown = true - proc render*(state: SignupModal): VNode = + proc render*(state: SignupModal, recaptchaSiteKey: Option[string]): VNode = + setForeignNodeId("recaptcha") + result = buildHtml(): - tdiv(class=class({"active": state.shown}, "modal modal-sm"), + tdiv(class=class({"active": state.shown}, "modal"), id="signup-modal"): a(href="", class="modal-overlay", "aria-label"="close", onClick=(ev: Event, n: VNode) => onClose(ev, n, state)) @@ -82,8 +72,13 @@ when defined(js): "password", true ) + if recaptchaSiteKey.isSome: + tdiv(id="recaptcha"): + tdiv(class="g-recaptcha", + "data-sitekey"=recaptchaSiteKey.get()) + script(src="https://www.google.com/recaptcha/api.js") tdiv(class="modal-footer"): - button(class="btn btn-primary", + button(class=class({"loading": state.loading}, "btn btn-primary"), onClick=(ev: Event, n: VNode) => onSignUpClick(ev, n, state)): text "Create account" button(class="btn", From c338d5e930e22b8a8c26d65a5b70d103f493c8c2 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 16 May 2018 14:39:14 +0100 Subject: [PATCH 179/451] Fixes crash with profile view on new profile. --- forum.nim | 28 ++++++++++++++++------------ redesign/profile.nim | 28 +++++++++++++++------------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/forum.nim b/forum.nim index 3f951bc..c55f074 100644 --- a/forum.nim +++ b/forum.nim @@ -1286,38 +1286,42 @@ routes: let postsQuery = sql(""" select p.id, strftime('%s', p.creation), - u.name, u.email, strftime('%s', u.lastOnline), u.status, - strftime('%s', u.creation), u.id, t.name, t.id $1 order by p.id desc limit 10; """ % postsFrom) + let userQuery = sql(""" + select name, email, strftime('%s', lastOnline), status, + strftime('%s', creation), id + from person + where name = ? + """) + var profile = Profile( threads: @[], posts: @[] ) - let rows = db.getAllRows(postsQuery, username) - let userID = rows[0][7] - profile.user = selectUser(@[ - rows[0][2], rows[0][3], rows[0][4], rows[0][5] - ], avatarSize=200) - profile.joinTime = rows[0][6].parseInt() + let userRow = db.getRow(userQuery, username) + + let userID = userRow[^1] + profile.user = selectUser(userRow, avatarSize=200) + profile.joinTime = userRow[4].parseInt() profile.postCount = getValue(db, sql("select count(*) " & postsFrom), username).parseInt() profile.threadCount = getValue(db, sql("select count(*) " & threadsFrom), userID).parseInt() if c.rank >= Admin: - profile.email = some(rows[0][3]) + profile.email = some(userRow[1]) - for row in rows: + for row in db.getAllRows(postsQuery, username): profile.posts.add( PostLink( creation: row[1].parseInt(), - topic: row[8], - threadId: row[9].parseInt(), + topic: row[2], + threadId: row[3].parseInt(), postId: row[0].parseInt() ) ) diff --git a/redesign/profile.nim b/redesign/profile.nim index 7c78249..7ab23ac 100644 --- a/redesign/profile.nim +++ b/redesign/profile.nim @@ -79,8 +79,9 @@ when defined(js): dl(): dt(text "Joined") dd(text threadlist.renderActivity(profile.joinTime)) - dt(text "Last Post") - dd(text renderActivity(profile.posts[0].creation)) + if profile.posts.len > 0: + dt(text "Last Post") + dd(text renderActivity(profile.posts[0].creation)) dt(text "Last Online") dd(text renderActivity(profile.user.lastOnline)) dt(text "Posts") @@ -102,16 +103,17 @@ when defined(js): dd(class="spoiler"): text profile.email.get() - tdiv(class="columns"): - tdiv(class="column col-6"): - h4(text "Latest Posts") - tdiv(class="posts"): - for post in profile.posts: - genPostLink(post) - tdiv(class="column col-6"): - h4(text "Latest Threads") - tdiv(class="posts"): - for thread in profile.threads: - genPostLink(thread) + if profile.posts.len > 0 or profile.threads.len > 0: + tdiv(class="columns"): + tdiv(class="column col-6"): + h4(text "Latest Posts") + tdiv(class="posts"): + for post in profile.posts: + genPostLink(post) + tdiv(class="column col-6"): + h4(text "Latest Threads") + tdiv(class="posts"): + for thread in profile.threads: + genPostLink(thread) From c6b42c5979f32397f6ab1eadc89385749df487d9 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 16 May 2018 14:49:59 +0100 Subject: [PATCH 180/451] Prevent registration with duplicate emails. --- forum.nim | 14 +++++++++----- redesign/nimforum.scss | 5 +++++ redesign/threadlist.nim | 13 +++++++++---- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/forum.nim b/forum.nim index c55f074..85fd345 100644 --- a/forum.nim +++ b/forum.nim @@ -1099,6 +1099,14 @@ proc executeRegister(c: TForumData, name, pass, antibot, userIp, ## Registers a new user and returns a new session key for that user's ## session if registration was successful. Exceptions are raised otherwise. + # email validation + if not ('@' in email and '.' in email): + raise newForumError("Invalid email", @["email"]) + if getValue( + db, sql"select email from person where email = ?", email + ).len > 0: + raise newForumError("Email already exists", @["email"]) + # Username validation: if name.len == 0 or not allCharsInSet(name, UsernameIdent): raise newForumError("Invalid username", @["username"]) @@ -1118,10 +1126,6 @@ proc executeRegister(c: TForumData, name, pass, antibot, userIp, "Invalid recaptcha answer", @[] ) - # email validation - if not ('@' in email and '.' in email): - raise newForumError("Invalid email", @["email"]) - # perform registration: var salt = makeSalt() let password = makePassword(pass, salt) @@ -1142,7 +1146,7 @@ proc executeRegister(c: TForumData, name, pass, antibot, userIp, exec(db, sql("INSERT INTO person(name, password, email, salt, status, lastOnline) " & "VALUES (?, ?, ?, ?, ?, DATETIME('now'))"), name, - password, email, salt, $Moderated) + password, email, salt, $EmailUnconfirmed) return password diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index cc823b5..45b8018 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -144,6 +144,11 @@ $logo-height: $navbar-height - 20px; a.visited { color: lighten($body-font-color, 40%); } + + i { + // Icon + margin-right: $control-padding-x-sm; + } } $super-popular-color: #f86713; diff --git a/redesign/threadlist.nim b/redesign/threadlist.nim index 7036fe6..dbc1dac 100644 --- a/redesign/threadlist.nim +++ b/redesign/threadlist.nim @@ -100,12 +100,17 @@ when defined(js): tr(class=class({"no-border": noBorder})): td(class="thread-title"): if thread.isLocked: - italic(class="fas fa-lock fa-xs") + italic(class="fas fa-lock fa-xs", + title="Thread cannot be replied to") if thread.isInvisible: - italic(class="fas fa-eye-slash fa-xs") + italic(class="fas fa-eye-slash fa-xs", + title="Thread is moderated") if thread.isSolved: - italic(class="fas fa-check-square fa-xs") - a(href=makeUri("/t/" & $thread.id), onClick=anchorCB): text thread.topic + italic(class="fas fa-check-square fa-xs", + title="Thread has a solution") + a(href=makeUri("/t/" & $thread.id), + onClick=anchorCB): + text thread.topic td(): render(thread.category) genUserAvatars(thread.users) From 60694d9fbdf9e13cea138768bb77fba4c84b0a61 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 16 May 2018 15:13:41 +0100 Subject: [PATCH 181/451] Moderated posts are now shown to correct people in correct circumstances. --- forum.nim | 10 ++-------- redesign/forum.nim | 2 +- redesign/header.nim | 6 ++++-- redesign/postlist.nim | 12 ++++++++++++ redesign/threadlist.nim | 42 ++++++++++++++++++++++++++--------------- 5 files changed, 46 insertions(+), 26 deletions(-) diff --git a/forum.nim b/forum.nim index 85fd345..3a6f689 100644 --- a/forum.nim +++ b/forum.nim @@ -1205,19 +1205,13 @@ routes: let threadRow = getRow(db, threadsQuery, id) let thread = selectThread(threadRow) - let modClause = - if c.rank >= Moderator: - "(1 or u.id = ?)" - else: - "(u.status <> 'Moderated' or p.author = ?)" let postsQuery = sql( """select p.id, p.content, strftime('%s', p.creation), p.author, u.name, u.email, strftime('%s', u.lastOnline), u.status from post p, person u - where u.id = p.author and p.thread = ? and $# - and (u.status <> 'Spammer' or p.author = ?) - order by p.id""" % modClause + where u.id = p.author and p.thread = ? + order by p.id""" ) var list = PostList( diff --git a/redesign/forum.nim b/redesign/forum.nim index 1c08ec4..42d979a 100644 --- a/redesign/forum.nim +++ b/redesign/forum.nim @@ -67,7 +67,7 @@ proc render(): VNode = ) ) ), - r("/", (params: Params) => renderThreadList()) + r("/", (params: Params) => renderThreadList(getLoggedInUser())) ]) window.onPopState = onPopState diff --git a/redesign/header.nim b/redesign/header.nim index d4d764f..a09221a 100644 --- a/redesign/header.nim +++ b/redesign/header.nim @@ -68,9 +68,11 @@ when defined(js): let uri = makeUri("status.json", [("logout", $logout)]) ajaxGet(uri, @[], onStatus) + proc getLoggedInUser*(): Option[User] = + state.data.map(x => x.user).flatten + proc isLoggedIn*(): bool = - let user = state.data.map(x => x.user).flatten - not user.isNone + not getLoggedInUser().isNone proc renderHeader*(): VNode = if state.data.isNone: diff --git a/redesign/postlist.nim b/redesign/postlist.nim index ee41d12..5f90fd8 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -223,6 +223,18 @@ when defined(js): section(class="container grid-xl"): tdiv(class="title"): p(): text list.thread.topic + if list.thread.isLocked: + italic(class="fas fa-lock fa-xs", + title="Thread cannot be replied to") + text "Locked" + if list.thread.isModerated: + italic(class="fas fa-eye-slash fa-xs", + title="Thread is moderated") + text "Moderated" + if list.thread.isSolved: + italic(class="fas fa-check-square fa-xs", + title="Thread has a solution") + text "Solved" render(list.thread.category) tdiv(class="posts"): var prevPost: Option[Post] = none[Post]() diff --git a/redesign/threadlist.nim b/redesign/threadlist.nim index dbc1dac..9f2465e 100644 --- a/redesign/threadlist.nim +++ b/redesign/threadlist.nim @@ -22,7 +22,7 @@ type lastVisit*: int64 ## Unix timestamp moreCount*: int ## How many more threads are left -proc isInvisible*(thread: Thread): bool = +proc isModerated*(thread: Thread): bool = ## Determines whether the specified thread is under moderation. thread.author.rank <= Moderated @@ -30,7 +30,7 @@ when defined(js): include karax/prelude import karax / [vstyles, kajax, kdom] - import karaxutils, error + import karaxutils, error, user type State = ref object @@ -38,7 +38,6 @@ when defined(js): loading: bool status: HttpCode - proc onNewThread(threadId, postId: int) proc newState(): State = State( list: none[ThreadList](), @@ -49,10 +48,20 @@ when defined(js): var state = newState() - proc onNewThread(threadId, postId: int) = - discard + proc visibleTo(thread: Thread, user: Option[User]): bool = + ## Determines whether the specified thread should be shown to the user. + ## + ## The rules for this are determined by the rank of the user, their + ## settings (TODO), and whether the thread's creator is moderated or not. + if user.isNone(): return not thread.isModerated - proc genTopButtons(): VNode = + let rank = user.get().rank + if rank < Moderator and thread.isModerated: + return thread.author == user.get() + + return true + + proc genTopButtons(currentUser: Option[User]): VNode = result = buildHtml(): section(class="navbar container grid-xl", id="main-buttons"): section(class="navbar-section"): @@ -67,10 +76,11 @@ when defined(js): button(class="btn btn-link"): text "Most Active" button(class="btn btn-link"): text "Categories" section(class="navbar-section"): - a(href=makeUri("/newthread"), onClick=anchorCB): - button(class="btn btn-secondary"): - italic(class="fas fa-plus") - text " New Thread" + if currentUser.isSome(): + a(href=makeUri("/newthread"), onClick=anchorCB): + button(class="btn btn-secondary"): + italic(class="fas fa-plus") + text " New Thread" proc genUserAvatars(users: seq[User]): VNode = result = buildHtml(td): @@ -102,7 +112,7 @@ when defined(js): if thread.isLocked: italic(class="fas fa-lock fa-xs", title="Thread cannot be replied to") - if thread.isInvisible: + if thread.isModerated: italic(class="fas fa-eye-slash fa-xs", title="Thread is moderated") if thread.isSolved: @@ -147,7 +157,7 @@ when defined(js): let start = state.list.get().threads.len ajaxGet(makeUri("threads.json?start=" & $start), @[], onThreadList) - proc genThreadList(): VNode = + proc genThreadList(currentUser: Option[User]): VNode = if state.status != Http200: return renderError("Couldn't retrieve threads.") @@ -171,6 +181,8 @@ when defined(js): tbody(): for i in 0 ..< list.threads.len: let thread = list.threads[i] + if not visibleTo(thread, currentUser): continue + let isLastVisit = i+1 < list.threads.len and list.threads[i].activity < list.lastVisit @@ -191,7 +203,7 @@ when defined(js): td(colspan="6", onClick=onLoadMore): span(text "load more threads") - proc renderThreadList*(): VNode = + proc renderThreadList*(currentUser: Option[User]): VNode = result = buildHtml(tdiv): - genTopButtons() - genThreadList() \ No newline at end of file + genTopButtons(currentUser) + genThreadList(currentUser) \ No newline at end of file From 84caff7c97ceb13c9be80f60d743caeeee817149 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 16 May 2018 17:43:01 +0100 Subject: [PATCH 182/451] Adds global reply button to postlist. --- redesign/nimforum.scss | 49 +++++++++++++++++++++++++++++------------- redesign/postlist.nim | 7 ++++++ 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index 45b8018..fe4bb53 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -91,25 +91,29 @@ $logo-height: $navbar-height - 20px; } // - Main buttons +.btn-secondary { + background: $secondary-btn-color; + border-color: darken($secondary-btn-color, 5%); + color: invert($secondary-btn-color); + + margin-right: $control-padding-x*2; + + &:hover, &:focus { + background: darken($secondary-btn-color, 5%); + border-color: darken($secondary-btn-color, 10%); + } + + &:focus { + @include control-shadow(darken($secondary-btn-color, 40%)); + } +} + #main-buttons { margin-top: $control-padding-y*2; margin-bottom: $control-padding-y*2; - .dropdown > .btn, .btn-secondary { - background: $secondary-btn-color; - border-color: darken($secondary-btn-color, 5%); - color: invert($secondary-btn-color); - - margin-right: $control-padding-x*2; - - &:hover, &:focus { - background: darken($secondary-btn-color, 5%); - border-color: darken($secondary-btn-color, 10%); - } - - &:focus { - @include control-shadow(darken($secondary-btn-color, 40%)); - } + .dropdown > .btn { + @extend .btn-secondary; } } @@ -244,6 +248,8 @@ $views-color: #545d70; @extend .container; margin: 0; padding: 0; + + margin-bottom: 10rem; // Just some empty space at the bottom. } .post { @@ -336,6 +342,19 @@ $views-color: #545d70; } } +#thread-buttons { + border-top: 1px solid $border-color; + width: 100%; + padding-top: $control-padding-y; + padding-bottom: $control-padding-y; + @extend .clearfix; + + .btn { + float: right; + margin-right: 0; + } +} + blockquote { border-left: 0.2rem solid darken($bg-color, 10%); background-color: $bg-color; diff --git a/redesign/postlist.nim b/redesign/postlist.nim index 5f90fd8..824aa94 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -249,4 +249,11 @@ when defined(js): if prevPost.isSome: genTimePassed(prevPost.get(), none[Post](), true) + tdiv(id="thread-buttons"): + button(class="btn btn-secondary", + onClick=(e: Event, n: VNode) => + onReplyClick(e, n, none[Post]())): + italic(class="fas fa-reply") + text " Reply" + render(state.replyBox, list.thread, state.replyingTo, false) \ No newline at end of file From c5fd70d275c87456de197714f64ac28630537947 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 16 May 2018 18:45:42 +0100 Subject: [PATCH 183/451] Implements edit box and rearranges post buttons. --- forum.nim | 15 +++++++++++ redesign/editbox.nim | 43 +++++++++++++++++++++++++++++ redesign/forum.nim | 2 +- redesign/postlist.nim | 63 ++++++++++++++++++++++++++++++++----------- redesign/replybox.nim | 1 + 5 files changed, 108 insertions(+), 16 deletions(-) create mode 100644 redesign/editbox.nim diff --git a/forum.nim b/forum.nim index 3a6f689..633c212 100644 --- a/forum.nim +++ b/forum.nim @@ -1258,6 +1258,21 @@ routes: resp $(%list), "application/json" + get "/karax/post.rst": + createTFD() + let postId = getInt(@"id", -1) + cond postId != -1 + + let postQuery = sql""" + select content from post where id = ?; + """ + + let content = getValue(db, postQuery, postId) + if content.len == 0: + resp Http404, "Post not found" + else: + resp content, "text/x-rst" + get "/karax/profile.json": createTFD() var diff --git a/redesign/editbox.nim b/redesign/editbox.nim new file mode 100644 index 0000000..2cdf9ce --- /dev/null +++ b/redesign/editbox.nim @@ -0,0 +1,43 @@ +when defined(js): + import httpcore, options, sugar + + include karax/prelude + import karax/kajax + + import replybox, post, karaxutils, threadlist + + type + EditBox* = ref object + box: ReplyBox + post: Option[Post] + rawContent: Option[kstring] ## The raw rst for a post (needs to be loaded) + status: HttpCode + + proc newEditBox*(): EditBox = + EditBox( + box: newReplyBox(nil) + ) + + proc onRawContent(httpStatus: int, response: kstring, state: EditBox) = + state.status = httpStatus.HttpCode + if state.status != Http200: return + + state.rawContent = some(response) + + proc render*(state: EditBox, post: Post): VNode = + if state.post.isNone() or state.post.get().id != post.id: + state.post = some(post) + var params = @[("id", $post.id)] + let uri = makeUri("post.rst", params) + ajaxGet(uri, @[], (s: int, r: kstring) => onRawContent(s, r, state)) + + return buildHtml(tdiv(class="loading")) + + state.box.setText(state.rawContent.get()) + result = buildHtml(): + tdiv(class="edit-box"): + renderContent( + state.box, + none[Thread](), + none[Post]() + ) \ No newline at end of file diff --git a/redesign/forum.nim b/redesign/forum.nim index 42d979a..f61c50d 100644 --- a/redesign/forum.nim +++ b/redesign/forum.nim @@ -63,7 +63,7 @@ proc render(): VNode = renderPostList( params["id"].parseInt(), if postId == 0: none[int]() else: some[int](postId), - isLoggedIn() + getLoggedInUser() ) ) ), diff --git a/redesign/postlist.nim b/redesign/postlist.nim index 824aa94..2563a74 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -17,7 +17,7 @@ when defined(js): include karax/prelude import karax / [vstyles, kajax, kdom] - import karaxutils, error, replybox + import karaxutils, error, replybox, editbox type State = ref object @@ -26,6 +26,8 @@ when defined(js): status: HttpCode replyingTo: Option[Post] replyBox: ReplyBox + editing: Option[Post] ## If in edit mode, this contains the post. + editBox: EditBox proc onReplyPosted(id: int) proc newState(): State = @@ -34,7 +36,8 @@ when defined(js): loading: false, status: Http200, replyingTo: none[Post](), - replyBox: newReplyBox(onReplyPosted) + replyBox: newReplyBox(onReplyPosted), + editBox: newEditBox() ) var @@ -106,10 +109,21 @@ when defined(js): ## Executed when a reply has been successfully posted. loadMore(state.list.get().posts.len, @[id]) + proc onEditPosted(id: int, content: string) = + ## Executed when an edit has been successfully posted. + let list = state.list.get() + for i in 0 ..< list.posts.len: + if list.posts[i].id == id: + list.posts[i].info.content = content + break + proc onReplyClick(e: Event, n: VNode, p: Option[Post]) = state.replyingTo = p state.replyBox.show() + proc onEditClick(e: Event, n: VNode, p: Option[Post]) = + state.editing = p + proc onLoadMore(ev: Event, n: VNode, start: int, post: Post) = loadMore(start, post.moreBefore) # TODO: Don't load all! @@ -128,8 +142,12 @@ when defined(js): span(class="more-post-count"): text "(" & $post.moreBefore.len & ")" - proc genPost(post: Post, thread: Thread, isLoggedIn: bool): VNode = + proc genPost(post: Post, thread: Thread, currentUser: Option[User]): VNode = let postCopy = post # TODO: Another workaround here, closure capture :( + let loggedIn = currentUser.isSome() + let authoredByUser = + loggedIn and currentUser.get().name == post.author.name + result = buildHtml(): tdiv(class="post", id = $post.id): tdiv(class="post-icon"): @@ -144,18 +162,33 @@ when defined(js): a(href=renderPostUrl(post, thread), title=title): text renderActivity(post.info.creation) tdiv(class="post-content"): - verbatim(post.info.content) + if state.editing.isSome() and state.editing.get() == post: + render(state.editBox, postCopy) + else: + verbatim(post.info.content) tdiv(class="post-buttons"): - tdiv(class="like-button"): - button(class="btn"): - span(class="like-count"): - if post.likes.len > 0: - text $post.likes.len - italic(class="far fa-heart") - if isLoggedIn: - tdiv(class="flag-button"): + if authoredByUser: + tdiv(class="edit-button", onClick=(e: Event, n: VNode) => + onEditClick(e, n, some(postCopy))): button(class="btn"): - italic(class="far fa-flag") + italic(class="far fa-edit") + tdiv(class="delete-button"): + button(class="btn"): + italic(class="far fa-trash-alt") + else: + tdiv(class="like-button"): + button(class="btn"): + span(class="like-count"): + if post.likes.len > 0: + text $post.likes.len + italic(class="far fa-heart") + + if loggedIn: + tdiv(class="flag-button"): + button(class="btn"): + italic(class="far fa-flag") + + if loggedIn: tdiv(class="reply-button"): button(class="btn", onClick=(e: Event, n: VNode) => onReplyClick(e, n, some(postCopy))): @@ -205,7 +238,7 @@ when defined(js): text diffStr proc renderPostList*(threadId: int, postId: Option[int], - isLoggedIn: bool): VNode = + currentUser: Option[User]): VNode = if state.status != Http200: return renderError("Couldn't retrieve posts.") @@ -243,7 +276,7 @@ when defined(js): genTimePassed(prevPost.get(), some(post), false) if post.moreBefore.len > 0: genLoadMore(post, i) - genPost(post, list.thread, isLoggedIn) + genPost(post, list.thread, currentUser) prevPost = some(post) if prevPost.isSome: diff --git a/redesign/replybox.nim b/redesign/replybox.nim index 99400f8..af46fe6 100644 --- a/redesign/replybox.nim +++ b/redesign/replybox.nim @@ -40,6 +40,7 @@ when defined(js): state.shown = true proc getText*(state: ReplyBox): kstring = state.text + proc setText*(state: ReplyBox, text: kstring) = state.text = text proc onPreviewPost(httpStatus: int, response: kstring, state: ReplyBox) = postFinished: From 1be362259c54da8c2fbc396c35c27039b2c81251 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 16 May 2018 19:14:28 +0100 Subject: [PATCH 184/451] Implements Cancel/Save buttons for edit box. --- redesign/nimforum.scss | 32 +++++++++++----- redesign/postlist.nim | 85 +++++++++++++++++++++++++++--------------- redesign/replybox.nim | 2 +- 3 files changed, 78 insertions(+), 41 deletions(-) diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index fe4bb53..a3413c6 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -101,6 +101,8 @@ $logo-height: $navbar-height - 20px; &:hover, &:focus { background: darken($secondary-btn-color, 5%); border-color: darken($secondary-btn-color, 10%); + + color: invert($secondary-btn-color); } &:focus { @@ -127,8 +129,6 @@ $logo-height: $navbar-height - 20px; } textarea.form-input, .panel-body > div { - margin-top: $control-padding-y*2; - resize: vertical; min-height: 40vh; } @@ -474,14 +474,12 @@ blockquote { } } +.form-input.post-text-area { + margin-top: $control-padding-y*2; + resize: vertical; +} + #reply-box { - - .form-input { - // For reply text area. - margin-top: $control-padding-y*2; - resize: vertical; - } - .panel { margin-top: $control-padding-y*2; } @@ -503,6 +501,22 @@ hr { border: 0; } +.edit-box { + margin-bottom: $control-padding-y; + + .form-input.post-text-area { + margin-bottom: $control-padding-y*2; + } +} + +.edit-buttons { + float: right; + + > div { + display: inline-block; + } +} + @import "syntax.scss"; // - Profile view diff --git a/redesign/postlist.nim b/redesign/postlist.nim index 2563a74..461f587 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -117,12 +117,15 @@ when defined(js): list.posts[i].info.content = content break + proc onEditConfirm(e: Event, n: VNode, p: Post) = + discard + proc onReplyClick(e: Event, n: VNode, p: Option[Post]) = state.replyingTo = p state.replyBox.show() - proc onEditClick(e: Event, n: VNode, p: Option[Post]) = - state.editing = p + proc onEditClick(e: Event, n: VNode, p: Post) = + state.editing = some(p) proc onLoadMore(ev: Event, n: VNode, start: int, post: Post) = loadMore(start, post.moreBefore) # TODO: Don't load all! @@ -142,12 +145,58 @@ when defined(js): span(class="more-post-count"): text "(" & $post.moreBefore.len & ")" - proc genPost(post: Post, thread: Thread, currentUser: Option[User]): VNode = - let postCopy = post # TODO: Another workaround here, closure capture :( + proc genPostButtons(post: Post, currentUser: Option[User]): Vnode = let loggedIn = currentUser.isSome() let authoredByUser = loggedIn and currentUser.get().name == post.author.name + if state.editing.isSome() and state.editing.get() == post: + result = buildHtml(): + tdiv(class="edit-buttons"): + tdiv(class="reply-button"): + button(class="btn btn-link", + onClick=(e: Event, n: VNode) => + (state.editing = none[Post]())): + text " Cancel" + tdiv(class="save-button"): + button(class="btn btn-primary", onClick=(e: Event, n: VNode) => + onEditConfirm(e, n, post)): + italic(class="fas fa-check") + text " Save" + else: + result = buildHtml(): + tdiv(class="post-buttons"): + if authoredByUser: + tdiv(class="edit-button", onClick=(e: Event, n: VNode) => + onEditClick(e, n, post)): + button(class="btn"): + italic(class="far fa-edit") + tdiv(class="delete-button"): + button(class="btn"): + italic(class="far fa-trash-alt") + else: + tdiv(class="like-button"): + button(class="btn"): + span(class="like-count"): + if post.likes.len > 0: + text $post.likes.len + italic(class="far fa-heart") + + if loggedIn: + tdiv(class="flag-button"): + button(class="btn"): + italic(class="far fa-flag") + + if loggedIn: + tdiv(class="reply-button"): + button(class="btn", onClick=(e: Event, n: VNode) => + onReplyClick(e, n, some(post))): + italic(class="fas fa-reply") + text " Reply" + + proc genPost(post: Post, thread: Thread, currentUser: Option[User]): VNode = + let postCopy = post # TODO: Another workaround here, closure capture :( + result = buildHtml(): tdiv(class="post", id = $post.id): tdiv(class="post-icon"): @@ -166,34 +215,8 @@ when defined(js): render(state.editBox, postCopy) else: verbatim(post.info.content) - tdiv(class="post-buttons"): - if authoredByUser: - tdiv(class="edit-button", onClick=(e: Event, n: VNode) => - onEditClick(e, n, some(postCopy))): - button(class="btn"): - italic(class="far fa-edit") - tdiv(class="delete-button"): - button(class="btn"): - italic(class="far fa-trash-alt") - else: - tdiv(class="like-button"): - button(class="btn"): - span(class="like-count"): - if post.likes.len > 0: - text $post.likes.len - italic(class="far fa-heart") - if loggedIn: - tdiv(class="flag-button"): - button(class="btn"): - italic(class="far fa-flag") - - if loggedIn: - tdiv(class="reply-button"): - button(class="btn", onClick=(e: Event, n: VNode) => - onReplyClick(e, n, some(postCopy))): - italic(class="fas fa-reply") - text " Reply" + genPostButtons(postCopy, currentUser) proc genTimePassed(prevPost: Post, post: Option[Post], last: bool): VNode = var latestTime = diff --git a/redesign/replybox.nim b/redesign/replybox.nim index af46fe6..5a1e032 100644 --- a/redesign/replybox.nim +++ b/redesign/replybox.nim @@ -115,7 +115,7 @@ when defined(js): elif state.rendering.isSome(): verbatim(state.rendering.get()) else: - textarea(class="form-input", rows="5", + textarea(class="form-input post-text-area", rows="5", onChange=(e: Event, n: VNode) => onChange(e, n, state), value=state.text) From fc7dabdddaa1f0d088f749ed96c8e7354bb5b712 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 16 May 2018 22:41:36 +0100 Subject: [PATCH 185/451] Finalises implementation of post editing. --- forum.nim | 67 +++++++++++++++++++++++++++++++++++ redesign/editbox.nim | 66 ++++++++++++++++++++++++++++++----- redesign/nimforum.scss | 23 +++++++----- redesign/postlist.nim | 79 +++++++++++++++++++----------------------- redesign/user.nim | 2 +- 5 files changed, 174 insertions(+), 63 deletions(-) diff --git a/forum.nim b/forum.nim index 633c212..0f11d6c 100644 --- a/forum.nim +++ b/forum.nim @@ -1021,6 +1021,9 @@ proc executeReply(c: TForumData, threadId: int, content: string, if rateLimitCheck(c): raise newForumError("You're posting too fast!") + if not validateRst(c, content): + raise newForumError("Message needs to be valid RST", @["msg"]) + # TODO: Replying to. # Verify that content can be parsed as RST. let retID = insertID( @@ -1039,6 +1042,42 @@ proc executeReply(c: TForumData, threadId: int, content: string, return retID +proc updatePost(c: TForumData, postId: int, content: string, + subject: Option[string]) = + ## Updates an existing post. + assert c.loggedIn() + + let postQuery = sql""" + select author, strftime('%s', creation), thread + from post where id = ? + """ + + let postRow = getRow(db, postQuery, postId) + + # Verify that the current user has permissions to edit the specified post. + let creation = fromUnix(postRow[1].parseInt) + let isArchived = (getTime() - creation).weeks > 8 + let canEdit = c.rank == Admin or c.username == postRow[0] + if isArchived: + raise newForumError("This post is archived and can no longer be edited") + if not canEdit: + raise newForumError("You cannot edit this post") + + if not validateRst(c, content): + raise newForumError("Message needs to be valid RST", @["msg"]) + + # Update post. + exec(db, crud(crUpdate, "post", "content"), content, $postId) + exec(db, crud(crUpdate, "post_fts", "content"), content, $postId) + # Check if post is the first post of the thread. + if subject.isSome(): + let threadId = postRow[2] + let row = db.getRow(sql(""" + select id from post where thread = ? order by id asc + """), threadId) + if row[0] == $postId: + exec(db, crud(crUpdate, "thread", "name"), subject.get(), threadId) + proc executeNewThread(c: TForumData, subject, msg: string): (int64, int64) = const query = sql""" @@ -1472,6 +1511,34 @@ routes: except ForumError as exc: resp Http400, $(%exc.data), "application/json" + post "/karax/updatePost": + createTFD() + if not c.loggedIn(): + let err = PostError( + errorFields: @[], + message: "Not logged in." + ) + resp Http401, $(%err), "application/json" + + let formData = request.formData + cond "msg" in formData + cond "postId" in formData + + let msg = formData["msg"].body + let postId = getInt(formData["postId"].body, -1) + cond postId != -1 + let subject = + if "subject" in formData: + some(formData["subject"].body) + else: + none[string]() + + try: + updatePost(c, postId, msg, subject) + resp Http200, msg.rstToHtml(), "text/html" + except ForumError as exc: + resp Http400, $(%exc.data), "application/json" + post "/karax/newthread": createTFD() if not c.loggedIn(): diff --git a/redesign/editbox.nim b/redesign/editbox.nim index 2cdf9ce..861c629 100644 --- a/redesign/editbox.nim +++ b/redesign/editbox.nim @@ -1,21 +1,30 @@ when defined(js): - import httpcore, options, sugar + import httpcore, options, sugar, json include karax/prelude import karax/kajax - import replybox, post, karaxutils, threadlist + import replybox, post, karaxutils, threadlist, error type + OnEditPosted* = proc (id: int, content: string, subject: Option[string]) + EditBox* = ref object box: ReplyBox - post: Option[Post] + post: Post rawContent: Option[kstring] ## The raw rst for a post (needs to be loaded) + loading: bool status: HttpCode + error: Option[PostError] + onEditPosted: OnEditPosted + onEditCancel: proc () - proc newEditBox*(): EditBox = + proc newEditBox*(onEditPosted: OnEditPosted, onEditCancel: proc ()): EditBox = EditBox( - box: newReplyBox(nil) + box: newReplyBox(nil), + onEditPosted: onEditPosted, + onEditCancel: onEditCancel, + status: Http200 ) proc onRawContent(httpStatus: int, response: kstring, state: EditBox) = @@ -23,21 +32,60 @@ when defined(js): if state.status != Http200: return state.rawContent = some(response) + state.box.setText(state.rawContent.get()) + + proc onEditPost(httpStatus: int, response: kstring, state: EditBox) = + postFinished: + state.onEditPosted( + state.post.id, + $response, + none[string]() + ) + + proc save(state: EditBox) = + if state.loading: + # TODO: Weird behaviour: onClick handler gets called 80+ times. + return + state.loading = true + state.error = none[PostError]() + + let formData = newFormData() + formData.append("msg", state.box.getText()) + formData.append("postId", $state.post.id) + # TODO: Subject + let uri = makeUri("/updatePost") + ajaxPost(uri, @[], cast[cstring](formData), + (s: int, r: kstring) => onEditPost(s, r, state)) proc render*(state: EditBox, post: Post): VNode = - if state.post.isNone() or state.post.get().id != post.id: - state.post = some(post) + if state.rawContent.isNone() or state.post.id != post.id: + state.post = post + state.rawContent = none[kstring]() var params = @[("id", $post.id)] let uri = makeUri("post.rst", params) ajaxGet(uri, @[], (s: int, r: kstring) => onRawContent(s, r, state)) return buildHtml(tdiv(class="loading")) - state.box.setText(state.rawContent.get()) result = buildHtml(): tdiv(class="edit-box"): renderContent( state.box, none[Thread](), none[Post]() - ) \ No newline at end of file + ) + + if state.error.isSome(): + span(class="text-error"): + text state.error.get().message + + tdiv(class="edit-buttons"): + tdiv(class="reply-button"): + button(class="btn btn-link", + onClick=(e: Event, n: VNode) => (state.onEditCancel())): + text " Cancel" + tdiv(class="save-button"): + button(class=class({"loading": state.loading}, "btn btn-primary"), + onClick=(e: Event, n: VNode) => state.save()): + italic(class="fas fa-check") + text " Save" \ No newline at end of file diff --git a/redesign/nimforum.scss b/redesign/nimforum.scss index a3413c6..e6f5376 100644 --- a/redesign/nimforum.scss +++ b/redesign/nimforum.scss @@ -502,21 +502,26 @@ hr { } .edit-box { - margin-bottom: $control-padding-y; + .edit-buttons { + margin-top: $control-padding-y*2; + + float: right; + + > div { + display: inline-block; + } + } + + .text-error { + margin-top: $control-padding-y*3; + display: inline-block; + } .form-input.post-text-area { margin-bottom: $control-padding-y*2; } } -.edit-buttons { - float: right; - - > div { - display: inline-block; - } -} - @import "syntax.scss"; // - Profile view diff --git a/redesign/postlist.nim b/redesign/postlist.nim index 461f587..795cb94 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -30,6 +30,8 @@ when defined(js): editBox: EditBox proc onReplyPosted(id: int) + proc onEditPosted(id: int, content: string, subject: Option[string]) + proc onEditCancelled() proc newState(): State = State( list: none[PostList](), @@ -37,7 +39,7 @@ when defined(js): status: Http200, replyingTo: none[Post](), replyBox: newReplyBox(onReplyPosted), - editBox: newEditBox() + editBox: newEditBox(onEditPosted, onEditCancelled) ) var @@ -109,17 +111,17 @@ when defined(js): ## Executed when a reply has been successfully posted. loadMore(state.list.get().posts.len, @[id]) - proc onEditPosted(id: int, content: string) = + proc onEditCancelled() = state.editing = none[Post]() + + proc onEditPosted(id: int, content: string, subject: Option[string]) = ## Executed when an edit has been successfully posted. + state.editing = none[Post]() let list = state.list.get() for i in 0 ..< list.posts.len: if list.posts[i].id == id: list.posts[i].info.content = content break - proc onEditConfirm(e: Event, n: VNode, p: Post) = - discard - proc onReplyClick(e: Event, n: VNode, p: Option[Post]) = state.replyingTo = p state.replyBox.show() @@ -151,48 +153,37 @@ when defined(js): loggedIn and currentUser.get().name == post.author.name if state.editing.isSome() and state.editing.get() == post: - result = buildHtml(): - tdiv(class="edit-buttons"): - tdiv(class="reply-button"): - button(class="btn btn-link", - onClick=(e: Event, n: VNode) => - (state.editing = none[Post]())): - text " Cancel" - tdiv(class="save-button"): - button(class="btn btn-primary", onClick=(e: Event, n: VNode) => - onEditConfirm(e, n, post)): - italic(class="fas fa-check") - text " Save" - else: - result = buildHtml(): - tdiv(class="post-buttons"): - if authoredByUser: - tdiv(class="edit-button", onClick=(e: Event, n: VNode) => - onEditClick(e, n, post)): - button(class="btn"): - italic(class="far fa-edit") - tdiv(class="delete-button"): - button(class="btn"): - italic(class="far fa-trash-alt") - else: - tdiv(class="like-button"): - button(class="btn"): - span(class="like-count"): - if post.likes.len > 0: - text $post.likes.len - italic(class="far fa-heart") + return buildHtml(tdiv()) - if loggedIn: - tdiv(class="flag-button"): - button(class="btn"): - italic(class="far fa-flag") + result = buildHtml(): + tdiv(class="post-buttons"): + if authoredByUser: + tdiv(class="edit-button", onClick=(e: Event, n: VNode) => + onEditClick(e, n, post)): + button(class="btn"): + italic(class="far fa-edit") + tdiv(class="delete-button"): + button(class="btn"): + italic(class="far fa-trash-alt") + else: + tdiv(class="like-button"): + button(class="btn"): + span(class="like-count"): + if post.likes.len > 0: + text $post.likes.len + italic(class="far fa-heart") if loggedIn: - tdiv(class="reply-button"): - button(class="btn", onClick=(e: Event, n: VNode) => - onReplyClick(e, n, some(post))): - italic(class="fas fa-reply") - text " Reply" + tdiv(class="flag-button"): + button(class="btn"): + italic(class="far fa-flag") + + if loggedIn: + tdiv(class="reply-button"): + button(class="btn", onClick=(e: Event, n: VNode) => + onReplyClick(e, n, some(post))): + italic(class="fas fa-reply") + text " Reply" proc genPost(post: Post, thread: Thread, currentUser: Option[User]): VNode = let postCopy = post # TODO: Another workaround here, closure capture :( diff --git a/redesign/user.nim b/redesign/user.nim index 545a4a6..ba8b494 100644 --- a/redesign/user.nim +++ b/redesign/user.nim @@ -8,7 +8,7 @@ type Moderated ## new member: posts manually reviewed before everybody ## can see them User ## Ordinary user - Moderator ## Moderator: can ban/moderate users + Moderator ## Moderator: can change a user's rank Admin ## Admin: can do everything User* = object From 2b8c6d585f365cc4668b50dc6037e83156c890f1 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 16 May 2018 22:50:55 +0100 Subject: [PATCH 186/451] Show post edit buttons for admins too. --- redesign/postlist.nim | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/redesign/postlist.nim b/redesign/postlist.nim index 795cb94..5620a06 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -151,13 +151,16 @@ when defined(js): let loggedIn = currentUser.isSome() let authoredByUser = loggedIn and currentUser.get().name == post.author.name + let currentAdmin = + currentUser.isSome() and currentUser.get().rank == Admin + # Don't show buttons if the post is being edited. if state.editing.isSome() and state.editing.get() == post: return buildHtml(tdiv()) result = buildHtml(): tdiv(class="post-buttons"): - if authoredByUser: + if authoredByUser or currentAdmin: tdiv(class="edit-button", onClick=(e: Event, n: VNode) => onEditClick(e, n, post)): button(class="btn"): From 765b43cf4a27461fb941357088d108908cdd5484 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 16 May 2018 22:55:25 +0100 Subject: [PATCH 187/451] Fixes like button not showing up for admins. --- redesign/postlist.nim | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/redesign/postlist.nim b/redesign/postlist.nim index 5620a06..6446c88 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -168,7 +168,8 @@ when defined(js): tdiv(class="delete-button"): button(class="btn"): italic(class="far fa-trash-alt") - else: + + if not authoredByUser: tdiv(class="like-button"): button(class="btn"): span(class="like-count"): From 5c86ae5d129b9cb9dbc6ac141f3505dbcdcfab04 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 16 May 2018 23:06:21 +0100 Subject: [PATCH 188/451] Add small TODO for edit box. --- redesign/postlist.nim | 3 +++ 1 file changed, 3 insertions(+) diff --git a/redesign/postlist.nim b/redesign/postlist.nim index 6446c88..2bce6f7 100644 --- a/redesign/postlist.nim +++ b/redesign/postlist.nim @@ -129,6 +129,9 @@ when defined(js): proc onEditClick(e: Event, n: VNode, p: Post) = state.editing = some(p) + # TODO: Ensure the edit box is as big as its content. Auto resize the + # text area. + proc onLoadMore(ev: Event, n: VNode, start: int, post: Post) = loadMore(start, post.moreBefore) # TODO: Don't load all! From 87605e7d9456cb388734b2cbfc0711988a28e651 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 17 May 2018 00:10:36 +0100 Subject: [PATCH 189/451] Some work on settings tab in profile page. --- redesign/forum.nim | 2 +- redesign/profile.nim | 96 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 84 insertions(+), 14 deletions(-) diff --git a/redesign/forum.nim b/redesign/forum.nim index f61c50d..ee74f8f 100644 --- a/redesign/forum.nim +++ b/redesign/forum.nim @@ -54,7 +54,7 @@ proc render(): VNode = ), r("/profile/@username", (params: Params) => - (render(state.profile, params["username"])) + (render(state.profile, params["username"], getLoggedInUser())) ), r("/t/@id", (params: Params) => diff --git a/redesign/profile.nim b/redesign/profile.nim index 7ab23ac..6760de1 100644 --- a/redesign/profile.nim +++ b/redesign/profile.nim @@ -19,15 +19,20 @@ when defined(js): import karaxutils type + ProfileTab* = enum + Overview, Settings + ProfileState* = ref object profile: Option[Profile] + currentTab: ProfileTab loading: bool status: HttpCode proc newProfileState*(): ProfileState = ProfileState( loading: false, - status: Http200 + status: Http200, + currentTab: Overview ) proc onProfile(httpStatus: int, response: kstring, state: ProfileState) = @@ -55,7 +60,11 @@ when defined(js): p(title=title): text renderActivity(link.creation) - proc render*(state: ProfileState, username: string): VNode = + proc render*( + state: ProfileState, + username: string, + currentUser: Option[User] + ): VNode = if state.status != Http200: return renderError("Couldn't retrieve profile.") @@ -66,6 +75,19 @@ when defined(js): return buildHtml(tdiv(class="loading loading-lg")) let profile = state.profile.get() + let isAdmin = currentUser.isSome() and currentUser.get().rank == Admin + + # TODO: Surely there is a better way to handle this. + let rankSelect = buildHtml(): + if isAdmin: + select(class="form-select", value = $profile.user.rank): + for r in Rank: + option(text $r) + else: + select(class="form-select", value = $profile.user.rank, disabled=""): + for r in Rank: + option(text $r) + result = buildHtml(): section(class="container grid-xl"): tdiv(class="profile"): @@ -103,17 +125,65 @@ when defined(js): dd(class="spoiler"): text profile.email.get() - if profile.posts.len > 0 or profile.threads.len > 0: + if currentUser.isSome(): + let user = currentUser.get() + if user.name == profile.user.name or user.rank == Admin: + ul(class="tab"): + li(class=class( + {"active": state.currentTab == Overview}, + "tab-item" + ), + onClick=(e: Event, n: VNode) => (state.currentTab = Overview) + ): + a(): + text "Overview" + li(class=class( + {"active": state.currentTab == Settings}, + "tab-item" + ), + onClick=(e: Event, n: VNode) => (state.currentTab = Settings) + ): + a(): + text "Settings" + + case state.currentTab + of Overview: + if profile.posts.len > 0 or profile.threads.len > 0: + tdiv(class="columns"): + tdiv(class="column col-6"): + h4(text "Latest Posts") + tdiv(class="posts"): + for post in profile.posts: + genPostLink(post) + tdiv(class="column col-6"): + h4(text "Latest Threads") + tdiv(class="posts"): + for thread in profile.threads: + genPostLink(thread) + of Settings: tdiv(class="columns"): tdiv(class="column col-6"): - h4(text "Latest Posts") - tdiv(class="posts"): - for post in profile.posts: - genPostLink(post) - tdiv(class="column col-6"): - h4(text "Latest Threads") - tdiv(class="posts"): - for thread in profile.threads: - genPostLink(thread) - + form(class="form-horizontal"): + tdiv(class="form-group"): + tdiv(class="col-3 col-sm-12"): + label(class="form-label"): + text "Username" + tdiv(class="col-9 col-sm-12"): + input(class="form-input", + `type`="text", + value=profile.user.name, + disabled="") + tdiv(class="form-group"): + tdiv(class="col-3 col-sm-12"): + label(class="form-label"): + text "Email" + tdiv(class="col-9 col-sm-12"): + input(class="form-input", + `type`="text", value=profile.email.get()) + tdiv(class="form-group"): + tdiv(class="col-3 col-sm-12"): + label(class="form-label"): + text "Rank" + tdiv(class="col-9 col-sm-12"): + rankSelect From 81e5bf5af04091b350f8c9d610160e6e115ac6ad Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 17 May 2018 12:54:50 +0100 Subject: [PATCH 190/451] Improvements to profile settings. --- forum.nim | 2 +- redesign/profile.nim | 90 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 79 insertions(+), 13 deletions(-) diff --git a/forum.nim b/forum.nim index 0f11d6c..b270d0e 100644 --- a/forum.nim +++ b/forum.nim @@ -1365,7 +1365,7 @@ routes: profile.threadCount = getValue(db, sql("select count(*) " & threadsFrom), userID).parseInt() - if c.rank >= Admin: + if c.rank >= Admin or c.username == username: profile.email = some(userRow[1]) for row in db.getAllRows(postsQuery, username): diff --git a/redesign/profile.nim b/redesign/profile.nim index 6760de1..6d4fa05 100644 --- a/redesign/profile.nim +++ b/redesign/profile.nim @@ -1,4 +1,4 @@ -import options, httpcore, json, sugar, times +import options, httpcore, json, sugar, times, strformat import threadlist, post, category, error, user @@ -22,8 +22,13 @@ when defined(js): ProfileTab* = enum Overview, Settings + ProfileSettings* = object + email: kstring + rank: Rank + ProfileState* = ref object profile: Option[Profile] + settings: ProfileSettings currentTab: ProfileTab loading: bool status: HttpCode @@ -32,9 +37,21 @@ when defined(js): ProfileState( loading: false, status: Http200, - currentTab: Overview + currentTab: Overview, + settings: ProfileSettings( + email: "", + rank: Spammer + ) ) + proc resetSettings(state: ProfileState) = + let profile = state.profile.get() + if profile.email.isSome(): + state.settings = ProfileSettings( + email: profile.email.get(), + rank: profile.user.rank + ) + proc onProfile(httpStatus: int, response: kstring, state: ProfileState) = # TODO: Try to abstract these. state.loading = false @@ -45,6 +62,7 @@ when defined(js): let profile = to(parsed, Profile) state.profile = some(profile) + resetSettings(state) proc genPostLink(link: PostLink): VNode = let url = renderPostUrl(link) @@ -60,6 +78,14 @@ when defined(js): p(title=title): text renderActivity(link.creation) + proc onEmailChange(event: Event, node: VNode, state: ProfileState) = + state.settings.email = node.value + + if state.settings.email != state.profile.get().email.get(): + state.settings.rank = EmailUnconfirmed + else: + state.settings.rank = state.profile.get().user.rank + proc render*( state: ProfileState, username: string, @@ -77,16 +103,34 @@ when defined(js): let profile = state.profile.get() let isAdmin = currentUser.isSome() and currentUser.get().rank == Admin - # TODO: Surely there is a better way to handle this. - let rankSelect = buildHtml(): + let rankSelect = buildHtml(tdiv()): if isAdmin: - select(class="form-select", value = $profile.user.rank): + select(class="form-select", value = $state.settings.rank): for r in Rank: option(text $r) + p(class="form-input-hint text-warning"): + text "As an admin you can modify anyone's rank. Remember: with " & + "great power comes great responsibility." else: - select(class="form-select", value = $profile.user.rank, disabled=""): - for r in Rank: - option(text $r) + input(class="form-input", + `type`="text", value = $state.settings.rank, disabled="") + p(class="form-input-hint"): + text "Your rank determines the actions you can perform " & + "on the forum." + case state.settings.rank: + of Spammer, Troll: + p(class="form-input-hint text-warning"): + text "Your account was banned." + of EmailUnconfirmed: + p(class="form-input-hint text-warning"): + text "You cannot post until you confirm your email." + of Moderated: + p(class="form-input-hint text-warning"): + text "Your account is under moderation. This is a spam prevention "& + "measure. You can write posts but only moderators and admins "& + "will see them until your account is verified by them." + else: + discard result = buildHtml(): section(class="container grid-xl"): @@ -135,7 +179,7 @@ when defined(js): ), onClick=(e: Event, n: VNode) => (state.currentTab = Overview) ): - a(): + a(class="c-hand"): text "Overview" li(class=class( {"active": state.currentTab == Settings}, @@ -143,8 +187,9 @@ when defined(js): ), onClick=(e: Event, n: VNode) => (state.currentTab = Settings) ): - a(): - text "Settings" + a(class="c-hand"): + italic(class="fas fa-cog") + text " Settings" case state.currentTab of Overview: @@ -173,13 +218,26 @@ when defined(js): `type`="text", value=profile.user.name, disabled="") + p(class="form-input-hint"): + text fmt("Users can refer to you by writing" & + " @{profile.user.name} in their posts.") tdiv(class="form-group"): tdiv(class="col-3 col-sm-12"): label(class="form-label"): text "Email" tdiv(class="col-9 col-sm-12"): input(class="form-input", - `type`="text", value=profile.email.get()) + `type`="text", value=state.settings.email, + oninput=(e: Event, n: VNode) => + onEmailChange(e, n, state) + ) + p(class="form-input-hint"): + text "Your avatar is linked to this email and can be " & + "changed at " + a(href="https://gravatar.com/emails"): + text "gravatar.com" + text ". Note that any changes to your email will " & + "require email verification." tdiv(class="form-group"): tdiv(class="col-3 col-sm-12"): label(class="form-label"): @@ -187,3 +245,11 @@ when defined(js): tdiv(class="col-9 col-sm-12"): rankSelect + tdiv(class="float-right"): + button(class="btn btn-link", + onClick=(e: Event, n: VNode) => (resetSettings(state))): + text "Cancel" + + button(class="btn btn-primary"): + italic(class="fas fa-check") + text " Save" \ No newline at end of file From 581eba73e359fd7665957be623b764994666113d Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 17 May 2018 13:42:35 +0100 Subject: [PATCH 191/451] Implements post button for requesting a password reset. --- redesign/error.nim | 1 + redesign/postbutton.nim | 77 +++++++++++++++++++++++++++++++++++++++++ redesign/profile.nim | 13 ++++++- 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 redesign/postbutton.nim diff --git a/redesign/error.nim b/redesign/error.nim index e921a1a..a534bde 100644 --- a/redesign/error.nim +++ b/redesign/error.nim @@ -5,6 +5,7 @@ type message*: string when defined(js): + import json include karax/prelude import karax / [vstyles, kajax, kdom] diff --git a/redesign/postbutton.nim b/redesign/postbutton.nim new file mode 100644 index 0000000..913b392 --- /dev/null +++ b/redesign/postbutton.nim @@ -0,0 +1,77 @@ +## Simple generic button that can be clicked to make a post request. +## The button will show a loading indicator and a tick on success. +## +## Used for password reset emails. + +import options, httpcore, json, sugar +when defined(js): + include karax/prelude + import karax/[kajax, kdom] + + import error, karaxutils + + type + PostButton* = ref object + uri, title, icon: string + formData: FormData + error: Option[PostError] + loading: bool + posted: bool + + proc newPostButton*(uri: string, formData: FormData, + title: string, icon: string): PostButton = + PostButton( + uri: uri, + formData: formData, + title: title, + icon: icon + ) + + proc newResetPasswordButton*(email: string): PostButton = + var formData = newFormData() + formData.append("email", email) + result = newPostButton( + makeUri("/resetPassword"), + formData, + "Send password reset email", + "fas fa-envelope", + ) + + proc onPost(httpStatus: int, response: kstring, state: PostButton) = + postFinished: + discard + + proc onClick(ev: Event, n: VNode, state: PostButton) = + if state.loading or state.posted: return + + state.loading = true + state.posted = true + state.error = none[PostError]() + + # TODO: This is a hack, karax should support this. + ajaxPost(state.uri, @[], cast[cstring](state.formData), + (s: int, r: kstring) => onPost(s, r, state)) + + ev.preventDefault() + + proc render*(state: PostButton, disabled: bool): VNode = + result = buildHtml(tdiv()): + button(class=class({ + "loading": state.loading, + "disabled": disabled + }, + "btn btn-secondary" + ), + onClick=(e: Event, n: VNode) => (onClick(e, n, state))): + if state.posted: + if state.error.isNone(): + italic(class="fas fa-check") + else: + italic(class="fas fa-times") + else: + italic(class=state.icon) + text " " & state.title + + if state.error.isSome(): + p(class="text-error"): + text state.error.get().message \ No newline at end of file diff --git a/redesign/profile.nim b/redesign/profile.nim index 6d4fa05..b531aa4 100644 --- a/redesign/profile.nim +++ b/redesign/profile.nim @@ -16,7 +16,7 @@ type when defined(js): include karax/prelude import karax/[kajax] - import karaxutils + import karaxutils, postbutton type ProfileTab* = enum @@ -32,6 +32,7 @@ when defined(js): currentTab: ProfileTab loading: bool status: HttpCode + resetPassword: Option[PostButton] proc newProfileState*(): ProfileState = ProfileState( @@ -63,6 +64,8 @@ when defined(js): state.profile = some(profile) resetSettings(state) + if profile.email.isSome(): + state.resetPassword = some(newResetPasswordButton(profile.email.get())) proc genPostLink(link: PostLink): VNode = let url = renderPostUrl(link) @@ -244,6 +247,14 @@ when defined(js): text "Rank" tdiv(class="col-9 col-sm-12"): rankSelect + if state.resetPassword.isSome(): + tdiv(class="form-group"): + tdiv(class="col-3 col-sm-12"): + label(class="form-label"): + text "Password" + tdiv(class="col-9 col-sm-12"): + render(state.resetPassword.get(), + disabled=state.settings.rank==EmailUnconfirmed) tdiv(class="float-right"): button(class="btn btn-link", From 37d9fb3bb756bce2e0f8b4a505c24bc76c9c3a5d Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 17 May 2018 16:11:01 +0100 Subject: [PATCH 192/451] Removes email from profile stats. --- redesign/profile.nim | 4 ---- redesign/user.nim | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/redesign/profile.nim b/redesign/profile.nim index b531aa4..0794c9d 100644 --- a/redesign/profile.nim +++ b/redesign/profile.nim @@ -167,10 +167,6 @@ when defined(js): text $profile.threadCount dt(text "Rank") dd(text $profile.user.rank) - if profile.email.isSome(): - dt(text "Email") - dd(class="spoiler"): - text profile.email.get() if currentUser.isSome(): let user = currentUser.get() diff --git a/redesign/user.nim b/redesign/user.nim index ba8b494..6966a7b 100644 --- a/redesign/user.nim +++ b/redesign/user.nim @@ -4,6 +4,7 @@ type Rank* {.pure.} = enum ## serialized as 'status' Spammer ## spammer: every post is invisible Troll ## troll: cannot write new posts + Banned ## A non-specific ban EmailUnconfirmed ## member with unconfirmed email address Moderated ## new member: posts manually reviewed before everybody ## can see them From 825c1d654896d7e92c66ecca61a751441350405f Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 17 May 2018 16:16:45 +0100 Subject: [PATCH 193/451] s/redesign/frontend/ --- forum.nim | 15 +++++++-------- {redesign => frontend}/builder.nim | 0 {redesign => frontend}/category.nim | 0 {redesign => frontend}/editbox.nim | 0 {redesign => frontend}/error.nim | 0 {redesign => frontend}/forum.nim | 0 {redesign => frontend}/forum.nim.cfg | 0 {redesign => frontend}/header.nim | 0 {redesign => frontend}/index.html | 0 {redesign => frontend}/karax.html | 0 {redesign => frontend}/karaxutils.nim | 0 {redesign => frontend}/login.nim | 0 {redesign => frontend}/newthread.nim | 0 {redesign => frontend}/nimforum.scss | 0 {redesign => frontend}/post.nim | 0 {redesign => frontend}/postbutton.nim | 0 {redesign => frontend}/postlist.nim | 0 {redesign => frontend}/profile.nim | 0 {redesign => frontend}/replybox.nim | 0 {redesign => frontend}/signup.nim | 0 {redesign => frontend}/syntax.scss | 0 {redesign => frontend}/thread.html | 0 {redesign => frontend}/threadlist.nim | 0 {redesign => frontend}/user.nim | 0 {redesign => frontend}/usermenu.nim | 0 redesign/spectre | 1 - utils.nim | 2 +- 27 files changed, 8 insertions(+), 10 deletions(-) rename {redesign => frontend}/builder.nim (100%) rename {redesign => frontend}/category.nim (100%) rename {redesign => frontend}/editbox.nim (100%) rename {redesign => frontend}/error.nim (100%) rename {redesign => frontend}/forum.nim (100%) rename {redesign => frontend}/forum.nim.cfg (100%) rename {redesign => frontend}/header.nim (100%) rename {redesign => frontend}/index.html (100%) rename {redesign => frontend}/karax.html (100%) rename {redesign => frontend}/karaxutils.nim (100%) rename {redesign => frontend}/login.nim (100%) rename {redesign => frontend}/newthread.nim (100%) rename {redesign => frontend}/nimforum.scss (100%) rename {redesign => frontend}/post.nim (100%) rename {redesign => frontend}/postbutton.nim (100%) rename {redesign => frontend}/postlist.nim (100%) rename {redesign => frontend}/profile.nim (100%) rename {redesign => frontend}/replybox.nim (100%) rename {redesign => frontend}/signup.nim (100%) rename {redesign => frontend}/syntax.scss (100%) rename {redesign => frontend}/thread.html (100%) rename {redesign => frontend}/threadlist.nim (100%) rename {redesign => frontend}/user.nim (100%) rename {redesign => frontend}/usermenu.nim (100%) delete mode 160000 redesign/spectre diff --git a/forum.nim b/forum.nim index b270d0e..965456c 100644 --- a/forum.nim +++ b/forum.nim @@ -13,8 +13,8 @@ import import cgi except setCookie import options -import redesign/threadlist except User -import redesign/[ +import frontend/threadlist except User +import frontend/[ category, postlist, error, header, post, profile, user, karaxutils ] @@ -346,8 +346,7 @@ proc getBanErrorMsg(banValue: string; rank: Rank): string = if banValue.len > 0: return "You have been banned: " & banValue case rank - of Spammer: return "You are a spammer." - of Troll: return "You have been banned." + of Spammer, Troll, Banned: return "You have been banned." of EmailUnconfirmed: return "You need to confirm your email first." of Moderated, Rank.User, Moderator, Admin: @@ -1202,11 +1201,11 @@ routes: resp data get "/karax/nimforum.css": - resp readFile("redesign/nimforum.css"), "text/css" + resp readFile("frontend/nimforum.css"), "text/css" get "/karax/nimcache/forum.js": - resp readFile("redesign/nimcache/forum.js"), "application/javascript" + resp readFile("frontend/nimcache/forum.js"), "application/javascript" get "/karax/images/crown.png": - resp readFile("redesign/images/crown.png"), "image/png" + resp readFile("frontend/images/crown.png"), "image/png" get "/karax/threads.json": @@ -1563,7 +1562,7 @@ routes: resp Http400, $(%exc.data), "application/json" get re"/karax/(.+)?": - resp readFile("redesign/karax.html") + resp readFile("frontend/karax.html") get "/threadActivity.xml": createTFD() diff --git a/redesign/builder.nim b/frontend/builder.nim similarity index 100% rename from redesign/builder.nim rename to frontend/builder.nim diff --git a/redesign/category.nim b/frontend/category.nim similarity index 100% rename from redesign/category.nim rename to frontend/category.nim diff --git a/redesign/editbox.nim b/frontend/editbox.nim similarity index 100% rename from redesign/editbox.nim rename to frontend/editbox.nim diff --git a/redesign/error.nim b/frontend/error.nim similarity index 100% rename from redesign/error.nim rename to frontend/error.nim diff --git a/redesign/forum.nim b/frontend/forum.nim similarity index 100% rename from redesign/forum.nim rename to frontend/forum.nim diff --git a/redesign/forum.nim.cfg b/frontend/forum.nim.cfg similarity index 100% rename from redesign/forum.nim.cfg rename to frontend/forum.nim.cfg diff --git a/redesign/header.nim b/frontend/header.nim similarity index 100% rename from redesign/header.nim rename to frontend/header.nim diff --git a/redesign/index.html b/frontend/index.html similarity index 100% rename from redesign/index.html rename to frontend/index.html diff --git a/redesign/karax.html b/frontend/karax.html similarity index 100% rename from redesign/karax.html rename to frontend/karax.html diff --git a/redesign/karaxutils.nim b/frontend/karaxutils.nim similarity index 100% rename from redesign/karaxutils.nim rename to frontend/karaxutils.nim diff --git a/redesign/login.nim b/frontend/login.nim similarity index 100% rename from redesign/login.nim rename to frontend/login.nim diff --git a/redesign/newthread.nim b/frontend/newthread.nim similarity index 100% rename from redesign/newthread.nim rename to frontend/newthread.nim diff --git a/redesign/nimforum.scss b/frontend/nimforum.scss similarity index 100% rename from redesign/nimforum.scss rename to frontend/nimforum.scss diff --git a/redesign/post.nim b/frontend/post.nim similarity index 100% rename from redesign/post.nim rename to frontend/post.nim diff --git a/redesign/postbutton.nim b/frontend/postbutton.nim similarity index 100% rename from redesign/postbutton.nim rename to frontend/postbutton.nim diff --git a/redesign/postlist.nim b/frontend/postlist.nim similarity index 100% rename from redesign/postlist.nim rename to frontend/postlist.nim diff --git a/redesign/profile.nim b/frontend/profile.nim similarity index 100% rename from redesign/profile.nim rename to frontend/profile.nim diff --git a/redesign/replybox.nim b/frontend/replybox.nim similarity index 100% rename from redesign/replybox.nim rename to frontend/replybox.nim diff --git a/redesign/signup.nim b/frontend/signup.nim similarity index 100% rename from redesign/signup.nim rename to frontend/signup.nim diff --git a/redesign/syntax.scss b/frontend/syntax.scss similarity index 100% rename from redesign/syntax.scss rename to frontend/syntax.scss diff --git a/redesign/thread.html b/frontend/thread.html similarity index 100% rename from redesign/thread.html rename to frontend/thread.html diff --git a/redesign/threadlist.nim b/frontend/threadlist.nim similarity index 100% rename from redesign/threadlist.nim rename to frontend/threadlist.nim diff --git a/redesign/user.nim b/frontend/user.nim similarity index 100% rename from redesign/user.nim rename to frontend/user.nim diff --git a/redesign/usermenu.nim b/frontend/usermenu.nim similarity index 100% rename from redesign/usermenu.nim rename to frontend/usermenu.nim diff --git a/redesign/spectre b/redesign/spectre deleted file mode 160000 index 7a6af53..0000000 --- a/redesign/spectre +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7a6af53bca72b549b2cbf1948763348bd5b30dcd diff --git a/utils.nim b/utils.nim index 2d6e97e..028c42e 100644 --- a/utils.nim +++ b/utils.nim @@ -7,7 +7,7 @@ from times import getTime, getGMTime, format let UsernameIdent* = IdentChars # TODO: Double check that everyone follows this. -import redesign/karaxutils +import frontend/karaxutils export parseInt proc `%`*[T](opt: Option[T]): JsonNode = From 9f087be6e75f39b601d89c8398e31ec639c016bb Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 17 May 2018 16:33:04 +0100 Subject: [PATCH 194/451] Minor cleanup of old useless files. --- cache.nim | 32 - forum.nim | 2 +- public/css/style.css | 764 -------------------- public/images/Feed-icon.svg | 18 - public/images/bg.png | Bin 149129 -> 0 bytes public/images/forum-posts.png | Bin 174 -> 0 bytes public/images/forum-reply.png | Bin 405 -> 0 bytes public/images/forum-views.png | Bin 387 -> 0 bytes public/images/glow-arrow.png | Bin 8078 -> 0 bytes public/images/glow-line.png | Bin 2261 -> 0 bytes public/images/glow-line2.png | Bin 2295 -> 0 bytes public/images/head-link.png | Bin 180 -> 0 bytes public/images/head-link_hover.png | Bin 620 -> 0 bytes public/images/head.png | Bin 164 -> 0 bytes public/images/logo.png | Bin 111615 -> 0 bytes public/images/smilieys/icon_cool.png | Bin 2546 -> 0 bytes public/images/smilieys/icon_e_biggrin.png | Bin 2424 -> 0 bytes public/images/smilieys/icon_e_confused.png | Bin 2455 -> 0 bytes public/images/smilieys/icon_e_sad.png | Bin 2920 -> 0 bytes public/images/smilieys/icon_e_smile.png | Bin 2382 -> 0 bytes public/images/smilieys/icon_e_surprised.png | Bin 2791 -> 0 bytes public/images/smilieys/icon_e_wink.png | Bin 2526 -> 0 bytes public/images/smilieys/icon_exclaim.png | Bin 897 -> 0 bytes public/images/smilieys/icon_mad.png | Bin 2746 -> 0 bytes public/images/smilieys/icon_neutral.png | Bin 2193 -> 0 bytes public/images/smilieys/icon_razz.png | Bin 2508 -> 0 bytes todo.txt | 14 - 27 files changed, 1 insertion(+), 829 deletions(-) delete mode 100644 cache.nim delete mode 100644 public/css/style.css delete mode 100644 public/images/Feed-icon.svg delete mode 100644 public/images/bg.png delete mode 100644 public/images/forum-posts.png delete mode 100644 public/images/forum-reply.png delete mode 100644 public/images/forum-views.png delete mode 100644 public/images/glow-arrow.png delete mode 100644 public/images/glow-line.png delete mode 100644 public/images/glow-line2.png delete mode 100644 public/images/head-link.png delete mode 100644 public/images/head-link_hover.png delete mode 100644 public/images/head.png delete mode 100644 public/images/logo.png delete mode 100644 public/images/smilieys/icon_cool.png delete mode 100644 public/images/smilieys/icon_e_biggrin.png delete mode 100644 public/images/smilieys/icon_e_confused.png delete mode 100644 public/images/smilieys/icon_e_sad.png delete mode 100644 public/images/smilieys/icon_e_smile.png delete mode 100644 public/images/smilieys/icon_e_surprised.png delete mode 100644 public/images/smilieys/icon_e_wink.png delete mode 100644 public/images/smilieys/icon_exclaim.png delete mode 100644 public/images/smilieys/icon_mad.png delete mode 100644 public/images/smilieys/icon_neutral.png delete mode 100644 public/images/smilieys/icon_razz.png delete mode 100644 todo.txt diff --git a/cache.nim b/cache.nim deleted file mode 100644 index 6023393..0000000 --- a/cache.nim +++ /dev/null @@ -1,32 +0,0 @@ -import tables, uri -type - CacheInfo = object - valid: bool - value: string - - CacheHolder = ref object - caches: Table[string, CacheInfo] - -proc normalizePath(x: string): string = - let u = parseUri(x) - result = u.path & (if u.query != "": '?' & u.query else: "") - -proc newCacheHolder*(): CacheHolder = - new result - result.caches = initTable[string, CacheInfo]() - -proc invalidate*(cache: CacheHolder, name: string) = - cache.caches[name.normalizePath()].valid = false - -proc invalidateAll*(cache: CacheHolder) = - for key, val in mpairs(cache.caches): - val.valid = false - -template get*(cache: CacheHolder, name: string, grabValue: untyped): untyped = - ## Check to see if the cache contains value for ``name``. If it does and the - ## cache is valid then doesn't recalculate it but returns the cached version. - mixin normalizePath - let nName = name.normalizePath() - if not (cache.caches.hasKey(nName) and cache.caches[nName].valid): - cache.caches[nName] = CacheInfo(valid: true, value: grabValue) - cache.caches[nName].value diff --git a/forum.nim b/forum.nim index 965456c..987c2aa 100644 --- a/forum.nim +++ b/forum.nim @@ -8,7 +8,7 @@ import os, strutils, times, md5, strtabs, math, db_sqlite, - scgi, jester, asyncdispatch, asyncnet, cache, sequtils, + scgi, jester, asyncdispatch, asyncnet, sequtils, parseutils, utils, random, rst, recaptcha, json, re, sugar import cgi except setCookie import options diff --git a/public/css/style.css b/public/css/style.css deleted file mode 100644 index 26ca19d..0000000 --- a/public/css/style.css +++ /dev/null @@ -1,764 +0,0 @@ - -a, a * { cursor:pointer; } - -html { margin:0; overflow-x:auto; } -body { - overflow-x:hidden; - min-width:1030px; - margin:0; - font: 13pt Helvetica,Arial,sans-serif; - background:#152534 url("/images/bg.png") no-repeat fixed center top; } - -pre { - color: #F5F5F5; - overflow:auto; - margin:0; - padding:15px 10px; - font-size:10pt; - font-style:normal; - line-height:14pt; - background:rgba(0,0,0,.75); - border-left:8px solid rgba(0,0,0,.3); - margin-bottom: 10pt; - font-family: "DejaVu Sans Mono", monospace; -} -pre, pre * { cursor:text; } -pre .Comment { color:#6D6D6D; font-style:italic; } -pre .Keyword { color:#43A8CF; font-weight:bold; } -pre .Type { color:#128B7D; font-weight:bold; } -pre .Operator { font-weight: bold; } -pre .atr { color:#128B7D; font-weight:bold; font-style:italic; } -pre .def { color:#CAD6E4; font-weight:bold; font-style:italic; } -pre .StringLit { color:#854D6A; font-weight:bold; } -pre .DecNumber, pre .FloatNumber { color:#8AB647; } -pre .tab { border-left:1px dotted rgba(67,168,207,0.4); } -pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } -pre .EscapeSequence -{ - color: #C08D12; -} - -.tall { height:100%; } -.pre { padding:0 5px; font: 11pt "DejaVu Sans Mono",monospace; background:rgba(255,255,255,.30); border-radius:3px; } - -.page-layout { margin:0 auto; width:1000px; } -.docs-layout { margin:0 40px; } -.talk-layout { margin:0 40px; } -.wide-layout { margin:0 auto; } - -#head { - height:100px; - margin-bottom: 40px; - background:url("/images/head.png") repeat-x bottom; } -#head.docs { margin-left:280px; background:rgba(0,0,0,.25) url("/images/head-fade.png") no-repeat right top; } -#head > div { position:relative } - - #head-logo { - position:absolute; - left:-390px; - top:0; - width:917px; - height:268px; - pointer-events:none; - background:url("/images/logo.png") no-repeat; } - #head.docs #head-logo { left:-381px; position:fixed; } - #head.forum #head-logo { left:-370px; } - - #head-logo-link { - position:absolute; - display:block; - top:10px; - left:10px; - width:236px; - height:85px; } - #head.docs #head-logo-link { left:-260px; } - #head.forum #head-logo-link { left:30px; } - - #head-links { position:absolute; right:0; bottom:13px; } - #head.docs #head-links, - #head.forum #head-links { right:20px; } - #head-links > a { - display:block; - float:left; - padding:10px 25px 25px 25px; - color:rgba(255,255,255,.5); - font-size:14pt; - text-decoration:none; - letter-spacing:1px; - background:url("/images/head-link.png") no-repeat center bottom; - transition: - color 0.3s ease-in-out, - text-shadow 0.4s ease-in-out; } - #head-links > a:hover, - #head-links > a.active { - position: relative; - color:#1cb3ec; - text-shadow:0 0 4px rgba(28,179,236,.8); - background-image:url("/images/head-link_hover.png"); } - - #head-links > a.active:after { - display: block; - content: ""; - width: 771px; - background: url("/images/glow-arrow.png") no-repeat left; - height: 41px; - position: absolute; - left: 50%; - bottom: -49px; - transform: translateX(-618px); } - - #head-banner { width:200px; height:100px; background:#000; } - - #glow-line-vert { - position:fixed; - top:100px; - left:280px; - width:3px; - height:844px; - background:url("/images/glow-line-vert.png") no-repeat; } - - -#body { z-index:1; position:relative; background:rgba(220,231,248,.6); color:black; } -#body.docs { margin:0 40px 20px 320px; } -#body.forum { margin:0 40px 20px 40px; min-height: 700px; } - - #body-border { - position:absolute; - top:-25px; - left:0; - right:0; - height:35px; - background:rgba(0,0,0,.25); } - - #body-border-left { - position:absolute; - left:-25px; - top:-25px; - bottom:-25px; - width:35px; - background:rgba(0,0,0,.25); } - - #body-border-right { - position:absolute; - right:-25px; - top:-25px; - bottom:-25px; - width:35px; - background:rgba(0,0,0,.25); } - - #body-border-bottom { - position:absolute; - left:10px; - right:10px; - bottom:-25px; - height:35px; - background:rgba(0,0,0,.25); } - - #body.docs #body-border, - #body.forum #body-border { left:10px; right:10px; } - - #glow-line { - position:absolute; - top:-27px; - left:100px; - right:-25px; - height:3px; - background:url("/images/glow-line.png") no-repeat left; } - #glow-line-bottom { - position:absolute; - bottom:-27px; - left:-25px; - right:100px; - height:3px; - background:url("/images/glow-line2.png") no-repeat right; } - - #content { padding:40px 0; } - #content.page { width:680px; min-height:800px; padding-left:20px; } - #content h1 { font-size:20pt; letter-spacing:1px; color:rgba(0,0,0,.75); } - #content h2 { font-size:16pt; letter-spacing:1px; color:rgba(0,0,0,.7); margin-top:40px; } - #content p { color: #1D1D1D; margin: 5pt 0pt; } - #content a { color:#CEDAE9; text-decoration:none; } - #content a:hover { color:#fff; } - #content ul { padding-left:20px; } - #content li { margin-bottom:10px; text-align:justify; } - - #talk-heads { overflow:auto; margin:0 8px 0 8px; } - #talk-heads > div { float:left; font-size:120%; font-weight:bold; } - #talk-heads > .topic { width:45%; } - #talk-heads > .detail { width:15%; } - #talk-heads > .activity { width:25%; } - #talk-heads > .users { width:15%; } - #talk-heads > div > div { margin:0 10px 10px 10px; padding:0 10px 10px 10px; border-bottom:1px dashed rgba(0,0,0,0.4); } - #talk-heads > .topic > div { margin-left:0; } - #talk-heads > .activity > div { margin-right:0; } - - #talk-thread > div { - background-color: rgba(255, 255, 255, 0.5); - } - #talk-thread > div, - #talk-threads > div { - position:relative; - margin:5px 0; - overflow:auto; - border-radius:3px; - border:8px solid rgba(0,0,0,.8); - border-top:none; - border-bottom:none; - } - #talk-threads > div - { - line-height: 150%; - background:rgba(0,0,0,0.1); - } - #talk-threads > div:nth-child(odd) { background:rgba(0,0,0,0.2); } - #talk-thread > div > div, - #talk-threads > div > div - { - float:left; - text-overflow: ellipsis; - overflow: hidden; - font-size: 13pt; - } - #talk-threads > div > div > div { margin: 5px 10px; } - #talk-thread > div > div > div { margin: 15px 10px; } - #talk-thread > div > .topic - { - margin-top: 15pt; - white-space: normal; - } - #talk-thread > div > .topic > div - { - margin-left: 15px; - } - #talk-thread > div > .topic > div > span.date - { - position: absolute; - top: 5px; - right: 10pt; - border-bottom: 1px dashed; - color: #3D3D3D; - } - #talk-threads > div > .topic { width:45%; } - #talk-threads > div > .users { width:15%; overflow:hidden; height: 30px; } - #talk-threads > div > .users > div > img - { - margin-bottom: -4pt; - cursor: help; - width: 20px; - } - #talk-threads > div > .detail { width:16%; overflow:hidden; } - #talk-thread > div > .author, - #talk-threads > div > .activity { - overflow:hidden; - background:rgba(0,0,0,0.8); - color: white; - - } - #talk-thread > div > .author { - width: 15%; - } - #talk-threads > div > .activity { - width:24%; - font-size: 9pt; - } - #talk-threads > div > .activity a - { - color: #1CB3EC; - } - #talk-threads > div > .activity a:hover - { - color: #ffffff; - } - #talk-thread > div > .author { - height: 100%; - position: absolute; - } - #talk-thread > div > .author a, - #talk-threads > div > .author a { color:#1cb3ec !important; } - #talk-thread > div > .author a:hover, - #talk-threads > div > .author a:hover { color:#fff !important; } - #talk-threads > div > .topic .pages { float:right; } - #talk-threads > div > .topic .pages > a - { - margin-right: 5pt; - } - #talk-threads > div > .topic > div > a - { - font-weight:bold; - white-space: nowrap; - } - #talk-threads > div > .topic > div > a:visited { color: #1a1a1a; } - #talk-threads > div > .detail > div { float:left; margin:0; } - #talk-threads > div > .detail > div > div { margin-left:15px; padding: 5px 5px 5px 22px; } - #talk-threads > div > .detail > div { width:50%; } - #talk-threads > div > .detail > div:first-child > div { background:url("/images/forum-views.png") no-repeat left; cursor: help; } - #talk-threads > div > .detail > div:last-child > div { background:url("/images/forum-posts.png") no-repeat left; cursor: help; } - - #talk-thread > div { margin:20px 0; min-height:160px; padding-bottom: 10pt; } - #talk-thread > div > .author > div > .avatar { margin-top:20px; } - #talk-thread > div > .author > div > .name { } - #talk-thread > div > .author > div > .date { font-size: 8pt; color: white; } - #talk-thread > div > .topic { width:85%; padding-bottom:10px; margin-left: 15%; } - #talk-thread > div > .topic pre, #markhelp pre.listing { - overflow:auto; - margin:0; - padding:15px 10px; - font-size:10pt; - font-style:normal; - line-height:14pt; - background:rgba(0,0,0,.75); - border-left:8px solid rgba(0,0,0,.3); - margin-bottom: 10pt; - font-family: "DejaVu Sans Mono", monospace; - } - #talk-thread > div > .topic a, #talk-thread > div > .topic a:visited, - #markhelp a, #markhelp a:visited - { - color: #3680C9; - text-decoration: none; - } - #talk-thread > div > .topic a:hover - { - text-decoration: underline; - } - #talk-head, - #talk-info { - overflow:auto; - border-radius:3px; - border:8px solid rgba(0,0,0,.2); - border-top:none; - border-bottom:none; - background:rgba(0,0,0,0.1); } - #talk-head { margin-bottom:20px; } - #talk-info { margin-top:20px; } - #talk-head > div, - #talk-info > div { float:left; } - #talk-head > .info, - #talk-info > .info { width:80%; } - #talk-head > .info-post, - #talk-info > .info-post { width: 85%; } - #talk-head > .user, - #talk-info > .user { width:20%; background:rgba(0,0,0,.2); } - #talk-head > .user-post, - #talk-info > .user-post { width: 15%; background:rgba(0,0,0,.2); } - #talk-info > .user-post .reply { font-weight:bold; padding-left:22px; background:url("/images/forum-reply.png") no-repeat left; } - #talk-info > .user-post a span - { - color: #CEDAE9 !important; - } - #talk-info > .user-post > a > div:hover > span - { - color: #fff !important; - } - #talk-head > div > div, - #talk-info > div > div, - #talk-info > div > a > div { padding:5px 20px; color: #1a1a1a; } - #talk-head > div > div { color: #353535; } - #talk-head > .detail > div { float:left; margin:0; } - #talk-head > .detail > div > div { padding-left:22px; } - #talk-head > .detail > div:first-child > div { background:url("/images/forum-views.png") no-repeat left; } - #talk-head > .detail > div:last-child > div { background:url("/images/forum-posts.png") no-repeat left; } - - #talk-nav { margin:20px 8px 0 8px; padding-top:10px; border-top:1px dashed rgba(0,0,0,0.4); text-align:center; } - #talk-nav > a.active { text-decoration:underline !important; } - #talk-nav > a, #talk-nav > span, #talk-info > .info-post > div > a, - #talk-info > .info-post > div > span { margin-left: 5pt; } - - .standout { - padding:5px 30px; - margin-bottom:20px; - border:8px solid rgba(0,0,0,.8); - border-right-width:16px; - border-top-width:0; - border-bottom-width:0; - border-radius:3px; - background:rgba(0,0,0,0.1); - box-shadow:1px 3px 12px rgba(0,0,0,.4); } - .standout h3 { margin-bottom:10px; padding-bottom:10px; border-bottom:1px dashed rgba(0,0,0,.8); } - .standout li { margin:0 !important; padding-top:10px; border-top:1px dashed rgba(0,0,0,.2); } - .standout ul { padding-bottom:5px; } - .standout ul.tools { list-style:url("/images/docs-tools.png"); } - .standout ul.library { list-style:url("/images/docs-library.png"); } - .standout ul.internal { list-style:url("/images/docs-internal.png"); } - .standout ul.tutorial { list-style:url("/images/docs-tutorial.png"); } - .standout ul.example { list-style:url("/images/docs-example.png"); } - .standout li:first-child { padding-top:0; border-top:none; } - .standout li p { margin:0 0 10px 0 !important; line-height:130%; } - .standout li > a { font-weight:bold; } - - .forum-user-info, - .forum-user-info * { cursor:help } - -#foot { height:150px; position:relative; top:-10px; letter-spacing:1px; } -#foot.home { background:url("/images/foot.png") repeat-x top; height:200px; } -#foot.docs { margin-left:320px; margin-right:40px; } -#foot.forum { margin-left:40px; margin-right:40px; } -#foot > div { position:relative; } -#foot.home > div { width:960px; } -#foot h4 { font-size:11pt; color:rgba(255,255,255,.4); margin:40px 0 6px 0; } -#foot a:hover { color:#fff; } - - #foot-links { float:left; } - #foot-links > div { float:left; padding:0 40px 0 0; line-height:120%; } - #foot-links a { display:block; font-size:10pt; color:rgba(255,255,255,.3); text-decoration:none; } - #foot-legal { float:right; font-size:10pt; color:rgba(255,255,255,.3); line-height:150%; text-align:right; } - #foot-legal a { color:inherit; text-decoration:none; } - #foot-legal > h4 > a { color:inherit; } - - #mascot { - z-index:2; - position:absolute; - top:-340px; - right:25px; - width:202px; - height:319px; - background:url("/images/mascot.png") no-repeat; } - -article#content -{ - width: 80%; - display: inline-block; -} - -div#sidebar -{ - background-color: rgba(255, 255, 255, 0.1); - - border-left: 8px solid rgba(0, 0, 0, 0.8); - border-right: 8px solid rgba(0, 0, 0, 0.8); - border-bottom: 8px solid rgba(0, 0, 0, 0.8); - border-radius: 3px; - - width: 15%; - margin-top: 40px; - - display: inline-block; - float: right; - - color: #FFF; -} - -div#sidebar .title -{ - background-color: rgba(0, 0, 0, 0.8); - color: #FFF; - text-align: center; - padding: 10pt; -} - -div#sidebar .content -{ - padding: 12pt; - overflow: auto; - -} - -div#sidebar .content .button, .runDiv>button, .runDiv>a -{ - background-color: rgba(0,0,0,0.2); - text-decoration: none; - color: #FFF; - padding: 4pt; - float: right; - border-bottom: 2px solid rgba(0,0,0,0.24); - font-size: 11pt; - margin-top: 5pt; -} - -div#sidebar .content .button:hover -{ - border-bottom: 2px solid rgba(0,0,0,0.5); -} - -div#sidebar .content input -{ - width: 99%; - margin-bottom: 10pt; - margin-top: 2pt; - - border: 1px solid #6D6D6D; - font-size: 12pt; -} - -div#sidebar .content a.avatar img -{ - float: left; - margin-top: 5pt; -} - -div#sidebar .content a.user -{ - background-color: rgba(0, 0, 0, 0.8); - color: #1cb3ec; - padding: 5pt; - width: 93%; - display: block; - text-align: center; - text-decoration: none; -} - -div#sidebar .content a.user:hover -{ - color: #FFF; -} - -div#sidebar .user .button -{ - float: left; - margin-top: 5pt; - width: 52.5%; -} - -div#sidebar .user .logout -{ - clear: left; - width: 52pt; - text-align: center; - margin-left: 0pt; -} - -div#sidebar .user .avatar > img -{ - margin-right: 5pt; -} - -div#sidebar .content .search -{ - text-align: center; - margin: auto; - display: block; - width: 95%; -} - -div#sidebar .content a#passreset { - color: #CEDAE9; - font-size: 9pt; - display: block; - text-decoration: none; - margin-top: -4pt; -} - -div#sidebar .content a#passreset:hover { - color: #fff; -} - - - -span.error -{ - float: left; - width: 100%; - color: #FF4848; - text-align: center; - font-size: 10pt; - background-color: rgba(0,0,0,0.8); - padding: 5pt 0pt; - font-weight: bold; -} - -section#body #content span.error -{ - width: 25%; - margin-top: 5px; - margin-bottom: 5px; -} - -article#content form -{ - border-right: 8px solid rgba(0, 0, 0, 0.2); - background-color: rgba(255, 255, 255, 0.1); - padding: 10pt 20pt; -} - -article#content form > input, article#content form > textarea -{ - border: 1px solid #6D6D6D; -} - -article#content form > input[type=text] -{ - width: 70%; - min-width: 500px; -} - -article#content form > textarea -{ - width: 100%; - height: 200px; -} - -article#content form > input:focus, article#content form > textarea:focus -{ - border: 1px solid #1cb3ec; -} - -hr -{ - border: 1px solid #3D3D3D; -} - -.activity .isoDate -{ - display: none; -} - - -/* highlighting current post */ - -div:target { - background: rgba(139, 218, 255, 0.25) !important; -} - -/* full-text search */ - -.searchResults h4 b, -.searchResults h5 b { - border-bottom: 1px dotted #ffffff; -} -.titleHeader { - margin-right: 1em; - color: #121212; - font-weight: bold; -} - -.postTitle b { - border-bottom: 1px solid #D7300C; -} - -.postTitle a:hover { - text-decoration: none !important; - border-bottom: 1px solid #D7300C; -} - -.searchForm { - margin-top: 0px; - margin-right: 1em; - margin-bottom: 0px; - margin-left: 1em; -} - -.searchHelp { - color: #000000 !important; - float: right; - font-size: 11px; - left: -17px; - top: 3px; - position: relative; - text-decoration: none; - text-shadow: #FFFF00 1px 1px 2px; - cursor: help; -} - -#talk-thread.searchResults > div > div > div { - margin: 15px 8px; -} - -form.searchNav { - display: inline; - border: none !important; - background: transparent !important; -} - -.searchNav input { - background: #858C97; - color: #000000; - border: 1px solid #333; -} - -.clear { - clear: both; - height: 1px; -} - -img.smiley { - width: 20px; - height: 20px; - vertical-align: middle; - margin: 0; -} - -img.rssfeed { - width: 16px; - float: right; - margin-top: 10px; -} - - -#markhelp { - width: 80%; - background-color: #cbcfd6; - padding: 2pt 10pt; - margin-top: 10pt; -} - -#markhelp .markheading { - background-color: #6fa1ff; - text-align: center; -} - -#markhelp table.rst { - width: 100%; - margin: 10px 0px; - font-size: 12pt; - border-collapse: collapse; -} - -#markhelp table tr, #markhelp table td { - width: 50%; - border: 1px solid #7d7d7d; -} - -#markhelp table td { - padding:4px 9px; -} - -blockquote { - padding: 0px 8px; - margin: 10px 0px; - border-left: 2px solid rgb(61, 61, 61); - color: rgb(109, 109, 109); -} - -blockquote p { - color: rgb(109, 109, 109) !important; - -} - -.runDiv > hr { - border: 1px solid #80828d; -} - -.runDiv > a { - color: #FFF !important; - padding: 3.5pt; - margin-right: 3pt; -} - -.runDiv > button, .runDiv > a { - cursor: pointer; - border: none; /* remove border from runDiv>button */ - border-bottom: 2px solid rgba(0,0,0,0.24); - background-color: #80828d; -} - -.runDiv>button:hover, .runDiv > a:hover { - text-decoration: none !important; - border-bottom: 2px solid rgba(255, 255, 255, 0.5); -} - -.runDiv > .resDiv { - width: 80%; - padding: 0.2em 1em 0.2em 1em; - display: inline-block; -} - -.successComp { - color: lightgreen; -} -.failedComp { - color: lightcoral; -} -.date > a, .date > a:hover, .date > a:visited { - color: #3D3D3D !important; - text-decoration: none !important; -} \ No newline at end of file diff --git a/public/images/Feed-icon.svg b/public/images/Feed-icon.svg deleted file mode 100644 index b325149..0000000 --- a/public/images/Feed-icon.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/public/images/bg.png b/public/images/bg.png deleted file mode 100644 index 91f335913d7c01b14e7b063354768f1f9b955291..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 149129 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

_;vtA}lQoQQ}qP3*`qO(sKh6w*9 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 N002ovPDHLkV1kn-m2Ch3 diff --git a/public/images/forum-posts.png b/public/images/forum-posts.png deleted file mode 100644 index 4c4c63b06f1f472393b8dbde2e4da822dbd3560a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^0w6XA8<1SE`<)7qVoUONcVYMsf(!O8p9~b?Ebxdd z2GVaqm~nOfyScI&x{QBZMM^Smg`H)0Bv9FR$0rX*D47#hQZU- K&t;ucLK6VE8ZSx! diff --git a/public/images/forum-reply.png b/public/images/forum-reply.png deleted file mode 100644 index 12e9c60ed9bcbaf0e3bfdbc9824f64b5f563ce61..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 405 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

SB%ADnC-=EqhoL zxDo1nm6|u&W0?QZ_-pvjkmKDc{$Js`;<0tGv&00pj(&QKKxD?1yVu?=6%&$yn|)gq zw!?3W-ZQ}d11rbY@!>&CfH@c_PMYa%%Fhe`sm1^Dyjat$wUCvFk~@f=u0oPKKJ5Z* z&90~VpN_(815rRl3m7o(vpnCj@Fcs&BT{BrX~W40$-15`#<#2O1%Oo`UK{uJ&fM%t z{(oGNmj(}VZVg#J?nkj4-EKG`d#+?|vPA`ema5nPGAkf=uk{?rGI)M^oXLfO`uwhPj-#%u11pDGqy7*0UGIwk diff --git a/public/images/glow-line.png b/public/images/glow-line.png deleted file mode 100644 index 6607bdee90e9b89858ca7cd0c243c7830c71a4e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2261 zcmV;`2rBo9P)OxV zSEvt@yMEgdTR&fmL*{|j_OwjT1QR%fus<>fJ^lEl7DitZ4n}qExaEA6PBmpdTZ>3J zKNT~>y@rxN&O{gM0T@bXLdy$^(Goc&d4YppvK9pDIy0L#?UM&xv+ zx#N?Gevk4pJlJ{5foXe=SzWDsvCpd4Ze;y)V9&3YYL-1uh*9s z@35-p@B+fYM_cT_A?)_HPdD*TiK9<8>W1O#HTnqr z4%58)x)e?k~Pd#aPWFGF|qxh8+S7xL>vxO0c} z^V3l7j+7^-;Q%P(o61LLDveZjUWR%px=T7ejE?6TO{LsSG(H)MTeoWQ>70I}L7L_$HNs+;NL5-Weu+bxKKP;WngQxPG8=W{L!ri&op z87)FA1!BZqp3&6X2F)%M0hg}Y?f;Im*MeM9uN$D0`tIJLc}Dh4e#Q@xHh1&)suKG7 ztl8(1ibM5C5Txbx*ad{}e5t~gOBbrLsvki@AiK(Uh}qI?tRg}K?jz%#DOb7WK5T@{ z%)U$MZK?O0US9p83)q@(ge@8wEp1JiF95{`>bj6+W1+CvKZVMlc~h(vTg&{J)tu`z zv4xU=KnT5@!7RT3

? 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 RY4HF6002ovPDHLkV1h;FVOszI diff --git a/public/images/head-link.png b/public/images/head-link.png deleted file mode 100644 index 8e55b94b4aa84f9abe5fa4263106aff5b15e58b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 180 zcmeAS@N?(olHy`uVBq!ia0vp^_CU|gW!U_%O?XxI14-? zi-9_Tvb@(Boit`w00r4gJbhi+?{V;Ot6TrPJ6#PZ{5-w@ diff --git a/public/images/head-link_hover.png b/public/images/head-link_hover.png deleted file mode 100644 index 4180fbc1f6831d8c8fab7ef05ee08c720b650943..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 620 zcmV-y0+aoTP)xfCgiLS0%%ot+fj{4w~q zZP|hxC+&=qjYdkbGe{`=ytw$X|QQ^H|_AHX~_%U&wH=$UJRv_5y>!A z3NBD-gQ_9pMh&CZPwqyNrpUYIaDWH7fG@_|aS=j8;Nod(eE#ftB3l!a1^O^uxDK=y zFIzMJU6I!%E~n5xax8H*rW2vUB*fq~IHg0yrW(b71^$$?xFmV^r9jx*vRM-=Ci7Pl zBk+s9(LZvS>^+NI$cLO!0-{z=7pWR8&|n&Gw%uSY&%8~W%Po_M9lP%D+T7nujKHr| z54O>No(CJqgIw6f$f-NPFj8UFp(S$E%Mj*UCVMRtF7_=ZtHkwhK@Ir*-gc9t(64Nu zf6MhNgo%A9Vj1~zw3%uD1pwX&t4`?SEVc}(2gk$}AHu$j0pxmQ9?|SKHflc?dUu4NBX04Lnh{uKWXlKi30~!N7B6pANjgjygOdnB3AwM!z0) zu_cFwOAamt1F_`vthdk-xDH0^Ev|`uz25e`*f@jnV)G5PnrNUVI-xZH0000|s?uXD00$(}UFNr1&sfF)7kj?(TES}cw~jBIW(2L_~EX6*rLX7F_Nb6Mw<&;$Uc ClPk3V diff --git a/public/images/logo.png b/public/images/logo.png deleted file mode 100644 index 2594249cf54a9ab9cbe8c374aee56b4cbdf8269f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 111615 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 g=CV-?)28KoG(uB#dBxbM9Gj9Y% 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 P)63$gD$djf0`1 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 diff --git a/public/images/smilieys/icon_cool.png b/public/images/smilieys/icon_cool.png deleted file mode 100644 index 37e55e35b6ccc668e08a2ddc1b9f63c13383c32f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2546 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 V_9{I_@IL?m002ovPDHLkV1k+To}K^z diff --git a/public/images/smilieys/icon_e_sad.png b/public/images/smilieys/icon_e_sad.png deleted file mode 100644 index 1ef02913f3c6febd1f25d77c279953d334bc6dcc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2920 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 diff --git a/public/images/smilieys/icon_e_surprised.png b/public/images/smilieys/icon_e_surprised.png deleted file mode 100644 index 60a82d4bb883a0f57abd1d2e49eac999505bd854..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2791 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 diff --git a/public/images/smilieys/icon_e_wink.png b/public/images/smilieys/icon_e_wink.png deleted file mode 100644 index e32171de6f7855083b29e5bc5610b5746f23e24a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2526 zcmV<42_g20P)t}& zYaDpAt|BJjKW!;c!rnEHfFUCW3SW(e4_}@H*)K&w%^N=jb9wTJlVCNf4Pr$FA*x1Pli>$x*z_4F^JFJdogMhJz*FGbQL9+dV(kGuT*n)v?M(S z7OL(R1n#oeW{w}V$=+TS`eOQ*q zOCq4ZTs^6yfU)`B{nFQCctNJ3ty}N_l>c!u)ctuX>|6IJ9NIJun*Wg)34!q@^(UeU zxK*sW;S0WCNpwKq%YSh%>|g&VbQUv_of)WD5<|dky(-feyf)1rJe~hij02X6|JeIZ`c29YrKC@n>8NJ=lpE!0C(GQ-_+>s`g*3s_wLudIn^(sU(8Jy zi{J;d0{ff51YkekkTwz_#CyWrFB#>46-@8`U3 z#pCyKHOGcuKL|V09)e?8v67DJE1egu%Hec{AYx*Do>ff5mU(p#g;AqST zr}+wSFQ*fnU1z|4B6C>a`?2IE(uGp&BK98G`uqeqxiwBgm8ewGe?s7w6C9_u$R^-6HeF(iEwXoi7sZw2A}YjBya+JwOp5U>dc+doxKsneFDKZK zuaivzWtt_J!o`x1roIPi-<(3KkCszo8C&R)hz zQ{@91Zy# zsB;&ac)^n;%zDBd2M#S2O+fMM4@+vfH$U?e=o`E~B7ur0E*LHmXvM2FYZYzzOs7}^ zECzQWKzz?A7rw6J2gzRR%QbLSjYZYx;rA;gWT}5sJ z>?bzf0cE#CoMsL$0gE%S8BnzJVOaKa zw?t>im@H|SqG;7Aoh=eb7^oclKzMWW_3w>6b!4PpL;BY^w6A_KuxA1=+cIphJ&nzH-&O5vm~~EjEb|<5xf^LU&GI9B2geQQX$kNg#fZX&wAiuMG*9ku=D{T zni*^-0**)}0EeU=UxZ@FB_lt0%oY(Vkr~-Fqykq;C7@kL$AI-F;mqH`dApJ8|G{Ac zXWP-pJojEfkX=OG;2Nm}ko8vZL};q_F++?Ak<^$nOJOQu?+T^{1wRI3(#%jd=j)sx zf|;O2DgjtRlM9Vu0yGsviqLI6qmy$CEahXD0K1WW&Mk_TB*n-fpndFmffcIR?|I4M zSm2oCkl?v6F@|if|6D+?GRPr-v_f9tF=Er7<|Y5p7%nCU8jXp{2qperKtkoeHcaD& zsB#D*5%hoW#lyjk(>RSFgakCw(pSLYC+kkMDxa}z|j#Ukv};0qEZ$r*A8Ap626Ez*U*gM&-C^47NBPxNZM zhF|%qlSHk^;9g*xtbnLv!1uh8o0ClSN{aaT-clTe)7njFgDYDldPm4ymN*%O5}Ondh|bpAihy^k08%6a!eH7-_mhj(*L6wZN_&<8W6Bg+i;~^NOjZl#dC6Hp>pesnMRTI|KYv)e!r1lg zlj8-LO(LN{T0Q)2aZHU}dewP3B-|?U6;4L7+EW*XU|HI|G>x9{v8?)FWQgXHFir$kG-LNdgRkU&fAh#awzCxk#s zYxM2vM*q>-@|dq-37bQGL{OC(kB-G4nFK2-Ih>f`(a%TZ!_yXH$`3JZW}(W|B$PEo z$Z2L$Z#e;TP{SJ=Pmg=r;e>CR%aRvk3f+#R)M0US8UyGxl;Ta4O-zPl6Q1MKIHsz@ o6t)9@Ur);M1&07*qoM6N<$f&yp50RR91 diff --git a/public/images/smilieys/icon_exclaim.png b/public/images/smilieys/icon_exclaim.png deleted file mode 100644 index 9391f2bea947414c2d869a08d3f1d1e8a2ba391e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 897 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+yeRz!)Fk6XN>+|Nm7Cq5B;iR%vT* zQ&HKkpm0paaF!n0 z*{^K0M@j#nywM&x!-Gn?AT3G;`xNx{$Q$lcHrS`6w_gdU-C(bx{(c1@TYsN|0Z`3e zdHubL2KzvQK!*N4MZJAW`g?(FB|V^Cph}>|eF`8gdx54w6et2^p=^*|1)xzt5ujc$ z0vZcc05%h(5oj7n3S=+PK!_Tk0*Dk)9K?od1TjEdpcaS-hym3C)(){0rXFr1+yuM~ z?R_tGfFWK|666=mz|6wN&cVsWFCZ)`E-57=E3c@etfHo=rE6enZfWi8;_Bw%;~x+h z8WtWKmz0s2mtR>|-_X?6J7MCi*>mU3U$kV|s?}@Ou3Nux^VS_ZckSJG@X+BSM^BtO zd+z+zYu9hyx^w@*qsLF4K70P+<%f@7zW@C7=kLE|Q=9$*qdL~p#W6%e^6i0#=D~>! zY#-9KH)nd!Y6;u2M5J5guFe{-1s$QTBC1D4w>|%FFB|*5_P+nz&vyG>%&D{Z8Z>?L z1=aTJd?)8l*S!!^ao*zw`$?YOy=fd>Tjn`^Xt`F$^l#dW6Nm5nTjd?NuP3|r!}qy; zuRm<}J0AMuw}U(TpS=qoEB!fpv5@cIyo(n(>K|{(5@6X%n_rS=Jw`FIf2;yq_EOIiTm_jZ;pAW)=_Rukv-=k~>?!vhVJZ_&;o=$!zUD%qRW; PBbvd})z4*}Q$iB}CJTCj diff --git a/public/images/smilieys/icon_mad.png b/public/images/smilieys/icon_mad.png deleted file mode 100644 index 3029c96f15110bc4c101388e92ef46ac425a6757..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2746 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 Wty2bOP1`E~0000 Date: Thu, 17 May 2018 16:37:11 +0100 Subject: [PATCH 195/451] Reintroduce spectre submodule. --- .gitmodules | 4 ++-- frontend/spectre | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) create mode 160000 frontend/spectre diff --git a/.gitmodules b/.gitmodules index ef41742..78fdace 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "redesign/spectre"] - path = redesign/spectre +[submodule "frontend/spectre"] + path = frontend/spectre url = https://github.com/picturepan2/spectre diff --git a/frontend/spectre b/frontend/spectre new file mode 160000 index 0000000..7a6af53 --- /dev/null +++ b/frontend/spectre @@ -0,0 +1 @@ +Subproject commit 7a6af53bca72b549b2cbf1948763348bd5b30dcd From 840d8164ebd27c369c5f2a5c84346cf33410087f Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 17 May 2018 17:13:02 +0100 Subject: [PATCH 196/451] Moves the new design to `/`. --- forum.nim | 181 +++++----------------------------------- frontend/forum.nim | 12 ++- frontend/header.nim | 2 +- frontend/karax.html | 6 +- frontend/karaxutils.nim | 2 +- 5 files changed, 34 insertions(+), 169 deletions(-) diff --git a/forum.nim b/forum.nim index 987c2aa..0a3f4db 100644 --- a/forum.nim +++ b/forum.nim @@ -1191,24 +1191,19 @@ proc executeRegister(c: TForumData, name, pass, antibot, userIp, initialise() routes: - get "/": - createTFD() - c.isThreadsList = true - var count = 0 - let threadList = genThreadsList(c, count) - let data = genMain(c, threadList, - additionalHeaders = genRSSHeaders(c), showRssLinks = true) - resp data - get "/karax/nimforum.css": + get "/nimforum.css": resp readFile("frontend/nimforum.css"), "text/css" - get "/karax/nimcache/forum.js": + get "/nimcache/forum.js": resp readFile("frontend/nimcache/forum.js"), "application/javascript" - get "/karax/images/crown.png": - resp readFile("frontend/images/crown.png"), "image/png" + get re"/images/(.+?\.png)/?": + let path = "frontend/images/" & request.matches[0] + if fileExists(path): + resp readFile(path), "image/png" + else: + resp Http404, "No such file." - - get "/karax/threads.json": + get "/threads.json": var start = getInt(@"start", 0) count = getInt(@"count", 30) @@ -1227,7 +1222,7 @@ routes: resp $(%list), "application/json" - get "/karax/posts.json": + get "/posts.json": createTFD() var id = getInt(@"id", -1) @@ -1274,7 +1269,7 @@ routes: resp $(%list), "application/json" - get "/karax/specific_posts.json": + get "/specific_posts.json": createTFD() var ids = parseJson(@"ids") @@ -1296,7 +1291,7 @@ routes: resp $(%list), "application/json" - get "/karax/post.rst": + get "/post.rst": createTFD() let postId = getInt(@"id", -1) cond postId != -1 @@ -1311,7 +1306,7 @@ routes: else: resp content, "text/x-rst" - get "/karax/profile.json": + get "/profile.json": createTFD() var username = @"username" @@ -1395,7 +1390,7 @@ routes: resp $(%profile), "application/json" - post "/karax/login": + post "/login": createTFD() let formData = request.formData cond "username" in formData @@ -1411,7 +1406,7 @@ routes: except ForumError as exc: resp Http400, $(%exc.data), "application/json" - post "/karax/signup": + post "/signup": createTFD() let formData = request.formData let username = formData["username"].body @@ -1432,7 +1427,7 @@ routes: let exc = (ref ForumError)(getCurrentException()) resp Http400, $(%exc.data), "application/json" - get "/karax/status.json": + get "/status.json": createTFD() let user = @@ -1458,7 +1453,7 @@ routes: ) resp $(%status), "application/json" - post "/karax/preview": + post "/preview": createTFD() if not c.loggedIn(): let err = PostError( @@ -1481,7 +1476,7 @@ routes: ) resp Http400, $(%err), "application/json" - post "/karax/createPost": + post "/createPost": createTFD() if not c.loggedIn(): let err = PostError( @@ -1510,7 +1505,7 @@ routes: except ForumError as exc: resp Http400, $(%exc.data), "application/json" - post "/karax/updatePost": + post "/updatePost": createTFD() if not c.loggedIn(): let err = PostError( @@ -1538,7 +1533,7 @@ routes: except ForumError as exc: resp Http400, $(%exc.data), "application/json" - post "/karax/newthread": + post "/newthread": createTFD() if not c.loggedIn(): let err = PostError( @@ -1561,7 +1556,7 @@ routes: except ForumError as exc: resp Http400, $(%exc.data), "application/json" - get re"/karax/(.+)?": + get re"/(.+)?": resp readFile("frontend/karax.html") get "/threadActivity.xml": @@ -1573,140 +1568,6 @@ routes: createTFD() resp genPostsRSS(c), "application/atom+xml" - get "/t/@threadid/?@page?/?@postid?/?": - createTFD() - parseInt(@"threadid", c.threadId, -1..1000_000) - - if c.threadId == unselectedThread: - # Thread has just been deleted. - redirect(uri("/")) - - if @"page".len > 0: - parseInt(@"page", c.pageNum, 0..1000_000) - if @"postid".len > 0: - parseInt(@"postid", c.postId, 0..1000_000) - cond(c.pageNum > 0) - var count = 0 - var pSubject = getThreadTitle(c.threadid, c.pageNum) - cond validThreadId(c) - gatherTotalPosts(c) - if (@"action").len > 0: - var title = "" - case @"action" - of "reply": - let subject = getValue(db, - sql"select header from post where id = (select max(id) from post where thread = ?)", - $c.threadId).prependRe - body = genPostsList(c, $c.threadId, count) - cond count != 0 - body.add genFormPost(c, "doreply", "Reply", subject, "", false) - title = "Replying to thread: " & pSubject - of "edit": - cond c.postId != -1 - const query = sql"select header, content from post where id = ?" - let row = getRow(db, query, $c.postId) - let header = ||row[0] - let content = ||row[1] - body = genFormPost(c, "doedit", "Edit", header, content, true) - title = "Editing post" - else: discard - resp c.genMain(body, title & " - Nim Forum") - else: - incrementViews(c) - let posts = genPostsList(c, $c.threadId, count) - cond count != 0 - resp genMain(c, posts, pSubject & " - Nim Forum") - - get "/page/?@page?/?": - createTFD() - c.isThreadsList = true - cond(@"page" != "") - parseInt(@"page", c.pageNum, 0..1000_000) - cond(c.pageNum > 0) - var count = 0 - let list = genThreadsList(c, count) - if count == 0: - pass() - resp genMain(c, list, "Page " & $c.pageNum & " - Nim Forum", - genRSSHeaders(c), showRssLinks = true) - - get "/profile/@nick/?": - createTFD() - cond(@"nick" != "") - var userinfo: TUserInfo - if gatherUserInfo(c, @"nick", userinfo): - resp genMain(c, c.genProfile(userinfo), - @"nick" & "'s profile - Nim Forum") - else: - halt() - - get "/login/?": - createTFD() - resp genMain(c, genFormLogin(c), "Log in - Nim Forum") - - get "/logout/?": - createTFD() - logout(c) - redirect(uri("/")) - - get "/register/?": - createTFD() - resp genMain(c, genFormRegister(c), "Register - Nim Forum") - - template readIDs() = - # Retrieve the threadid, postid and pagenum - if (@"threadid").len > 0: - parseInt(@"threadid", c.threadId, -1..1000_000) - if (@"postid").len > 0: - parseInt(@"postid", c.postId, -1..1000_000) - - template finishLogin() = - setCookie("sid", c.userpass, daysForward(7)) - redirect(uri("/")) - - template handleError(action: string, topText: string, isEdit: bool) = - if c.isPreview: - body().add genPostPreview(c, @"subject", @"content", - c.userName, $getGMTime(getTime())) - body().add genFormPost(c, action, topText, reuseText, reuseText, isEdit) - resp genMain(c, body(), "Nim Forum - " & - (if c.isPreview: "Preview" else: "Error")) - - post "/donewthread": - createTFD() - if newThread(c): - redirect(uri("/")) - else: - body = "" - handleError("donewthread", "New thread", false) - - post "/doreply": - createTFD() - readIDs() - if reply(c): - redirect(c.genThreadUrl(pageNum = $(c.getPagesInThread+1)) & "#" & $c.postId) - else: - var count = 0 - if c.isPreview: - c.pageNum = c.getPagesInThread+1 - body = genPostsList(c, $c.threadId, count) - handleError("doreply", "Reply", false) - - post "/doedit": - createTFD() - readIDs() - if edit(c, c.postId): - redirect(c.genThreadUrl(postId = $c.postId, - pageNum = $(c.getPagesInThread+1))) - else: - body = "" - handleError("doedit", "Edit", true) - - get "/newthread/?": - createTFD() - resp genMain(c, genFormPost(c, "donewthread", "New thread", "", "", false), - "New Thread - Nim Forum") - get "/deleteAll/?": createTFD() cond(@"nick" != "") diff --git a/frontend/forum.nim b/frontend/forum.nim index ee74f8f..c86386c 100644 --- a/frontend/forum.nim +++ b/frontend/forum.nim @@ -4,7 +4,7 @@ from dom import window, Location include karax/prelude import jester/patterns -import threadlist, postlist, header, profile, newthread +import threadlist, postlist, header, profile, newthread, error import karaxutils type @@ -29,7 +29,6 @@ proc onPopState(event: dom.Event) = state.url = window.location redraw() -const appName = "/karax" type Params = Table[string, string] type Route = object @@ -38,12 +37,17 @@ type proc r(n: string, p: proc (params: Params): VNode): Route = Route(n: n, p: p) proc route(routes: openarray[Route]): VNode = + let path = + if state.url.pathname.len == 0: "/" else: $state.url.pathname + let prefix = if appName == "/": "" else: appName for route in routes: - let pattern = (appName & route.n).parsePattern() - let (matched, params) = pattern.match($state.url.pathname) + let pattern = (prefix & route.n).parsePattern() + let (matched, params) = pattern.match(path) if matched: return route.p(params) + return renderError("Unmatched route: " & path) + proc render(): VNode = result = buildHtml(tdiv()): renderHeader() diff --git a/frontend/header.nim b/frontend/header.nim index a09221a..cffa5e9 100644 --- a/frontend/header.nim +++ b/frontend/header.nim @@ -84,7 +84,7 @@ when defined(js): tdiv(class="navbar container grid-xl"): section(class="navbar-section"): a(href=makeUri("/")): - img(src="/karax/images/crown.png", id="img-logo") # TODO: Customisation. + img(src="/images/logo.png", id="img-logo") # TODO: Customisation. section(class="navbar-section"): tdiv(class="input-group input-inline"): input(class="search-input input-sm", diff --git a/frontend/karax.html b/frontend/karax.html index 8c35343..85ea520 100644 --- a/frontend/karax.html +++ b/frontend/karax.html @@ -8,14 +8,14 @@ The Nim programming language forum - + - +

- + diff --git a/frontend/karaxutils.nim b/frontend/karaxutils.nim index e34b7a2..94372f0 100644 --- a/frontend/karaxutils.nim +++ b/frontend/karaxutils.nim @@ -24,7 +24,7 @@ when defined(js): import dom except window - const appName = "/karax/" + const appName* = "/" proc class*(classes: varargs[tuple[name: string, present: bool]], defaultClasses: string = ""): string = From 21b8165751b4d2ea391e4fa805f54f810ff6bc74 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 17 May 2018 17:27:08 +0100 Subject: [PATCH 197/451] Hide moderated posts. Add indicators of user rank on posts. --- frontend/nimforum.scss | 4 ++++ frontend/post.nim | 5 +++++ frontend/postlist.nim | 11 +++++++++++ frontend/threadlist.nim | 7 +++++-- 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/frontend/nimforum.scss b/frontend/nimforum.scss index e6f5376..00d17ef 100644 --- a/frontend/nimforum.scss +++ b/frontend/nimforum.scss @@ -300,6 +300,10 @@ $views-color: #545d70; .post-username { font-weight: bold; display: inline-block; + + i { + margin-left: $control-padding-x; + } } .post-time { diff --git a/frontend/post.nim b/frontend/post.nim index 1e70da4..0817c00 100644 --- a/frontend/post.nim +++ b/frontend/post.nim @@ -19,6 +19,7 @@ type ## older versions of the post. info*: PostInfo moreBefore*: seq[int] + isDeleted*: bool PostLink* = object ## Used by profile creation*: int64 @@ -26,6 +27,10 @@ type threadId*: int postId*: int +proc isModerated*(post: Post): bool = + ## Determines whether the specified thread is under moderation. + post.author.rank <= Moderated + when defined(js): import karaxutils diff --git a/frontend/postlist.nim b/frontend/postlist.nim index 2bce6f7..2226f1d 100644 --- a/frontend/postlist.nim +++ b/frontend/postlist.nim @@ -203,6 +203,15 @@ when defined(js): tdiv(class="post-title"): tdiv(class="post-username"): text post.author.name + if post.isModerated: + italic(class="fas fa-eye-slash", + title="User is moderated") + if post.author.rank == Moderator: + italic(class="fas fa-shield-alt", + title="User is a moderator") + if post.author.rank == Admin: + italic(class="fas fa-chess-knight", + title="User is an admin") tdiv(class="post-time"): let title = post.info.creation.fromUnix().local. format("MMM d, yyyy HH:mm") @@ -293,6 +302,8 @@ when defined(js): tdiv(class="posts"): var prevPost: Option[Post] = none[Post]() for i, post in list.posts: + if not post.visibleTo(currentUser): continue + if prevPost.isSome: genTimePassed(prevPost.get(), some(post), false) if post.moreBefore.len > 0: diff --git a/frontend/threadlist.nim b/frontend/threadlist.nim index 9f2465e..482b6e0 100644 --- a/frontend/threadlist.nim +++ b/frontend/threadlist.nim @@ -48,11 +48,14 @@ when defined(js): var state = newState() - proc visibleTo(thread: Thread, user: Option[User]): bool = - ## Determines whether the specified thread should be shown to the user. + proc visibleTo*[T](thread: T, user: Option[User]): bool = + ## Determines whether the specified thread (or post) should be + ## shown to the user. This procedure is generic and works on any + ## object with a `isModerated` proc. ## ## The rules for this are determined by the rank of the user, their ## settings (TODO), and whether the thread's creator is moderated or not. + mixin isModerated if user.isNone(): return not thread.isModerated let rank = user.get().rank From 3804faab3b329b115a2fee45a5678f5ab01d01ba Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 17 May 2018 17:35:54 +0100 Subject: [PATCH 198/451] Fixes issues caused by incorrect user comparisons. --- frontend/user.nim | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/user.nim b/frontend/user.nim index 6966a7b..2cfcb8e 100644 --- a/frontend/user.nim +++ b/frontend/user.nim @@ -21,6 +21,9 @@ type proc isOnline*(user: User): bool = return getTime().toUnix() - user.lastOnline < (60*5) +proc `==`*(u1, u2: User): bool = + u1.name == u2.name + when defined(js): include karax/prelude import karaxutils From 7eb6b081adbbcdccbb4f75b9859fb6ffeaf9079f Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 17 May 2018 19:22:46 +0100 Subject: [PATCH 199/451] Fixes forum to work in dev mode. Creates setup_nimforum script. --- createdb.nim | 128 ------------------------- forms.tmpl | 5 - forum.nim | 98 +++++-------------- frontend/header.nim | 4 +- setup_nimforum.nim | 228 ++++++++++++++++++++++++++++++++++++++++++++ static/license.rst | 10 +- utils.nim | 2 + 7 files changed, 259 insertions(+), 216 deletions(-) delete mode 100644 createdb.nim create mode 100644 setup_nimforum.nim diff --git a/createdb.nim b/createdb.nim deleted file mode 100644 index 83e4b86..0000000 --- a/createdb.nim +++ /dev/null @@ -1,128 +0,0 @@ -# -# -# The Nim Forum -# (c) Copyright 2012 Andreas Rumpf, Dominik Picheta -# Look at license.txt for more info. -# All rights reserved. -# - -import strutils, db_sqlite - -var db = open(connection="nimforum.db", user="postgres", password="", - database="nimforum") - -const - TUserName = "varchar(20)" - TPassword = "varchar(32)" - TEmail = "varchar(30)" - -db.exec(sql""" -create table if not exists thread( - id integer primary key, - name varchar(100) not null, - views integer not null, - modified timestamp not null default (DATETIME('now')) -);""", []) - -db.exec(sql""" -create unique index if not exists ThreadNameIx on thread (name); -""", []) - -db.exec(sql(""" -create table if not exists person( - id integer primary key, - name $# not null, - password $# not null, - email $# not null, - creation timestamp not null default (DATETIME('now')), - salt varbin(128) not null, - status varchar(30) not null, - lastOnline timestamp not null default (DATETIME('now')) -);""" % [TUserName, TPassword, TEmail]), []) -# echo "person table already exists" - -db.exec(sql(""" -alter table person -add ban varchar(128) not null default '' -""")) - -db.exec(sql""" -create unique index if not exists UserNameIx on person (name); -""", []) - -# ----------------------- Forum ------------------------------------------------ - - -if not db.tryExec(sql""" -create table if not exists post( - id integer primary key, - author integer not null, - ip inet not null, - header varchar(100) not null, - content varchar(1000) not null, - thread integer not null, - creation timestamp not null default (DATETIME('now')), - - foreign key (thread) references thread(id), - foreign key (author) references person(id) -);""", []): - echo "post table already exists" - -# -------------------- Session ------------------------------------------------- - -if not db.tryExec(sql(""" -create table if not exists session( - id integer primary key, - ip inet not null, - password $# not null, - userid integer not null, - lastModified timestamp not null default (DATETIME('now')), - foreign key (userid) references person(id) -);""" % [TPassword]), []): - echo "session table already exists" - -if not db.tryExec(sql""" -create table if not exists antibot( - id integer primary key, - ip inet not null, - answer varchar(30) not null, - created timestamp not null default (DATETIME('now')) -);""", []): - echo "antibot table already exists" - - -db.exec sql"create index PersonStatusIdx on person(status);" -db.exec sql"create index PostByAuthorIdx on post(thread, author);" - -# -------------------- Search -------------------------------------------------- - -if not db.tryExec(sql""" - CREATE VIRTUAL TABLE thread_fts USING fts4 ( - id INTEGER PRIMARY KEY, - name VARCHAR(100) NOT NULL - );""", []): - echo "thread_fts table already exists or fts4 not supported" -else: - db.exec(sql""" - INSERT INTO thread_fts - SELECT id, name FROM thread; - """, []) -if not db.tryExec(sql""" - CREATE VIRTUAL TABLE post_fts USING fts4 ( - id INTEGER PRIMARY KEY, - header VARCHAR(100) NOT NULL, - content VARCHAR(1000) NOT NULL - );""", []): - echo "post_fts table already exists or fts4 not supported" -else: - db.exec(sql""" - INSERT INTO post_fts - SELECT id, header, content FROM post; - """, []) - - -# ------------------------------------------------------------------------------ - -#discard stdin.readline() - -close(db) diff --git a/forms.tmpl b/forms.tmpl index c88ac9a..f50fb25 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -320,12 +320,9 @@ ${fieldValid(c, "email", "E-Mail:")} ${textWidget(c, "email", reuseText, maxlength=300)} - #if useCaptcha: ${fieldValid(c, "g-recaptcha-response", "Captcha:")} ${captcha.render(includeNoScript=true)} - - #end if #if c.errorMsg != "":
@@ -494,12 +491,10 @@ ${fieldValid(c, "nick", "Your nickname:")} - #if useCaptcha: ${fieldValid(c, "g-recaptcha-response", "Captcha:")} ${captcha.render(includeNoScript=true)} - #end if #if c.errorMsg != "":
diff --git a/forum.nim b/forum.nim index 0a3f4db..7273959 100644 --- a/forum.nim +++ b/forum.nim @@ -13,6 +13,8 @@ import import cgi except setCookie import options +import auth + import frontend/threadlist except User import frontend/[ category, postlist, error, header, post, profile, user, karaxutils @@ -85,7 +87,6 @@ var db: DbConn isFTSAvailable: bool config: Config - useCaptcha: bool captcha: ReCaptcha proc newForumError(message: string, @@ -230,62 +231,7 @@ proc genGravatar(email: string, size: int = 80): string = result = "" % [$size, $size, getGravatarUrl(email, size)] -proc randomSalt(): string = - result = "" - for i in 0..127: - var r = random(225) - if r >= 32 and r <= 126: - result.add(chr(random(225))) -proc devRandomSalt(): string = - when defined(posix): - result = "" - var f = open("/dev/urandom") - var randomBytes: array[0..127, char] - discard f.readBuffer(addr(randomBytes), 128) - for i in 0..127: - if ord(randomBytes[i]) >= 32 and ord(randomBytes[i]) <= 126: - result.add(randomBytes[i]) - f.close() - else: - result = randomSalt() - -proc makeSalt(): string = - ## Creates a salt using a cryptographically secure random number generator. - ## - ## Ensures that the resulting salt contains no ``\0``. - try: - result = devRandomSalt() - except IOError: - result = randomSalt() - - var newResult = "" - for i in 0 .. 0: var captchaValid: bool = false try: captchaValid = await captcha.verify(antibot, userIp) @@ -365,15 +311,10 @@ proc checkLoggedIn(c: TForumData) = c.req.ip, pass) let row = getRow(db, - sql"select name, email, status, ban from person where id = ?", c.userid) + sql"select name, email, status from person where id = ?", c.userid) c.username = ||row[0] c.email = ||row[1] c.rank = parseEnum[Rank](||row[2]) - let ban = getBanErrorMsg(||row[3], c.rank) - if ban.len > 0: - discard c.setError("name", ban) - logout(c) - return # Update lastOnline db.exec(sql"update person set lastOnline = DATETIME('now') where id = ?", @@ -917,16 +858,13 @@ proc initialise() = database="nimforum") isFTSAvailable = db.getAllRows(sql("SELECT name FROM sqlite_master WHERE " & "type='table' AND name='post_fts'")).len == 1 + config = loadConfig() if len(config.recaptchaSecretKey) > 0 and len(config.recaptchaSiteKey) > 0: - useCaptcha = true captcha = initReCaptcha(config.recaptchaSecretKey, config.recaptchaSiteKey) else: - useCaptcha = false - var http = true - if paramCount() > 0: - if paramStr(1) == "scgi": - http = false + doAssert config.isDev, "Recaptcha required for production!" + echo("[WARNING] No recaptcha secret key specified.") template createTFD() = var c {.inject.}: TForumData @@ -1027,13 +965,13 @@ proc executeReply(c: TForumData, threadId: int, content: string, # Verify that content can be parsed as RST. let retID = insertID( db, - crud(crCreate, "post", "author", "ip", "header", "content", "thread"), - c.userId, c.req.ip, subject, content, $threadId, "" + crud(crCreate, "post", "author", "ip", "content", "thread"), + c.userId, c.req.ip, content, $threadId, "" ) discard tryExec( db, - crud(crCreate, "post_fts", "id", "header", "content"), - retID.int, subject, content + crud(crCreate, "post_fts", "id", "content"), + retID.int, content ) exec(db, sql"update thread set modified = DATETIME('now') where id = ?", @@ -1156,7 +1094,7 @@ proc executeRegister(c: TForumData, name, pass, antibot, userIp, raise newForumError("Please choose a longer password", @["password"]) # captcha validation: - if useCaptcha: + if config.recaptchaSecretKey.len > 0: var verifyFut = captcha.verify(antibot, userIp) yield verifyFut if verifyFut.failed: @@ -1409,14 +1347,22 @@ routes: post "/signup": createTFD() let formData = request.formData + if not config.isDev: + cond "g-recaptcha-response" in formData + let username = formData["username"].body let password = formData["password"].body + let recaptcha = + if "g-recaptcha-response" in formData: + formData["g-recaptcha-response"].body + else: + "" try: discard await executeRegister( c, username, password, - formData["g-recaptcha-response"].body, + recaptcha, request.host, formData["email"].body ) @@ -1446,7 +1392,7 @@ routes: let status = UserStatus( user: user, recaptchaSiteKey: - if useCaptcha: + if not config.isDev: some(config.recaptchaSiteKey) else: none[string]() diff --git a/frontend/header.nim b/frontend/header.nim index cffa5e9..6d38306 100644 --- a/frontend/header.nim +++ b/frontend/header.nim @@ -75,8 +75,8 @@ when defined(js): not getLoggedInUser().isNone proc renderHeader*(): VNode = - if state.data.isNone: - getStatus() # TODO: Call this every render? + if state.data.isNone and state.status == Http200: + getStatus() let user = state.data.map(x => x.user).flatten result = buildHtml(tdiv()): # TODO: Why do some buildHtml's need this? diff --git a/setup_nimforum.nim b/setup_nimforum.nim new file mode 100644 index 0000000..3cc7847 --- /dev/null +++ b/setup_nimforum.nim @@ -0,0 +1,228 @@ +# +# +# The Nim Forum +# (c) Copyright 2018 Andreas Rumpf, Dominik Picheta +# Look at license.txt for more info. +# All rights reserved. +# +# Script to initialise the nimforum. + +import strutils, db_sqlite, os, times, json + +import auth, frontend/user + +proc backup(path: string) = + if existsFile(path): + let backupPath = path & "." & $getTime().toUnix() + echo(path, " already exists. Moving to ", backupPath) + moveFile(path, backupPath) + +proc initialiseDb(admin: tuple[username, password, email: string]) = + let path = getCurrentDir() / "nimforum.db" + backup(path) + + var db = open(connection="nimforum.db", user="", password="", + database="nimforum") + + const + userNameType = "varchar(20)" + passwordType = "varchar(50)" + emailType = "varchar(254)" # https://stackoverflow.com/a/574698/492186 + + # -- Category + + db.exec(sql""" + create table category( + id integer primary key, + name varchar(100) not null, + description varchar(500) not null, + color varchar(10) not null + ); + + insert into category (id, name, description, color) + values (0, 'Default', '', ''); + """) + + # -- Thread + + db.exec(sql""" + create table thread( + id integer primary key, + name varchar(100) not null, + views integer not null, + modified timestamp not null default (DATETIME('now')), + category integer not null default 0, + isLocked boolean not null default 0, + solution integer, + isDeleted boolean not null default 0, + + foreign key (category) references category(id), + foreign key (solution) references post(id) + );""", []) + + db.exec(sql""" + create unique index ThreadNameIx on thread (name); + """, []) + + # -- Person + + db.exec(sql(""" + create table person( + id integer primary key, + name $# not null, + password $# not null, + email $# not null, + creation timestamp not null default (DATETIME('now')), + salt varbin(128) not null, + status varchar(30) not null, + lastOnline timestamp not null default (DATETIME('now')), + isDeleted boolean not null default 0 + );""" % [userNameType, passwordType, emailType]), []) + + db.exec(sql""" + create unique index UserNameIx on person (name); + """, []) + db.exec sql"create index PersonStatusIdx on person(status);" + + # Create default user. + let salt = makeSalt() + let password = makePassword(admin.password, salt) + db.exec(sql""" + insert into person (id, name, password, email, salt, status) + values (0, ?, ?, ?, ?, ?); + """, admin.username, password, admin.email, salt, $Admin) + + # -- Post + + db.exec(sql""" + create table post( + id integer primary key, + author integer not null, + ip inet not null, + content varchar(1000) not null, + thread integer not null, + creation timestamp not null default (DATETIME('now')), + isDeleted boolean not null default 0, + + foreign key (thread) references thread(id), + foreign key (author) references person(id) + );""", []) + + db.exec sql"create index PostByAuthorIdx on post(thread, author);" + + db.exec(sql""" + create table postRevision( + id integer primary key, + creation timestamp not null default (DATETIME('now')), + original integer not null, + content varchar(1000) not null, + + foreign key (original) references post(id) + ) + """) + + # -- Session + + db.exec(sql(""" + create table session( + id integer primary key, + ip inet not null, + password $# not null, + userid integer not null, + lastModified timestamp not null default (DATETIME('now')), + foreign key (userid) references person(id) + );""" % [passwordType]), []) + + # -- Likes + + db.exec(sql(""" + create table like( + id integer primary key, + author integer not null, + post integer not null, + creation timestamp not null default (DATETIME('now')), + + foreign key (author) references person(id), + foreign key (post) references post(id) + ) + """)) + + # -- Report + + db.exec(sql(""" + create table report( + id integer primary key, + author integer not null, + post integer not null, + kind varchar(30) not null, + content varchar(500) not null default '', + + foreign key (author) references person(id), + foreign key (post) references post(id) + ) + """)) + + # -- FTS + + if not db.tryExec(sql""" + CREATE VIRTUAL TABLE thread_fts USING fts4 ( + id INTEGER PRIMARY KEY, + name VARCHAR(100) NOT NULL + );""", []): + echo "thread_fts table already exists or fts4 not supported" + else: + db.exec(sql""" + INSERT INTO thread_fts + SELECT id, name FROM thread; + """, []) + if not db.tryExec(sql""" + CREATE VIRTUAL TABLE post_fts USING fts4 ( + id INTEGER PRIMARY KEY, + content VARCHAR(1000) NOT NULL + );""", []): + echo "post_fts table already exists or fts4 not supported" + else: + db.exec(sql""" + INSERT INTO post_fts + SELECT id, content FROM post; + """, []) + + close(db) + +proc initialiseConfig( + name, hostname: string, + recaptcha: tuple[siteKey, secretKey: string], + smtp: tuple[address, user, password: string], + isDev: bool +) = + let path = getCurrentDir() / "forum.json" + backup(path) + + var j = %{ + "name": %name, + "hostname": %hostname, + "recaptchaSiteKey": %recaptcha.siteKey, + "recaptchaSecretKey": %recaptcha.secretKey, + "smtpAddress": %smtp.address, + "smtpUser": %smtp.user, + "smtpPassword": %smtp.password, + "isDev": %isDev + } + + writeFile(path, $j) + +when isMainModule: + if paramCount() > 0 and paramStr(1) == "--dev": + echo("Initialising nimforum for development...") + initialiseConfig( + "Development Forum", + "localhost.local", + recaptcha=("", ""), + smtp=("", "", ""), + isDev=true + ) + + initialiseDb( + admin=("admin", "admin", "admin@localhost.local") + ) + diff --git a/static/license.rst b/static/license.rst index 15534b7..3a15ec1 100644 --- a/static/license.rst +++ b/static/license.rst @@ -1,7 +1,7 @@ Forum content license ===================== -All the content contributed to the Nimrod Forum is `cc-wiki (aka cc-by-sa) +All the content contributed to the Nim Forum is `cc-wiki (aka cc-by-sa) `_ licensed, intended to be **shared and remixed**. In the future we may even provide all this data as a convenient data dump. @@ -16,13 +16,13 @@ attribution**:: Let us clarify what we mean by attribution. If you republish this content, we require that you: -* **Visually indicate that the content is from the Nimrod Forum**. It doesn’t +* **Visually indicate that the content is from the Nim Forum**. It doesn’t have to be obnoxious; a discreet text blurb is fine. * **Hyperlink directly to the original post** (e.g., - http://forum.nimrod-lang.org/t/186) + http://forum.nim-lang.org/t/186) * **Show the author names** for every post. * **Hyperlink each author name** directly back to their user profile page - (e.g., http://forum.nimrod-lang.org/profile/Araq) + (e.g., http://forum.nim-lang.org/profile/Araq) By “directly”, we mean each hyperlink must point directly to our domain in standard HTML visible even with JavaScript disabled, and not use a tinyurl or @@ -38,5 +38,5 @@ Feel free to remix and reuse to your heart’s content, as long as a good faith effort is made to attribute the content! Content previous to the forum license change of -http://forum.nimrod-lang.org/t/186 remains under the original authors' +http://forum.nim-lang.org/t/186 remains under the original authors' copyright, and therefore you cannot reuse it. diff --git a/utils.nim b/utils.nim index 028c42e..fea585e 100644 --- a/utils.nim +++ b/utils.nim @@ -24,6 +24,7 @@ type mlistAddress: string recaptchaSecretKey*: string recaptchaSiteKey*: string + isDev*: bool var docConfig: StringTableRef @@ -43,6 +44,7 @@ proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config = result.mlistAddress = root{"mlistAddress"}.getStr("") result.recaptchaSecretKey = root{"recaptchaSecretKey"}.getStr("") result.recaptchaSiteKey = root{"recaptchaSiteKey"}.getStr("") + result.isDev = root{"isDev"}.getBool() except: echo("[WARNING] Couldn't read config file: ", filename) From 87952e8d4de59feadd3778e8f518607487f1ea6d Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 17 May 2018 19:30:00 +0100 Subject: [PATCH 200/451] Fixes category creation in setup script. --- setup_nimforum.nim | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup_nimforum.nim b/setup_nimforum.nim index 3cc7847..c233a55 100644 --- a/setup_nimforum.nim +++ b/setup_nimforum.nim @@ -38,7 +38,9 @@ proc initialiseDb(admin: tuple[username, password, email: string]) = description varchar(500) not null, color varchar(10) not null ); + """) + db.exec(sql""" insert into category (id, name, description, color) values (0, 'Default', '', ''); """) From 3810220d3758d2b3cf8e10382b5cc0433321cc48 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 17 May 2018 19:32:30 +0100 Subject: [PATCH 201/451] Adds missing auth module. --- auth.nim | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 auth.nim diff --git a/auth.nim b/auth.nim new file mode 100644 index 0000000..3da20a8 --- /dev/null +++ b/auth.nim @@ -0,0 +1,60 @@ +import random, md5 + +import bcrypt + +proc randomSalt(): string = + result = "" + for i in 0..127: + var r = rand(225) + if r >= 32 and r <= 126: + result.add(chr(rand(225))) + +proc devRandomSalt(): string = + when defined(posix): + result = "" + var f = open("/dev/urandom") + var randomBytes: array[0..127, char] + discard f.readBuffer(addr(randomBytes), 128) + for i in 0..127: + if ord(randomBytes[i]) >= 32 and ord(randomBytes[i]) <= 126: + result.add(randomBytes[i]) + f.close() + else: + result = randomSalt() + +proc makeSalt*(): string = + ## Creates a salt using a cryptographically secure random number generator. + ## + ## Ensures that the resulting salt contains no ``\0``. + try: + result = devRandomSalt() + except IOError: + result = randomSalt() + + var newResult = "" + for i in 0 ..< result.len: + if result[i] != '\0': + newResult.add result[i] + return newResult + +proc makePassword*(password, salt: string, comparingTo = ""): string = + ## Creates an MD5 hash by combining password and salt. + when defined(windows): + result = getMD5(salt & getMD5(password)) + else: + let bcryptSalt = if comparingTo != "": comparingTo else: genSalt(8) + result = hash(getMD5(salt & getMD5(password)), bcryptSalt) + +proc makeIdentHash*(user, password, epoch, secret: string, + comparingTo = ""): string = + ## Creates a hash verifying the identity of a user. Used for password reset + ## links and email activation links. + ## If ``epoch`` is smaller than the epoch of the user's last login then + ## the link is invalid. + ## The ``secret`` is the 'salt' field in the ``person`` table. + echo(user, password, epoch, secret) + when defined(windows): + result = getMD5(user & password & epoch & secret) + else: + let bcryptSalt = if comparingTo != "": comparingTo else: genSalt(8) + result = hash(user & password & epoch & secret, bcryptSalt) \ No newline at end of file From e04403c7f1b4f31d11da578a5c99bf11af894392 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 17 May 2018 19:40:14 +0100 Subject: [PATCH 202/451] Adds replyingTo field to post. --- setup_nimforum.nim | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup_nimforum.nim b/setup_nimforum.nim index c233a55..6004d0c 100644 --- a/setup_nimforum.nim +++ b/setup_nimforum.nim @@ -105,9 +105,11 @@ proc initialiseDb(admin: tuple[username, password, email: string]) = thread integer not null, creation timestamp not null default (DATETIME('now')), isDeleted boolean not null default 0, + replyingTo integer, foreign key (thread) references thread(id), - foreign key (author) references person(id) + foreign key (author) references person(id), + foreign key (replyingTo) references post(id) );""", []) db.exec sql"create index PostByAuthorIdx on post(thread, author);" From 416655764d26ef3c90f6f223b8be092d15a05188 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 17 May 2018 19:54:15 +0100 Subject: [PATCH 203/451] Ensure deleted posts and threads are not accessible. --- forum.nim | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/forum.nim b/forum.nim index 7273959..27e2a73 100644 --- a/forum.nim +++ b/forum.nim @@ -1148,7 +1148,8 @@ routes: const threadsQuery = sql"""select id, name, views, strftime('%s', modified) from thread - order by modified desc limit ?, ?;""" # TODO: Moderation + where isDeleted = 0 + order by modified desc limit ?, ?;""" let thrCount = getValue(db, sql"select count(*) from thread;").parseInt() let moreCount = max(0, thrCount - (start + count)) @@ -1171,7 +1172,7 @@ routes: const threadsQuery = sql"""select id, name, views, strftime('%s', modified) from thread - where id = ?;""" + where id = ? and isDeleted = 0;""" let threadRow = getRow(db, threadsQuery, id) let thread = selectThread(threadRow) @@ -1181,7 +1182,7 @@ routes: """select p.id, p.content, strftime('%s', p.creation), p.author, u.name, u.email, strftime('%s', u.lastOnline), u.status from post p, person u - where u.id = p.author and p.thread = ? + where u.id = p.author and p.thread = ? and p.isDeleted = 0 order by p.id""" ) From 79c47d47f3dbe5ec41449791a08d837aee1c1299 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 17 May 2018 20:09:35 +0100 Subject: [PATCH 204/451] Implements proper 404s. --- forum.nim | 27 +++++++++++++++++++++++++++ frontend/error.nim | 17 ++++++++++++++++- frontend/forum.nim | 3 +++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/forum.nim b/forum.nim index 27e2a73..0e06a3b 100644 --- a/forum.nim +++ b/forum.nim @@ -1503,6 +1503,33 @@ routes: except ForumError as exc: resp Http400, $(%exc.data), "application/json" + get "/t/@id": + cond "id" in request.params + + const threadsQuery = + sql"""select id from thread where id = ? and isDeleted = 0;""" + + let value = getValue(db, threadsQuery, @"id") + if value == @"id": + pass + else: + redirect uri("/404") + + get "/profile/@username": + cond "username" in request.params + + const threadsQuery = + sql"""select name from person where name = ? and isDeleted = 0;""" + + let value = getValue(db, threadsQuery, @"username") + if value == @"username": + pass + else: + redirect uri("/404") + + get "/404": + resp Http404, readFile("frontend/karax.html") + get re"/(.+)?": resp readFile("frontend/karax.html") diff --git a/frontend/error.nim b/frontend/error.nim index a534bde..6a68df4 100644 --- a/frontend/error.nim +++ b/frontend/error.nim @@ -61,4 +61,19 @@ when defined(js): state.error = some(PostError( errorFields: @[], message: "Unknown error occurred." - )) \ No newline at end of file + )) + + proc render404*(): VNode = + result = buildHtml(): + tdiv(class="empty error"): + tdiv(class="empty icon"): + italic(class="fas fa-bug fa-5x") + p(class="empty-title h5"): + text "404 Not Found" + p(class="empty-subtitle"): + text "Cannot find what you are looking for, it might have been " & + "deleted. Sorry!" + tdiv(class="empty-action"): + a(href="/", onClick=anchorCB): + button(class="btn btn-primary"): + text "Go back home" \ No newline at end of file diff --git a/frontend/forum.nim b/frontend/forum.nim index c86386c..1ae644e 100644 --- a/frontend/forum.nim +++ b/frontend/forum.nim @@ -71,6 +71,9 @@ proc render(): VNode = ) ) ), + r("/404", + (params: Params) => render404() + ), r("/", (params: Params) => renderThreadList(getLoggedInUser())) ]) From c4df36d461b31652d9f96c92c238f6d1ed9b66aa Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 17 May 2018 20:14:41 +0100 Subject: [PATCH 205/451] Fixes thread activity not updating. --- forum.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forum.nim b/forum.nim index 0e06a3b..cf4a160 100644 --- a/forum.nim +++ b/forum.nim @@ -975,7 +975,7 @@ proc executeReply(c: TForumData, threadId: int, content: string, ) exec(db, sql"update thread set modified = DATETIME('now') where id = ?", - $c.threadId) + $threadId) return retID From e34501a61a0ec09353245e4dbfda27c735bfec60 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 17 May 2018 20:52:59 +0100 Subject: [PATCH 206/451] Implements replyingTo in the backend. --- forum.nim | 52 +++++++++++++++++++++++++++++++++++++---------- frontend/post.nim | 4 +++- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/forum.nim b/forum.nim index cf4a160..0802078 100644 --- a/forum.nim +++ b/forum.nim @@ -888,10 +888,12 @@ proc selectUser(userRow: seq[string], avatarSize: int=80): User = rank: parseEnum[Rank](userRow[3]) ) -proc selectPost(postRow: seq[string], skippedPosts: seq[int]): Post = +proc selectPost(postRow: seq[string], skippedPosts: seq[int], + replyingTo: Option[PostLink]): Post = return Post( id: postRow[0].parseInt, - author: selectUser(@[postRow[4], postRow[5], postRow[6], postRow[7]]), + replyingTo: replyingTo, + author: selectUser(@[postRow[5], postRow[6], postRow[7], postRow[8]]), likes: @[], # TODO: seen: false, # TODO: history: @[], # TODO: @@ -902,6 +904,28 @@ proc selectPost(postRow: seq[string], skippedPosts: seq[int]): Post = moreBefore: skippedPosts ) +proc selectReplyingTo(replyingTo: string): Option[PostLink] = + if replyingTo.len == 0: return + + const replyingToQuery = sql""" + select p.id, strftime('%s', p.creation), p.thread, + u.name, u.email, strftime('%s', u.lastOnline), u.status, + t.name + from post p, person u, thread t + where p.thread = t.id and p.author = u.id and p.id = ? and p.isDeleted = 0; + """ + + let row = getRow(db, replyingToQuery, replyingTo) + if row[0].len == 0: return + + return some(PostLink( + creation: row[1].parseInt(), + topic: row[^1], + threadId: row[2].parseInt(), + postId: row[0].parseInt(), + author: some(selectUser(@[row[3], row[4], row[5], row[6]])) + )) + proc selectThread(threadRow: seq[string]): Thread = const postsQuery = sql"""select count(*), strftime('%s', creation) from post @@ -950,23 +974,23 @@ proc selectThread(threadRow: seq[string]): Thread = return thread proc executeReply(c: TForumData, threadId: int, content: string, - replyingTo: int): int64 = + replyingTo: Option[int]): int64 = # TODO: Refactor TForumData. assert c.loggedIn() - let subject = "" # TODO: Remove this redundant field. if rateLimitCheck(c): raise newForumError("You're posting too fast!") if not validateRst(c, content): raise newForumError("Message needs to be valid RST", @["msg"]) - # TODO: Replying to. # Verify that content can be parsed as RST. let retID = insertID( db, - crud(crCreate, "post", "author", "ip", "content", "thread"), - c.userId, c.req.ip, content, $threadId, "" + crud(crCreate, "post", "author", "ip", "content", "thread", "replyingTo"), + c.userId, c.req.ip, content, $threadId, + if replyingTo.isSome(): $replyingTo.get() + else: nil ) discard tryExec( db, @@ -1043,7 +1067,7 @@ proc executeNewThread(c: TForumData, subject, msg: string): (int64, int64) = discard tryExec(db, crud(crCreate, "thread_fts", "id", "name"), c.threadID, subject) - result[1] = executeReply(c, result[0].int, msg, -1) + result[1] = executeReply(c, result[0].int, msg, none[int]()) discard tryExec(db, sql"insert into post_fts(post_fts) values('optimize')") discard tryExec(db, sql"insert into post_fts(thread_fts) values('optimize')") @@ -1180,6 +1204,7 @@ routes: let postsQuery = sql( """select p.id, p.content, strftime('%s', p.creation), p.author, + p.replyingTo, u.name, u.email, strftime('%s', u.lastOnline), u.status from post p, person u where u.id = p.author and p.thread = ? and p.isDeleted = 0 @@ -1200,7 +1225,8 @@ routes: let addDetail = i < count or rows.len-i < count or id == anchor if addDetail: - let post = selectPost(rows[i], skippedPosts) + let replyingTo = selectReplyingTo(rows[i][4]) + let post = selectPost(rows[i], skippedPosts, replyingTo) list.posts.add(post) skippedPosts = @[] else: @@ -1217,6 +1243,7 @@ routes: let intIDs = ids.elems.map(x => x.getInt()) let postsQuery = sql(""" select p.id, p.content, strftime('%s', p.creation), p.author, + p.replyingTo, u.name, u.email, strftime('%s', u.lastOnline), u.status from post p, person u where u.id = p.author and p.id in ($#) @@ -1226,7 +1253,7 @@ routes: var list: seq[Post] = @[] for row in db.getAllRows(postsQuery): - list.add(selectPost(row, @[])) + list.add(selectPost(row, @[], selectReplyingTo(row[4]))) resp $(%list), "application/json" @@ -1440,11 +1467,14 @@ routes: let threadId = getInt(formData["threadId"].body, -1) cond threadId != -1 - let replyingTo = + let replyingToId = if "replyingTo" in formData: getInt(formData["replyingTo"].body, -1) else: -1 + let replyingTo = + if replyingToId == -1: none[int]() + else: some(replyingToId) try: let id = executeReply(c, threadId, msg, replyingTo) diff --git a/frontend/post.nim b/frontend/post.nim index 0817c00..c2fda99 100644 --- a/frontend/post.nim +++ b/frontend/post.nim @@ -1,4 +1,4 @@ -import strformat +import strformat, options import user, threadlist @@ -20,12 +20,14 @@ type info*: PostInfo moreBefore*: seq[int] isDeleted*: bool + replyingTo*: Option[PostLink] PostLink* = object ## Used by profile creation*: int64 topic*: string threadId*: int postId*: int + author*: Option[User] ## Only used for `replyingTo`. proc isModerated*(post: Post): bool = ## Determines whether the specified thread is under moderation. From 1ecd8daa7c79791ce0d965ced444422019f7160a Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 17 May 2018 21:19:40 +0100 Subject: [PATCH 207/451] Implements categories in threads list. --- forum.nim | 22 +++++++++++++++------- frontend/category.nim | 19 ++++++++++++------- frontend/post.nim | 1 - frontend/threadlist.nim | 1 - 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/forum.nim b/forum.nim index 0802078..9ebc960 100644 --- a/forum.nim +++ b/forum.nim @@ -953,15 +953,19 @@ proc selectThread(threadRow: seq[string]): Thread = var thread = Thread( id: threadRow[0].parseInt, topic: threadRow[1], - category: Category(id: "", color: "#ff0000"), # TODO + category: Category( + id: threadRow[5].parseInt, + name: threadRow[6], + description: threadRow[7], + color: threadRow[8] + ), users: @[], replies: posts[0].parseInt-1, views: threadRow[2].parseInt, activity: threadRow[3].parseInt, creation: posts[1].parseInt, - isLocked: false, # TODO: + isLocked: threadRow[4] == "1", isSolved: false, # TODO: Add a field to `post` to identify the solution. - isDeleted: false # TODO: ) # Gather the users list. @@ -1171,8 +1175,10 @@ routes: count = getInt(@"count", 30) const threadsQuery = - sql"""select id, name, views, strftime('%s', modified) from thread - where isDeleted = 0 + sql"""select t.id, t.name, views, strftime('%s', modified), isLocked, + c.id, c.name, c.description, c.color + from thread t, category c + where isDeleted = 0 and category = c.id order by modified desc limit ?, ?;""" let thrCount = getValue(db, sql"select count(*) from thread;").parseInt() @@ -1195,8 +1201,10 @@ routes: count = 10 const threadsQuery = - sql"""select id, name, views, strftime('%s', modified) from thread - where id = ? and isDeleted = 0;""" + sql"""select t.id, t.name, views, strftime('%s', modified), isLocked, + c.id, c.name, c.description, c.color + from thread t, category c + where t.id = ? and isDeleted = 0 and category = c.id;""" let threadRow = getRow(db, threadsQuery, id) let thread = selectThread(threadRow) diff --git a/frontend/category.nim b/frontend/category.nim index 70c1293..6c20f94 100644 --- a/frontend/category.nim +++ b/frontend/category.nim @@ -1,7 +1,9 @@ type Category* = object - id*: string + id*: int + name*: string + description*: string color*: string @@ -13,11 +15,14 @@ when defined(js): proc render*(category: Category): VNode = result = buildHtml(): - if category.id.len > 0: - tdiv(class="triangle", - style=style( - (StyleAttr.borderBottom, kstring"0.6rem solid " & category.color) - )): - text category.id + if category.name.len >= 0: + tdiv(class="category", + "data-color"="#" & category.color): + tdiv(class="triangle", + style=style( + (StyleAttr.borderBottom, + kstring"0.6rem solid #" & category.color) + )) + text category.name else: span() \ No newline at end of file diff --git a/frontend/post.nim b/frontend/post.nim index c2fda99..dbde4af 100644 --- a/frontend/post.nim +++ b/frontend/post.nim @@ -19,7 +19,6 @@ type ## older versions of the post. info*: PostInfo moreBefore*: seq[int] - isDeleted*: bool replyingTo*: Option[PostLink] PostLink* = object ## Used by profile diff --git a/frontend/threadlist.nim b/frontend/threadlist.nim index 482b6e0..f311a0c 100644 --- a/frontend/threadlist.nim +++ b/frontend/threadlist.nim @@ -15,7 +15,6 @@ type creation*: int64 ## Unix timestamp isLocked*: bool isSolved*: bool - isDeleted*: bool ThreadList* = ref object threads*: seq[Thread] From 8d41060c54da620a71eed1395a1f955931a85343 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 17 May 2018 22:44:30 +0100 Subject: [PATCH 208/451] Implements display of replyingTo in front end. --- frontend/nimforum.scss | 11 ++++++++++- frontend/postlist.nim | 8 +++++++- frontend/user.nim | 7 ++++--- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/frontend/nimforum.scss b/frontend/nimforum.scss index 00d17ef..50481b6 100644 --- a/frontend/nimforum.scss +++ b/frontend/nimforum.scss @@ -306,8 +306,17 @@ $views-color: #545d70; } } - .post-time { + .post-metadata { float: right; + + .post-replyingTo { + display: inline-block; + margin-right: $control-padding-x; + + i.fa-reply { + transform: rotate(180deg); + } + } } } diff --git a/frontend/postlist.nim b/frontend/postlist.nim index 2226f1d..ab987ec 100644 --- a/frontend/postlist.nim +++ b/frontend/postlist.nim @@ -212,7 +212,13 @@ when defined(js): if post.author.rank == Admin: italic(class="fas fa-chess-knight", title="User is an admin") - tdiv(class="post-time"): + tdiv(class="post-metadata"): + if post.replyingTo.isSome(): + let replyingTo = post.replyingTo.get() + tdiv(class="post-replyingTo"): + a(href=renderPostUrl(replyingTo)): + italic(class="fas fa-reply") + renderUserMention(replyingTo.author.get()) let title = post.info.creation.fromUnix().local. format("MMM d, yyyy HH:mm") a(href=renderPostUrl(post, thread), title=title): diff --git a/frontend/user.nim b/frontend/user.nim index 2cfcb8e..7daa27e 100644 --- a/frontend/user.nim +++ b/frontend/user.nim @@ -38,6 +38,7 @@ when defined(js): proc renderUserMention*(user: User): VNode = result = buildHtml(): - # TODO: Add URL to profile. - span(class="user-mention"): - text "@" & user.name \ No newline at end of file + a(class="user-mention", + href=makeUri("/profile/" & user.name), + onClick=anchorCB): + text "@" & user.name \ No newline at end of file From 7f5e68331c719813f115a8309b9cca6435e7e2ca Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 18 May 2018 11:59:20 +0100 Subject: [PATCH 209/451] Adds needsPasswordReseti DB field for future use. --- setup_nimforum.nim | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup_nimforum.nim b/setup_nimforum.nim index 6004d0c..22eb38b 100644 --- a/setup_nimforum.nim +++ b/setup_nimforum.nim @@ -78,7 +78,8 @@ proc initialiseDb(admin: tuple[username, password, email: string]) = salt varbin(128) not null, status varchar(30) not null, lastOnline timestamp not null default (DATETIME('now')), - isDeleted boolean not null default 0 + isDeleted boolean not null default 0, + needsPasswordReset boolean not null default 0 );""" % [userNameType, passwordType, emailType]), []) db.exec(sql""" From 8acaca298bd70e5807e0b0ba00f4451a8f068c64 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 18 May 2018 12:01:00 +0100 Subject: [PATCH 210/451] Fixes bug with thread list loading twice on refresh. --- frontend/threadlist.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/threadlist.nim b/frontend/threadlist.nim index f311a0c..a50b66e 100644 --- a/frontend/threadlist.nim +++ b/frontend/threadlist.nim @@ -163,7 +163,7 @@ when defined(js): if state.status != Http200: return renderError("Couldn't retrieve threads.") - if state.list.isNone: + if state.list.isNone and (not state.loading): ajaxGet(makeUri("threads.json"), @[], onThreadList) return buildHtml(tdiv(class="loading loading-lg")) From d5f1c674c5f37cf9bfb35060f648ec438848879f Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 18 May 2018 14:03:35 +0100 Subject: [PATCH 211/451] Implements edit history. --- forum.nim | 43 +++++++++++++++++++++++++++++++++++------- frontend/editbox.nim | 3 +++ frontend/nimforum.scss | 13 +++++++++++++ frontend/post.nim | 3 +++ frontend/postlist.nim | 20 ++++++++++++++++++-- 5 files changed, 73 insertions(+), 9 deletions(-) diff --git a/forum.nim b/forum.nim index 9ebc960..3633ac1 100644 --- a/forum.nim +++ b/forum.nim @@ -889,14 +889,14 @@ proc selectUser(userRow: seq[string], avatarSize: int=80): User = ) proc selectPost(postRow: seq[string], skippedPosts: seq[int], - replyingTo: Option[PostLink]): Post = + replyingTo: Option[PostLink], history: seq[PostInfo]): Post = return Post( id: postRow[0].parseInt, replyingTo: replyingTo, author: selectUser(@[postRow[5], postRow[6], postRow[7], postRow[8]]), likes: @[], # TODO: seen: false, # TODO: - history: @[], # TODO: + history: history, info: PostInfo( creation: postRow[2].parseInt, content: postRow[1].rstToHtml() @@ -926,6 +926,20 @@ proc selectReplyingTo(replyingTo: string): Option[PostLink] = author: some(selectUser(@[row[3], row[4], row[5], row[6]])) )) +proc selectHistory(postId: int): seq[PostInfo] = + const historyQuery = sql""" + select strftime('%s', creation), content from postRevision + where original = ? + order by creation asc; + """ + + result = @[] + for row in getAllRows(db, historyQuery, $postId): + result.add(PostInfo( + creation: row[0].parseInt(), + content: row[1].rstToHtml() + )) + proc selectThread(threadRow: seq[string]): Thread = const postsQuery = sql"""select count(*), strftime('%s', creation) from post @@ -1032,7 +1046,15 @@ proc updatePost(c: TForumData, postId: int, content: string, raise newForumError("Message needs to be valid RST", @["msg"]) # Update post. - exec(db, crud(crUpdate, "post", "content"), content, $postId) + # - We create a new postRevision entry for our edit. + exec( + db, + crud(crCreate, "postRevision", "content", "original"), + content, + $postId + ) + # - We set the FTS to the latest content as searching for past edits is not + # supported. exec(db, crud(crUpdate, "post_fts", "content"), content, $postId) # Check if post is the first post of the thread. if subject.isSome(): @@ -1234,7 +1256,8 @@ routes: if addDetail: let replyingTo = selectReplyingTo(rows[i][4]) - let post = selectPost(rows[i], skippedPosts, replyingTo) + let history = selectHistory(rows[i][0].parseInt()) + let post = selectPost(rows[i], skippedPosts, replyingTo, history) list.posts.add(post) skippedPosts = @[] else: @@ -1261,7 +1284,8 @@ routes: var list: seq[Post] = @[] for row in db.getAllRows(postsQuery): - list.add(selectPost(row, @[], selectReplyingTo(row[4]))) + let history = selectHistory(row[0].parseInt()) + list.add(selectPost(row, @[], selectReplyingTo(row[4]), history)) resp $(%list), "application/json" @@ -1271,10 +1295,15 @@ routes: cond postId != -1 let postQuery = sql""" - select content from post where id = ?; + select content from ( + select content, creation from post where id = ? + union + select content, creation from postRevision where original = ? + ) + order by creation desc limit 1; """ - let content = getValue(db, postQuery, postId) + let content = getValue(db, postQuery, postId, postId) if content.len == 0: resp Http404, "Post not found" else: diff --git a/frontend/editbox.nim b/frontend/editbox.nim index 861c629..e6c6c62 100644 --- a/frontend/editbox.nim +++ b/frontend/editbox.nim @@ -58,6 +58,9 @@ when defined(js): (s: int, r: kstring) => onEditPost(s, r, state)) proc render*(state: EditBox, post: Post): VNode = + if state.status != Http200: + return renderError("Couldn't retrieve raw post") + if state.rawContent.isNone() or state.post.id != post.id: state.post = post state.rawContent = none[kstring]() diff --git a/frontend/nimforum.scss b/frontend/nimforum.scss index 50481b6..2fc3ad3 100644 --- a/frontend/nimforum.scss +++ b/frontend/nimforum.scss @@ -317,6 +317,19 @@ $views-color: #545d70; transform: rotate(180deg); } } + + .post-history { + display: inline-block; + margin-right: $control-padding-x; + + i { + font-size: 90%; + } + + .edit-count { + margin-right: $control-padding-x-sm/2; + } + } } } diff --git a/frontend/post.nim b/frontend/post.nim index dbde4af..77b97f8 100644 --- a/frontend/post.nim +++ b/frontend/post.nim @@ -28,6 +28,9 @@ type postId*: int author*: Option[User] ## Only used for `replyingTo`. +proc lastEdit*(post: Post): PostInfo = + post.history[^1] + proc isModerated*(post: Post): bool = ## Determines whether the specified thread is under moderation. post.author.rank <= Moderated diff --git a/frontend/postlist.nim b/frontend/postlist.nim index ab987ec..88fcbe3 100644 --- a/frontend/postlist.nim +++ b/frontend/postlist.nim @@ -119,7 +119,10 @@ when defined(js): let list = state.list.get() for i in 0 ..< list.posts.len: if list.posts[i].id == id: - list.posts[i].info.content = content + list.posts[i].history.add(PostInfo( + creation: getTime().toUnix(), + content: content + )) break proc onReplyClick(e: Event, n: VNode, p: Option[Post]) = @@ -219,6 +222,14 @@ when defined(js): a(href=renderPostUrl(replyingTo)): italic(class="fas fa-reply") renderUserMention(replyingTo.author.get()) + if post.history.len > 0: + let title = post.lastEdit.creation.fromUnix().local. + format("'Last modified' MMM d, yyyy HH:mm") + tdiv(class="post-history", title=title): + span(class="edit-count"): + text $post.history.len + italic(class="fas fa-pencil-alt") + let title = post.info.creation.fromUnix().local. format("MMM d, yyyy HH:mm") a(href=renderPostUrl(post, thread), title=title): @@ -227,7 +238,12 @@ when defined(js): if state.editing.isSome() and state.editing.get() == post: render(state.editBox, postCopy) else: - verbatim(post.info.content) + let content = + if post.history.len > 0: + post.lastEdit.content + else: + post.info.content + verbatim(content) genPostButtons(postCopy, currentUser) From 5c9f1bb85e8c26cb4f94909dc1b42967b786922f Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 18 May 2018 14:16:19 +0100 Subject: [PATCH 212/451] Rearrange post buttons. --- frontend/postlist.nim | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/frontend/postlist.nim b/frontend/postlist.nim index 88fcbe3..b323d1e 100644 --- a/frontend/postlist.nim +++ b/frontend/postlist.nim @@ -175,20 +175,18 @@ when defined(js): button(class="btn"): italic(class="far fa-trash-alt") - if not authoredByUser: - tdiv(class="like-button"): - button(class="btn"): - span(class="like-count"): - if post.likes.len > 0: - text $post.likes.len - italic(class="far fa-heart") - - if loggedIn: - tdiv(class="flag-button"): - button(class="btn"): - italic(class="far fa-flag") + tdiv(class="like-button"): + button(class="btn"): + span(class="like-count"): + if post.likes.len > 0: + text $post.likes.len + italic(class="far fa-heart") if loggedIn: + tdiv(class="flag-button"): + button(class="btn"): + italic(class="far fa-flag") + tdiv(class="reply-button"): button(class="btn", onClick=(e: Event, n: VNode) => onReplyClick(e, n, some(post))): From a0655e049de50262187e6d2e95f4da75218b2732 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 18 May 2018 17:04:34 +0100 Subject: [PATCH 213/451] Implements likes fully in frontend and backend. --- forum.nim | 93 +++++++++++++++++++++++++++++++++++++---- frontend/nimforum.scss | 4 ++ frontend/post.nim | 10 +++++ frontend/postbutton.nim | 68 +++++++++++++++++++++++++++++- frontend/postlist.nim | 13 +++--- 5 files changed, 169 insertions(+), 19 deletions(-) diff --git a/forum.nim b/forum.nim index 3633ac1..924d657 100644 --- a/forum.nim +++ b/forum.nim @@ -889,12 +889,13 @@ proc selectUser(userRow: seq[string], avatarSize: int=80): User = ) proc selectPost(postRow: seq[string], skippedPosts: seq[int], - replyingTo: Option[PostLink], history: seq[PostInfo]): Post = + replyingTo: Option[PostLink], history: seq[PostInfo], + likes: seq[User]): Post = return Post( id: postRow[0].parseInt, replyingTo: replyingTo, author: selectUser(@[postRow[5], postRow[6], postRow[7], postRow[8]]), - likes: @[], # TODO: + likes: likes, seen: false, # TODO: history: history, info: PostInfo( @@ -940,6 +941,18 @@ proc selectHistory(postId: int): seq[PostInfo] = content: row[1].rstToHtml() )) +proc selectLikes(postId: int): seq[User] = + const likeQuery = sql""" + select u.name, u.email, strftime('%s', u.lastOnline), u.status + from like h, person u + where h.post = ? and h.author = u.id + order by h.creation asc; + """ + + result = @[] + for row in getAllRows(db, likeQuery, $postId): + result.add(selectUser(row)) + proc selectThread(threadRow: seq[string]): Thread = const postsQuery = sql"""select count(*), strftime('%s', creation) from post @@ -1169,13 +1182,44 @@ proc executeRegister(c: TForumData, name, pass, antibot, userIp, raise newForumError("Couldn't send activation email", @["email"]) # Add account to person table - exec(db, - sql("INSERT INTO person(name, password, email, salt, status, lastOnline) " & - "VALUES (?, ?, ?, ?, ?, DATETIME('now'))"), name, - password, email, salt, $EmailUnconfirmed) + exec(db, sql""" + INSERT INTO person(name, password, email, salt, status, lastOnline) + VALUES (?, ?, ?, ?, ?, DATETIME('now')) + """, name, password, email, salt, $EmailUnconfirmed) return password +proc executeLike(c: TForumData, postId: int) = + # Verify the post exists and doesn't belong to the current user. + const postQuery = sql""" + select u.name from post p, person u + where p.id = ? and p.author = u.id and p.isDeleted = 0; + """ + + let postAuthor = getValue(db, postQuery, postId) + if postAuthor.len == 0: + raise newForumError("Specified post ID does not exist.", @["id"]) + + if postAuthor == c.username: + raise newForumError("You cannot like your own post.") + + # Save the like. + exec(db, crud(crCreate, "like", "author", "post"), c.userid, postId) + +proc executeUnlike(c: TForumData, postId: int) = + # Verify the post and like exists for the current user. + const likeQuery = sql""" + select l.id from like l, person u + where l.post = ? and l.author = u.id and u.name = ?; + """ + + let likeId = getValue(db, likeQuery, postId, c.username) + if likeId.len == 0: + raise newForumError("Like doesn't exist.", @["id"]) + + # Delete the like. + exec(db, crud(crDelete, "like"), likeId) + initialise() routes: @@ -1256,8 +1300,11 @@ routes: if addDetail: let replyingTo = selectReplyingTo(rows[i][4]) - let history = selectHistory(rows[i][0].parseInt()) - let post = selectPost(rows[i], skippedPosts, replyingTo, history) + let history = selectHistory(id) + let likes = selectLikes(id) + let post = selectPost( + rows[i], skippedPosts, replyingTo, history, likes + ) list.posts.add(post) skippedPosts = @[] else: @@ -1285,7 +1332,8 @@ routes: for row in db.getAllRows(postsQuery): let history = selectHistory(row[0].parseInt()) - list.add(selectPost(row, @[], selectReplyingTo(row[4]), history)) + let likes = selectLikes(row[0].parseInt()) + list.add(selectPost(row, @[], selectReplyingTo(row[4]), history, likes)) resp $(%list), "application/json" @@ -1570,6 +1618,33 @@ routes: except ForumError as exc: resp Http400, $(%exc.data), "application/json" + post re"/(like|unlike)": + createTFD() + if not c.loggedIn(): + let err = PostError( + errorFields: @[], + message: "Not logged in." + ) + resp Http401, $(%err), "application/json" + + let formData = request.formData + cond "id" in formData + + let postId = getInt(formData["id"].body, -1) + cond postId != -1 + + try: + case request.path + of "/like": + executeLike(c, postId) + of "/unlike": + executeUnlike(c, postId) + else: + assert false + resp Http200, "{}", "application/json" + except ForumError as exc: + resp Http400, $(%exc.data), "application/json" + get "/t/@id": cond "id" in request.params diff --git a/frontend/nimforum.scss b/frontend/nimforum.scss index 2fc3ad3..143eaad 100644 --- a/frontend/nimforum.scss +++ b/frontend/nimforum.scss @@ -366,6 +366,10 @@ $views-color: #545d70; .like-button i:hover, .like-button i.fas { color: #f783ac; } + + .like-count { + margin-right: $control-padding-x-sm; + } } #thread-buttons { diff --git a/frontend/post.nim b/frontend/post.nim index 77b97f8..7e46a6b 100644 --- a/frontend/post.nim +++ b/frontend/post.nim @@ -35,6 +35,16 @@ proc isModerated*(post: Post): bool = ## Determines whether the specified thread is under moderation. post.author.rank <= Moderated +proc isLikedBy*(post: Post, user: Option[User]): bool = + ## Determines whether the specified user has liked the post. + if user.isNone(): return false + + for u in post.likes: + if u.name == user.get().name: + return true + + return false + when defined(js): import karaxutils diff --git a/frontend/postbutton.nim b/frontend/postbutton.nim index 913b392..8bc89ef 100644 --- a/frontend/postbutton.nim +++ b/frontend/postbutton.nim @@ -8,7 +8,7 @@ when defined(js): include karax/prelude import karax/[kajax, kdom] - import error, karaxutils + import error, karaxutils, post, user type PostButton* = ref object @@ -74,4 +74,68 @@ when defined(js): if state.error.isSome(): p(class="text-error"): - text state.error.get().message \ No newline at end of file + text state.error.get().message + + + type + LikeButton* = ref object + error: Option[PostError] + loading: bool + + proc newLikeButton*(): LikeButton = + LikeButton() + + proc onPost(httpStatus: int, response: kstring, state: LikeButton, + post: Post, user: User) = + postFinished: + if post.isLikedBy(some(user)): + var newLikes: seq[User] = @[] + for like in post.likes: + if like.name != user.name: + newLikes.add(like) + post.likes = newLikes + else: + post.likes.add(user) + + proc onClick(ev: Event, n: VNode, state: LikeButton, post: Post, + currentUser: Option[User]) = + if state.loading: return + if currentUser.isNone(): + state.error = some[PostError](PostError(message: "Not logged in.")) + + state.loading = true + state.error = none[PostError]() + + # TODO: This is a hack, karax should support this. + var formData = newFormData() + formData.append("id", $post.id) + let uri = + if post.isLikedBy(currentUser): + makeUri("/unlike") + else: + makeUri("/like") + ajaxPost(uri, @[], cast[cstring](formData), + (s: int, r: kstring) => + onPost(s, r, state, post, currentUser.get())) + + ev.preventDefault() + + proc render*(state: LikeButton, post: Post, + currentUser: Option[User]): VNode = + + let liked = isLikedBy(post, currentUser) + let tooltip = + if state.error.isSome(): state.error.get().message + else: "" + + result = buildHtml(): + tdiv(class="like-button"): + button(class=class({"tooltip": state.error.isSome()}, "btn"), + onClick=(e: Event, n: VNode) => + (onClick(e, n, state, post, currentUser)), + "data-tooltip"=tooltip): + if post.likes.len > 0: + span(class="like-count"): + text $post.likes.len + + italic(class=class({"far": not liked, "fas": liked}, "fa-heart")) \ No newline at end of file diff --git a/frontend/postlist.nim b/frontend/postlist.nim index b323d1e..13c91d3 100644 --- a/frontend/postlist.nim +++ b/frontend/postlist.nim @@ -17,7 +17,7 @@ when defined(js): include karax/prelude import karax / [vstyles, kajax, kdom] - import karaxutils, error, replybox, editbox + import karaxutils, error, replybox, editbox, postbutton type State = ref object @@ -28,6 +28,7 @@ when defined(js): replyBox: ReplyBox editing: Option[Post] ## If in edit mode, this contains the post. editBox: EditBox + likeButton: LikeButton proc onReplyPosted(id: int) proc onEditPosted(id: int, content: string, subject: Option[string]) @@ -39,7 +40,8 @@ when defined(js): status: Http200, replyingTo: none[Post](), replyBox: newReplyBox(onReplyPosted), - editBox: newEditBox(onEditPosted, onEditCancelled) + editBox: newEditBox(onEditPosted, onEditCancelled), + likeButton: newLikeButton() ) var @@ -175,12 +177,7 @@ when defined(js): button(class="btn"): italic(class="far fa-trash-alt") - tdiv(class="like-button"): - button(class="btn"): - span(class="like-count"): - if post.likes.len > 0: - text $post.likes.len - italic(class="far fa-heart") + render(state.likeButton, post, currentUser) if loggedIn: tdiv(class="flag-button"): From dd9be8f639008d7217b5abe6fbc29d1e00e37f7e Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 18 May 2018 17:11:22 +0100 Subject: [PATCH 214/451] Reset LikeButton error on mouse leave. --- frontend/postbutton.nim | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/postbutton.nim b/frontend/postbutton.nim index 8bc89ef..4e146b6 100644 --- a/frontend/postbutton.nim +++ b/frontend/postbutton.nim @@ -102,6 +102,7 @@ when defined(js): if state.loading: return if currentUser.isNone(): state.error = some[PostError](PostError(message: "Not logged in.")) + return state.loading = true state.error = none[PostError]() @@ -133,7 +134,9 @@ when defined(js): button(class=class({"tooltip": state.error.isSome()}, "btn"), onClick=(e: Event, n: VNode) => (onClick(e, n, state, post, currentUser)), - "data-tooltip"=tooltip): + "data-tooltip"=tooltip, + onmouseleave=(e: Event, n: VNode) => + (state.error = none[PostError]())): if post.likes.len > 0: span(class="like-count"): text $post.likes.len From 41a6790fe8b3faf58fcb52c1cb581329e0ec5cb2 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 18 May 2018 19:54:33 +0100 Subject: [PATCH 215/451] Implements delete button fully in frontend and backend for posts and thread. --- forum.nim | 76 ++++++++++++++++++++---- frontend/delete.nim | 132 ++++++++++++++++++++++++++++++++++++++++++ frontend/postlist.nim | 31 ++++++++-- 3 files changed, 224 insertions(+), 15 deletions(-) create mode 100644 frontend/delete.nim diff --git a/forum.nim b/forum.nim index 924d657..4084888 100644 --- a/forum.nim +++ b/forum.nim @@ -953,6 +953,20 @@ proc selectLikes(postId: int): seq[User] = for row in getAllRows(db, likeQuery, $postId): result.add(selectUser(row)) +proc selectThreadAuthor(threadId: int): User = + const authorQuery = + sql""" + select name, email, strftime('%s', lastOnline), status + from person where id in ( + select author from post + where thread = ? + order by id + limit 1 + ) + """ + + return selectUser(getRow(db, authorQuery, threadId)) + proc selectThread(threadRow: seq[string]): Thread = const postsQuery = sql"""select count(*), strftime('%s', creation) from post @@ -964,16 +978,6 @@ proc selectThread(threadRow: seq[string]): Thread = from person u, post p where p.author = u.id and p.thread = ? group by name order by count(*) desc limit 5; """ - const authorQuery = - sql""" - select name, email, strftime('%s', lastOnline), status - from person where id in ( - select author from post - where thread = ? - order by id - limit 1 - ) - """ let posts = getRow(db, postsQuery, threadRow[0]) @@ -1000,7 +1004,7 @@ proc selectThread(threadRow: seq[string]): Thread = thread.users.add(selectUser(user)) # Grab the author. - thread.author = selectUser(getRow(db, authorQuery, thread.id)) + thread.author = selectThreadAuthor(thread.id) return thread @@ -1220,6 +1224,29 @@ proc executeUnlike(c: TForumData, postId: int) = # Delete the like. exec(db, crud(crDelete, "like"), likeId) +proc executeDeletePost(c: TForumData, postId: int) = + # Verify that this post belongs to the user. + const postQuery = sql""" + select p.id from post p + where p.author = ? and p.id = ? + """ + let id = getValue(db, postQuery, postId, c.username) + + if id.len == 0 and c.rank < Admin: + raise newForumError("You cannot delete this post") + + # Set the `isDeleted` flag. + exec(db, crud(crUpdate, "post", "isDeleted"), "1", postId) + +proc executeDeleteThread(c: TForumData, threadId: int) = + # Verify that this thread belongs to the user. + let author = selectThreadAuthor(threadId) + if author.name != c.username and c.rank < Admin: + raise newForumError("You cannot delete this thread") + + # Set the `isDeleted` flag. + exec(db, crud(crUpdate, "thread", "isDeleted"), "1", threadId) + initialise() routes: @@ -1645,6 +1672,33 @@ routes: except ForumError as exc: resp Http400, $(%exc.data), "application/json" + post re"/delete(Post|Thread)": + createTFD() + if not c.loggedIn(): + let err = PostError( + errorFields: @[], + message: "Not logged in." + ) + resp Http401, $(%err), "application/json" + + let formData = request.formData + cond "id" in formData + + let id = getInt(formData["id"].body, -1) + cond id != -1 + + try: + case request.path + of "/deletePost": + executeDeletePost(c, id) + of "/deleteThread": + executeDeleteThread(c, id) + else: + assert false + resp Http200, "{}", "application/json" + except ForumError as exc: + resp Http400, $(%exc.data), "application/json" + get "/t/@id": cond "id" in request.params diff --git a/frontend/delete.nim b/frontend/delete.nim new file mode 100644 index 0000000..789aebc --- /dev/null +++ b/frontend/delete.nim @@ -0,0 +1,132 @@ +when defined(js): + import sugar, httpcore, options, json + import dom except Event + + include karax/prelude + import karax / [kajax, kdom] + + import error, post, threadlist, user + import karaxutils + + type + DeleteKind* = enum + DeleteUser, DeletePost, DeleteThread + + DeleteModal* = ref object + shown: bool + loading: bool + onDeletePost: proc (post: Post) + onDeleteThread: proc (thread: Thread) + onDeleteUser: proc (user: User) + error: Option[PostError] + case kind: DeleteKind + of DeleteUser: + user: User + of DeletePost: + post: Post + of DeleteThread: + thread: Thread + + proc onDeletePost(httpStatus: int, response: kstring, state: DeleteModal) = + postFinished: + state.shown = false + case state.kind + of DeleteUser: + state.onDeleteUser(state.user) + of DeletePost: + state.onDeletePost(state.post) + of DeleteThread: + state.onDeleteThread(state.thread) + + proc onDelete(ev: Event, n: VNode, state: DeleteModal) = + state.loading = true + state.error = none[PostError]() + + let uri = + case state.kind + of DeleteUser: + makeUri("/deleteUser") + of DeleteThread: + makeUri("/deleteThread") + of DeletePost: + makeUri("/deletePost") + # TODO: This is a hack, karax should support this. + let formData = newFormData() + case state.kind + of DeleteUser: + formData.append("username", state.user.name) + of DeletePost: + formData.append("id", $state.post.id) + of DeleteThread: + formData.append("id", $state.thread.id) + ajaxPost(uri, @[], cast[cstring](formData), + (s: int, r: kstring) => onDeletePost(s, r, state)) + + proc onClose(ev: Event, n: VNode, state: DeleteModal) = + state.shown = false + ev.preventDefault() + + proc newDeleteModal*( + onDeletePost: proc (post: Post), + onDeleteThread: proc (thread: Thread), + onDeleteUser: proc (user: User), + ): DeleteModal = + DeleteModal( + shown: false, + onDeletePost: onDeletePost, + onDeleteThread: onDeleteThread, + onDeleteUser: onDeleteUser, + ) + + proc show*(state: DeleteModal, thing: User | Post | Thread) = + state.shown = true + state.error = none[PostError]() + when thing is User: + state.kind = DeleteUser + state.user = thing + when thing is Post: + state.kind = DeletePost + state.post = thing + when thing is Thread: + state.kind = DeleteThread + state.thread = thing + + proc render*(state: DeleteModal): VNode = + result = buildHtml(): + tdiv(class=class({"active": state.shown}, "modal modal-sm"), + id="login-modal"): + a(href="", class="modal-overlay", "aria-label"="close", + onClick=(ev: Event, n: VNode) => onClose(ev, n, state)) + tdiv(class="modal-container"): + tdiv(class="modal-header"): + a(href="", class="btn btn-clear float-right", + "aria-label"="close", + onClick=(ev: Event, n: VNode) => onClose(ev, n, state)) + tdiv(class="modal-title h5"): + text "Delete" + tdiv(class="modal-body"): + tdiv(class="content"): + p(): + text "Are you sure you want to delete this " + case state.kind + of DeleteUser: + text "user account?" + of DeleteThread: + text "thread?" + of DeletePost: + text "post?" + tdiv(class="modal-footer"): + if state.error.isSome(): + p(class="text-error"): + text state.error.get().message + + button(class=class( + {"loading": state.loading}, + "btn btn-primary" + ), + onClick=(ev: Event, n: VNode) => onDelete(ev, n, state)): + italic(class="fas fa-trash-alt") + text " Delete" + button(class="btn", + onClick=(ev: Event, n: VNode) => (state.shown = false)): + text "Cancel" \ No newline at end of file diff --git a/frontend/postlist.nim b/frontend/postlist.nim index 13c91d3..68f2e85 100644 --- a/frontend/postlist.nim +++ b/frontend/postlist.nim @@ -1,5 +1,6 @@ import options, json, times, httpcore, strformat, sugar, math, strutils +import sequtils import threadlist, category, post, user type @@ -17,7 +18,7 @@ when defined(js): include karax/prelude import karax / [vstyles, kajax, kdom] - import karaxutils, error, replybox, editbox, postbutton + import karaxutils, error, replybox, editbox, postbutton, delete type State = ref object @@ -29,10 +30,13 @@ when defined(js): editing: Option[Post] ## If in edit mode, this contains the post. editBox: EditBox likeButton: LikeButton + deleteModal: DeleteModal proc onReplyPosted(id: int) proc onEditPosted(id: int, content: string, subject: Option[string]) proc onEditCancelled() + proc onDeletePost(post: Post) + proc onDeleteThread(thread: Thread) proc newState(): State = State( list: none[PostList](), @@ -41,7 +45,8 @@ when defined(js): replyingTo: none[Post](), replyBox: newReplyBox(onReplyPosted), editBox: newEditBox(onEditPosted, onEditCancelled), - likeButton: newLikeButton() + likeButton: newLikeButton(), + deleteModal: newDeleteModal(onDeletePost, onDeleteThread, nil) ) var @@ -137,6 +142,21 @@ when defined(js): # TODO: Ensure the edit box is as big as its content. Auto resize the # text area. + proc onDeletePost(post: Post) = + state.list.get().posts.keepIf( + x => x.id != post.id + ) + + proc onDeleteThread(thread: Thread) = + window.location.href = makeUri("/") + + proc onDeleteClick(e: Event, n: VNode, p: Post) = + let list = state.list.get() + if list.posts[0].id == p.id: + state.deleteModal.show(list.thread) + else: + state.deleteModal.show(p) + proc onLoadMore(ev: Event, n: VNode, start: int, post: Post) = loadMore(start, post.moreBefore) # TODO: Don't load all! @@ -173,7 +193,8 @@ when defined(js): onEditClick(e, n, post)): button(class="btn"): italic(class="far fa-edit") - tdiv(class="delete-button"): + tdiv(class="delete-button", + onClick=(e: Event, n: VNode) => onDeleteClick(e, n, post)): button(class="btn"): italic(class="far fa-trash-alt") @@ -338,4 +359,6 @@ when defined(js): italic(class="fas fa-reply") text " Reply" - render(state.replyBox, list.thread, state.replyingTo, false) \ No newline at end of file + render(state.replyBox, list.thread, state.replyingTo, false) + + render(state.deleteModal) \ No newline at end of file From 8518c70a661077350509421d131d0679977d9e82 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 18 May 2018 20:48:01 +0100 Subject: [PATCH 216/451] Rearranges directories and files. --- .gitmodules | 3 ++ editdb.nim | 19 ----------- nimforum.nimble | 33 ++++++++++++++++---- {frontend => public/css}/nimforum.scss | 0 {frontend => public/css}/spectre | 0 {frontend => public/css}/syntax.scss | 0 {frontend => public}/karax.html | 0 {static => public}/license.rst | 0 {static => public}/rst.rst | 0 {static => public}/search-help.rst | 0 auth.nim => src/auth.nim | 0 forms.tmpl => src/forms.tmpl | 0 forum.nim => src/forum.nim | 20 +++++++----- forum.nim.cfg => src/forum.nim.cfg | 0 {frontend => src/frontend}/builder.nim | 0 {frontend => src/frontend}/category.nim | 0 {frontend => src/frontend}/delete.nim | 0 {frontend => src/frontend}/editbox.nim | 0 {frontend => src/frontend}/error.nim | 0 {frontend => src/frontend}/forum.nim | 0 {frontend => src/frontend}/forum.nim.cfg | 0 {frontend => src/frontend}/header.nim | 0 {frontend => src/frontend}/index.html | 0 {frontend => src/frontend}/karaxutils.nim | 0 {frontend => src/frontend}/login.nim | 0 {frontend => src/frontend}/newthread.nim | 0 {frontend => src/frontend}/post.nim | 0 {frontend => src/frontend}/postbutton.nim | 0 {frontend => src/frontend}/postlist.nim | 0 {frontend => src/frontend}/profile.nim | 0 {frontend => src/frontend}/replybox.nim | 0 {frontend => src/frontend}/signup.nim | 0 {frontend => src/frontend}/thread.html | 0 {frontend => src/frontend}/threadlist.nim | 0 {frontend => src/frontend}/user.nim | 0 {frontend => src/frontend}/usermenu.nim | 0 fts.sql => src/fts.sql | 0 main.tmpl => src/main.tmpl | 0 setup_nimforum.nim => src/setup_nimforum.nim | 0 utils.nim => src/utils.nim | 0 40 files changed, 43 insertions(+), 32 deletions(-) delete mode 100644 editdb.nim rename {frontend => public/css}/nimforum.scss (100%) rename {frontend => public/css}/spectre (100%) rename {frontend => public/css}/syntax.scss (100%) rename {frontend => public}/karax.html (100%) rename {static => public}/license.rst (100%) rename {static => public}/rst.rst (100%) rename {static => public}/search-help.rst (100%) rename auth.nim => src/auth.nim (100%) rename forms.tmpl => src/forms.tmpl (100%) rename forum.nim => src/forum.nim (99%) rename forum.nim.cfg => src/forum.nim.cfg (100%) rename {frontend => src/frontend}/builder.nim (100%) rename {frontend => src/frontend}/category.nim (100%) rename {frontend => src/frontend}/delete.nim (100%) rename {frontend => src/frontend}/editbox.nim (100%) rename {frontend => src/frontend}/error.nim (100%) rename {frontend => src/frontend}/forum.nim (100%) rename {frontend => src/frontend}/forum.nim.cfg (100%) rename {frontend => src/frontend}/header.nim (100%) rename {frontend => src/frontend}/index.html (100%) rename {frontend => src/frontend}/karaxutils.nim (100%) rename {frontend => src/frontend}/login.nim (100%) rename {frontend => src/frontend}/newthread.nim (100%) rename {frontend => src/frontend}/post.nim (100%) rename {frontend => src/frontend}/postbutton.nim (100%) rename {frontend => src/frontend}/postlist.nim (100%) rename {frontend => src/frontend}/profile.nim (100%) rename {frontend => src/frontend}/replybox.nim (100%) rename {frontend => src/frontend}/signup.nim (100%) rename {frontend => src/frontend}/thread.html (100%) rename {frontend => src/frontend}/threadlist.nim (100%) rename {frontend => src/frontend}/user.nim (100%) rename {frontend => src/frontend}/usermenu.nim (100%) rename fts.sql => src/fts.sql (100%) rename main.tmpl => src/main.tmpl (100%) rename setup_nimforum.nim => src/setup_nimforum.nim (100%) rename utils.nim => src/utils.nim (100%) diff --git a/.gitmodules b/.gitmodules index 78fdace..6ea9ea9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "frontend/spectre"] path = frontend/spectre url = https://github.com/picturepan2/spectre +[submodule "public/css/spectre"] + path = public/css/spectre + url = https://github.com/picturepan2/spectre diff --git a/editdb.nim b/editdb.nim deleted file mode 100644 index 7588822..0000000 --- a/editdb.nim +++ /dev/null @@ -1,19 +0,0 @@ - -import strutils, db_sqlite, ranks - -var db = open(connection="nimforum.db", user="postgres", password="", - database="nimforum") - -when false: - db.exec(sql("update person set status = ?"), $User) - db.exec(sql("update person set status = ? where ban <> ''"), $Troll) - db.exec(sql("update person set status = ? where ban like '%spam%'"), $Spammer) - db.exec(sql("update person set status = ? where ban = 'DEACTIVATED' or ban = 'EMAILCONFIRMATION'"), $EmailUnconfirmed) - db.exec(sql("update person set status = ? where admin = 'true'"), $Admin) -else: - db.exec sql"create index PersonStatusIdx on person(status);" - db.exec sql"create index PostByAuthorIdx on post(thread, author);" - db.exec sql"update person set name = 'cheatfate' where name = 'ka';" - - -close(db) diff --git a/nimforum.nimble b/nimforum.nimble index b6943e0..a0c17e7 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -1,11 +1,32 @@ -[Package] -name = "nimforum" +# Package version = "0.1.0" author = "Dominik Picheta" -description = "Nim forum" +description = "The Nim forum" license = "MIT" -bin = "forum" +srcDir = "src" -[Deps] -Requires: "nim >= 0.14.0, jester#head, bcrypt#head, recaptcha >= 1.0.0" +bin = @["forum"] + +skipExt = @["nim"] + +# Dependencies + +requires "nim >= 0.14.0" +requires "jester#head" +requires "bcrypt#head" +requires "recaptcha >= 1.0.0" +requires "sass" + +requires "karax" + +# Tasks + +task backend, "Runs the forum backend": + exec "nimble c src/forum.nim" + exec "./src/forum" + +task frontend, "Builds the necessary JS frontend": + exec "nimble js src/frontend/forum.nim" + mkDir "public/js" + cpFile "src/frontend/nimcache/forum.js", "public/js/forum.js" \ No newline at end of file diff --git a/frontend/nimforum.scss b/public/css/nimforum.scss similarity index 100% rename from frontend/nimforum.scss rename to public/css/nimforum.scss diff --git a/frontend/spectre b/public/css/spectre similarity index 100% rename from frontend/spectre rename to public/css/spectre diff --git a/frontend/syntax.scss b/public/css/syntax.scss similarity index 100% rename from frontend/syntax.scss rename to public/css/syntax.scss diff --git a/frontend/karax.html b/public/karax.html similarity index 100% rename from frontend/karax.html rename to public/karax.html diff --git a/static/license.rst b/public/license.rst similarity index 100% rename from static/license.rst rename to public/license.rst diff --git a/static/rst.rst b/public/rst.rst similarity index 100% rename from static/rst.rst rename to public/rst.rst diff --git a/static/search-help.rst b/public/search-help.rst similarity index 100% rename from static/search-help.rst rename to public/search-help.rst diff --git a/auth.nim b/src/auth.nim similarity index 100% rename from auth.nim rename to src/auth.nim diff --git a/forms.tmpl b/src/forms.tmpl similarity index 100% rename from forms.tmpl rename to src/forms.tmpl diff --git a/forum.nim b/src/forum.nim similarity index 99% rename from forum.nim rename to src/forum.nim index 4084888..dacceff 100644 --- a/forum.nim +++ b/src/forum.nim @@ -13,6 +13,8 @@ import import cgi except setCookie import options +import sass + import auth import frontend/threadlist except User @@ -866,6 +868,10 @@ proc initialise() = doAssert config.isDev, "Recaptcha required for production!" echo("[WARNING] No recaptcha secret key specified.") + let cssLoc = "public" / "css" + if not existsFile(cssLoc / "nimforum.css"): + sass.compileFile(cssLoc / "nimforum.scss", cssLoc / "nimforum.css") + template createTFD() = var c {.inject.}: TForumData new(c) @@ -1252,11 +1258,11 @@ initialise() routes: get "/nimforum.css": - resp readFile("frontend/nimforum.css"), "text/css" + resp readFile("public/css/nimforum.css"), "text/css" get "/nimcache/forum.js": - resp readFile("frontend/nimcache/forum.js"), "application/javascript" + resp readFile("public/js/forum.js"), "application/javascript" get re"/images/(.+?\.png)/?": - let path = "frontend/images/" & request.matches[0] + let path = "public/images/" & request.matches[0] if fileExists(path): resp readFile(path), "image/png" else: @@ -1724,10 +1730,10 @@ routes: redirect uri("/404") get "/404": - resp Http404, readFile("frontend/karax.html") + resp Http404, readFile("public/karax.html") get re"/(.+)?": - resp readFile("frontend/karax.html") + resp readFile("public/karax.html") get "/threadActivity.xml": createTFD() @@ -1870,10 +1876,10 @@ routes: else: resp genMain(c, genFormResetPassword(c), "Reset Password - Nim Forum") - const licenseRst = slurp("static/license.rst") get "/license": createTFD() - resp genMain(c, rstToHtml(licenseRst), "Content license - Nim Forum") + resp genMain(c, rstToHtml(readFile("static/license.rst")), + "Content license - Nim Forum") post "/search/?@page?": cond isFTSAvailable diff --git a/forum.nim.cfg b/src/forum.nim.cfg similarity index 100% rename from forum.nim.cfg rename to src/forum.nim.cfg diff --git a/frontend/builder.nim b/src/frontend/builder.nim similarity index 100% rename from frontend/builder.nim rename to src/frontend/builder.nim diff --git a/frontend/category.nim b/src/frontend/category.nim similarity index 100% rename from frontend/category.nim rename to src/frontend/category.nim diff --git a/frontend/delete.nim b/src/frontend/delete.nim similarity index 100% rename from frontend/delete.nim rename to src/frontend/delete.nim diff --git a/frontend/editbox.nim b/src/frontend/editbox.nim similarity index 100% rename from frontend/editbox.nim rename to src/frontend/editbox.nim diff --git a/frontend/error.nim b/src/frontend/error.nim similarity index 100% rename from frontend/error.nim rename to src/frontend/error.nim diff --git a/frontend/forum.nim b/src/frontend/forum.nim similarity index 100% rename from frontend/forum.nim rename to src/frontend/forum.nim diff --git a/frontend/forum.nim.cfg b/src/frontend/forum.nim.cfg similarity index 100% rename from frontend/forum.nim.cfg rename to src/frontend/forum.nim.cfg diff --git a/frontend/header.nim b/src/frontend/header.nim similarity index 100% rename from frontend/header.nim rename to src/frontend/header.nim diff --git a/frontend/index.html b/src/frontend/index.html similarity index 100% rename from frontend/index.html rename to src/frontend/index.html diff --git a/frontend/karaxutils.nim b/src/frontend/karaxutils.nim similarity index 100% rename from frontend/karaxutils.nim rename to src/frontend/karaxutils.nim diff --git a/frontend/login.nim b/src/frontend/login.nim similarity index 100% rename from frontend/login.nim rename to src/frontend/login.nim diff --git a/frontend/newthread.nim b/src/frontend/newthread.nim similarity index 100% rename from frontend/newthread.nim rename to src/frontend/newthread.nim diff --git a/frontend/post.nim b/src/frontend/post.nim similarity index 100% rename from frontend/post.nim rename to src/frontend/post.nim diff --git a/frontend/postbutton.nim b/src/frontend/postbutton.nim similarity index 100% rename from frontend/postbutton.nim rename to src/frontend/postbutton.nim diff --git a/frontend/postlist.nim b/src/frontend/postlist.nim similarity index 100% rename from frontend/postlist.nim rename to src/frontend/postlist.nim diff --git a/frontend/profile.nim b/src/frontend/profile.nim similarity index 100% rename from frontend/profile.nim rename to src/frontend/profile.nim diff --git a/frontend/replybox.nim b/src/frontend/replybox.nim similarity index 100% rename from frontend/replybox.nim rename to src/frontend/replybox.nim diff --git a/frontend/signup.nim b/src/frontend/signup.nim similarity index 100% rename from frontend/signup.nim rename to src/frontend/signup.nim diff --git a/frontend/thread.html b/src/frontend/thread.html similarity index 100% rename from frontend/thread.html rename to src/frontend/thread.html diff --git a/frontend/threadlist.nim b/src/frontend/threadlist.nim similarity index 100% rename from frontend/threadlist.nim rename to src/frontend/threadlist.nim diff --git a/frontend/user.nim b/src/frontend/user.nim similarity index 100% rename from frontend/user.nim rename to src/frontend/user.nim diff --git a/frontend/usermenu.nim b/src/frontend/usermenu.nim similarity index 100% rename from frontend/usermenu.nim rename to src/frontend/usermenu.nim diff --git a/fts.sql b/src/fts.sql similarity index 100% rename from fts.sql rename to src/fts.sql diff --git a/main.tmpl b/src/main.tmpl similarity index 100% rename from main.tmpl rename to src/main.tmpl diff --git a/setup_nimforum.nim b/src/setup_nimforum.nim similarity index 100% rename from setup_nimforum.nim rename to src/setup_nimforum.nim diff --git a/utils.nim b/src/utils.nim similarity index 100% rename from utils.nim rename to src/utils.nim From 770b5cddab6fd48b54cc0f5504c15632c080c202 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 18 May 2018 20:53:07 +0100 Subject: [PATCH 217/451] Fix rare threadlist race condition properly. --- src/frontend/threadlist.nim | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/frontend/threadlist.nim b/src/frontend/threadlist.nim index a50b66e..68391ff 100644 --- a/src/frontend/threadlist.nim +++ b/src/frontend/threadlist.nim @@ -163,8 +163,10 @@ when defined(js): if state.status != Http200: return renderError("Couldn't retrieve threads.") - if state.list.isNone and (not state.loading): - ajaxGet(makeUri("threads.json"), @[], onThreadList) + if state.list.isNone: + if not state.loading: + state.loading = true + ajaxGet(makeUri("threads.json"), @[], onThreadList) return buildHtml(tdiv(class="loading loading-lg")) From c6ed9e80c939a4a0e1dd14947ba99989eefa26df Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 18 May 2018 22:33:56 +0100 Subject: [PATCH 218/451] Implements web driver test suite and simple test. --- nimforum.nimble | 24 +++++++++- src/forum.nim | 9 ++-- src/frontend/header.nim | 4 +- src/setup_nimforum.nim | 75 +++++++++++++++++++++++--------- src/utils.nim | 2 + tests/browsertester.nim | 68 +++++++++++++++++++++++++++++ tests/browsertester.nims | 1 + tests/browsertests/scenario1.nim | 29 ++++++++++++ 8 files changed, 183 insertions(+), 29 deletions(-) create mode 100644 tests/browsertester.nim create mode 100644 tests/browsertester.nims create mode 100644 tests/browsertests/scenario1.nim diff --git a/nimforum.nimble b/nimforum.nimble index a0c17e7..7e0555a 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -20,13 +20,33 @@ requires "sass" requires "karax" +requires "webdriver" + # Tasks -task backend, "Runs the forum backend": +task backend, "Compiles and runs the forum backend": exec "nimble c src/forum.nim" exec "./src/forum" +task runbackend, "Runs the forum backend": + exec "./src/forum" + task frontend, "Builds the necessary JS frontend": exec "nimble js src/frontend/forum.nim" mkDir "public/js" - cpFile "src/frontend/nimcache/forum.js", "public/js/forum.js" \ No newline at end of file + cpFile "src/frontend/nimcache/forum.js", "public/js/forum.js" + +task testdb, "Creates a test DB": + exec "nimble c src/setup_nimforum" + exec "./src/setup_nimforum --test" + +task devdb, "Creates a test DB": + exec "nimble c src/setup_nimforum" + exec "./src/setup_nimforum --dev" + +task test, "Runs tester": + exec "nimble c src/forum.nim" + exec "nimble c -r tests/browsertester" + +task fasttest, "Runs tester without recompiling backend": + exec "nimble c -r tests/browsertester" \ No newline at end of file diff --git a/src/forum.nim b/src/forum.nim index dacceff..6c423b0 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -856,10 +856,6 @@ proc prependRe(s: string): string = proc initialise() = randomize() - db = open(connection="nimforum.db", user="postgres", password="", - database="nimforum") - isFTSAvailable = db.getAllRows(sql("SELECT name FROM sqlite_master WHERE " & - "type='table' AND name='post_fts'")).len == 1 config = loadConfig() if len(config.recaptchaSecretKey) > 0 and len(config.recaptchaSiteKey) > 0: @@ -868,6 +864,11 @@ proc initialise() = doAssert config.isDev, "Recaptcha required for production!" echo("[WARNING] No recaptcha secret key specified.") + db = open(connection=config.dbPath, user="", password="", + database="nimforum") + isFTSAvailable = db.getAllRows(sql("SELECT name FROM sqlite_master WHERE " & + "type='table' AND name='post_fts'")).len == 1 + let cssLoc = "public" / "css" if not existsFile(cssLoc / "nimforum.css"): sass.compileFile(cssLoc / "nimforum.scss", cssLoc / "nimforum.css") diff --git a/src/frontend/header.nim b/src/frontend/header.nim index 6d38306..88e49b8 100644 --- a/src/frontend/header.nim +++ b/src/frontend/header.nim @@ -93,11 +93,11 @@ when defined(js): if state.loading: tdiv(class="loading") elif user.isNone: - button(class="btn btn-primary btn-sm", + button(id="signup-btn", class="btn btn-primary btn-sm", onClick=(e: Event, n: VNode) => state.signupModal.show()): italic(class="fas fa-user-plus") text " Sign up" - button(class="btn btn-primary btn-sm", + button(id="login-btn", class="btn btn-primary btn-sm", onClick=(e: Event, n: VNode) => state.loginModal.show()): italic(class="fas fa-sign-in-alt") text " Log in" diff --git a/src/setup_nimforum.nim b/src/setup_nimforum.nim index 22eb38b..2cb51f0 100644 --- a/src/setup_nimforum.nim +++ b/src/setup_nimforum.nim @@ -7,21 +7,30 @@ # # Script to initialise the nimforum. -import strutils, db_sqlite, os, times, json +import strutils, db_sqlite, os, times, json, options import auth, frontend/user -proc backup(path: string) = +proc backup(path: string, contents: Option[string]=none[string]()) = if existsFile(path): + if contents.isSome() and readFile(path) == contents.get(): + # Don't backup if the files are equivalent. + echo("Not backing up because new file is the same.") + return + let backupPath = path & "." & $getTime().toUnix() echo(path, " already exists. Moving to ", backupPath) moveFile(path, backupPath) -proc initialiseDb(admin: tuple[username, password, email: string]) = - let path = getCurrentDir() / "nimforum.db" - backup(path) +proc initialiseDb(admin: tuple[username, password, email: string], + filename="nimforum.db") = + let path = getCurrentDir() / filename + if "-dev" notin filename and "-test" notin filename: + backup(path) - var db = open(connection="nimforum.db", user="", password="", + removeFile(path) + + var db = open(connection=path, user="", password="", database="nimforum") const @@ -198,10 +207,10 @@ proc initialiseConfig( name, hostname: string, recaptcha: tuple[siteKey, secretKey: string], smtp: tuple[address, user, password: string], - isDev: bool + isDev: bool, + dbPath: string ) = let path = getCurrentDir() / "forum.json" - backup(path) var j = %{ "name": %name, @@ -211,23 +220,47 @@ proc initialiseConfig( "smtpAddress": %smtp.address, "smtpUser": %smtp.user, "smtpPassword": %smtp.password, - "isDev": %isDev + "isDev": %isDev, + "dbPath": %dbPath } + backup(path, some($j)) writeFile(path, $j) when isMainModule: - if paramCount() > 0 and paramStr(1) == "--dev": - echo("Initialising nimforum for development...") - initialiseConfig( - "Development Forum", - "localhost.local", - recaptcha=("", ""), - smtp=("", "", ""), - isDev=true - ) + if paramCount() > 0: + case paramStr(1) + of "--dev": + let dbPath = "nimforum-dev.db" + echo("Initialising nimforum for development...") + initialiseConfig( + "Development Forum", + "localhost.local", + recaptcha=("", ""), + smtp=("", "", ""), + isDev=true, + dbPath + ) - initialiseDb( - admin=("admin", "admin", "admin@localhost.local") - ) + initialiseDb( + admin=("admin", "admin", "admin@localhost.local"), + dbPath + ) + of "--test": + let dbPath = "nimforum-test.db" + echo("Initialising nimforum for testing...") + initialiseConfig( + "Test Forum", + "localhost.local", + recaptcha=("", ""), + smtp=("", "", ""), + isDev=true, + dbPath + ) + initialiseDb( + admin=("admin", "admin", "admin@localhost.local"), + dbPath + ) + else: + quit("--dev|--test") diff --git a/src/utils.nim b/src/utils.nim index fea585e..f8cdbbb 100644 --- a/src/utils.nim +++ b/src/utils.nim @@ -25,6 +25,7 @@ type recaptchaSecretKey*: string recaptchaSiteKey*: string isDev*: bool + dbPath*: string var docConfig: StringTableRef @@ -45,6 +46,7 @@ proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config = result.recaptchaSecretKey = root{"recaptchaSecretKey"}.getStr("") result.recaptchaSiteKey = root{"recaptchaSiteKey"}.getStr("") result.isDev = root{"isDev"}.getBool() + result.dbPath = root{"dbPath"}.getStr("nimforum.db") except: echo("[WARNING] Couldn't read config file: ", filename) diff --git a/tests/browsertester.nim b/tests/browsertester.nim new file mode 100644 index 0000000..017003b --- /dev/null +++ b/tests/browsertester.nim @@ -0,0 +1,68 @@ +import options, osproc, streams, threadpool, os, strformat, httpclient + +import webdriver + +proc runProcess(cmd: string) = + let p = startProcess( + cmd, + options={ + poStdErrToStdOut, + poEvalCommand + } + ) + + let o = p.outputStream + while p.running and (not o.atEnd): + echo cmd.substr(0, 10), ": ", o.readLine() + + p.close() + +const backend = "forum" +const port = 5000 +const baseUrl = "http://localhost:" & $port & "/" +template withBackend(body: untyped): untyped = + ## Starts a new backend instance with a fresh DB. + doAssert(execCmd("nimble testdb") == QuitSuccess) + + spawn runProcess("nimble runbackend") + defer: + discard execCmd("killall " & backend) + + echo("Waiting for server...") + var success = false + for i in 0..5: + sleep(5000) + try: + let client = newHttpClient() + doAssert client.getContent(baseUrl).len > 0 + success = true + break + except: + echo("Failed to getContent") + + doAssert success + + body + +import browsertests/scenario1 + +when isMainModule: + spawn runProcess("geckodriver -p 4444 --log config") + defer: + discard execCmd("killall geckodriver") + + doAssert(execCmd("nimble frontend") == QuitSuccess) + echo("Waiting for geckodriver to startup...") + sleep(5000) + + try: + let driver = newWebDriver() + let session = driver.createSession() + + withBackend: + scenario1.test(session, baseUrl) + + session.close() + except: + sleep(10000) # See if we can grab any more output. + raise \ No newline at end of file diff --git a/tests/browsertester.nims b/tests/browsertester.nims new file mode 100644 index 0000000..9d57ecf --- /dev/null +++ b/tests/browsertester.nims @@ -0,0 +1 @@ +--threads:on \ No newline at end of file diff --git a/tests/browsertests/scenario1.nim b/tests/browsertests/scenario1.nim new file mode 100644 index 0000000..491b78e --- /dev/null +++ b/tests/browsertests/scenario1.nim @@ -0,0 +1,29 @@ +import unittest, options, os + +import webdriver + +proc waitForLoad(session: Session) = + sleep(2000) + + while true: + let loading = session.findElement(".loading") + if loading.isNone: return + sleep(1000) + +proc test*(session: Session, baseUrl: string) = + session.navigate(baseUrl) + + waitForLoad(session) + + # Sanity checks + test "shows sign up": + let signUp = session.findElement("#signup-btn") + check signUp.get().getText() == "Sign up" + + test "shows log in": + let signUp = session.findElement("#login-btn") + check signUp.get().getText() == "Log in" + + test "is empty": + let thread = session.findElement("tr > td.thread-title") + check thread.isNone() \ No newline at end of file From e5772b8579d47ab8f62d0b6acec1ed6a53585a0c Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 19 May 2018 13:18:57 +0100 Subject: [PATCH 219/451] Implements login test. --- nimforum.nimble | 2 +- tests/browsertests/scenario1.nim | 47 +++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/nimforum.nimble b/nimforum.nimble index 7e0555a..7f61a77 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -20,7 +20,7 @@ requires "sass" requires "karax" -requires "webdriver" +requires "webdriver#a2be578" # Tasks diff --git a/tests/browsertests/scenario1.nim b/tests/browsertests/scenario1.nim index 491b78e..b710de7 100644 --- a/tests/browsertests/scenario1.nim +++ b/tests/browsertests/scenario1.nim @@ -2,13 +2,18 @@ import unittest, options, os import webdriver -proc waitForLoad(session: Session) = +proc waitForLoad(session: Session, timeout=20000) = + var waitTime = 0 sleep(2000) while true: let loading = session.findElement(".loading") if loading.isNone: return sleep(1000) + waitTime += 1000 + + if waitTime > timeout: + doAssert false, "Wait for load time exceeded" proc test*(session: Session, baseUrl: string) = session.navigate(baseUrl) @@ -21,9 +26,43 @@ proc test*(session: Session, baseUrl: string) = check signUp.get().getText() == "Sign up" test "shows log in": - let signUp = session.findElement("#login-btn") - check signUp.get().getText() == "Log in" + let logIn = session.findElement("#login-btn") + check logIn.get().getText() == "Log in" test "is empty": let thread = session.findElement("tr > td.thread-title") - check thread.isNone() \ No newline at end of file + check thread.isNone() + + # Logging in + test "can login": + let logIn = session.findElement("#login-btn").get() + logIn.click() + + let usernameField = session.findElement( + "#login-form input[name='username']" + ) + check usernameField.isSome() + let passwordField = session.findElement( + "#login-form input[name='password']" + ) + check passwordField.isSome() + + usernameField.get().sendKeys("admin") + passwordField.get().sendKeys("admin") + passwordField.get().click() # Focus field. + session.press(Key.Enter) + + waitForLoad(session, 5000) + + # Verify that the user menu has been initialised properly. + let profileButton = session.findElement( + "#main-navbar figure.avatar" + ).get() + profileButton.click() + + let profileName = session.findElement( + "#main-navbar .menu-right div.tile-content" + ).get() + + check profileName.getText() == "admin" + From e3055920ea7136b3fcc70f41e702e9f7f86d1541 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 19 May 2018 13:23:00 +0100 Subject: [PATCH 220/451] Adds travis.yml file. --- .travis.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5a6e20c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,29 @@ +os: + - linux + +language: c + +cache: + directories: + - "$HOME/.nimble" + - "$HOME/.choosenim" + +addons: + firefox: "60.0.1" + +before_install: + - wget https://github.com/mozilla/geckodriver/releases/download/v0.20.1/geckodriver-v0.20.1-linux64.tar.gz + - mkdir geckodriver + - tar -xzf geckodriver-v0.20.1-linux64.tar.gz -C geckodriver + - export PATH=$PATH:$PWD/geckodriver + +install: + - | + curl https://nim-lang.org/choosenim/init.sh -sSf > init.sh + sh init.sh -y + - export PATH=$HOME/.nimble/bin:$PATH + - nimble refresh -y + +script: + - export MOZ_HEADLESS=1 + - nimble -y test \ No newline at end of file From 8e7142420d4b3a8f30614031625747b709514f5c Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 19 May 2018 13:48:45 +0100 Subject: [PATCH 221/451] Pin the Nim version inside the travis file. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 5a6e20c..ea2ba65 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,7 @@ before_install: - export PATH=$PATH:$PWD/geckodriver install: + - export CHOOSENIM_CHOOSE_VERSION="#afee505a4586" - | curl https://nim-lang.org/choosenim/init.sh -sSf > init.sh sh init.sh -y From dff0e8911561d2ff45cab3a9bc8a6379447aef38 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 19 May 2018 14:05:55 +0100 Subject: [PATCH 222/451] Pin more dependencies. --- nimforum.nimble | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nimforum.nimble b/nimforum.nimble index 7f61a77..01b1285 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -13,12 +13,12 @@ skipExt = @["nim"] # Dependencies requires "nim >= 0.14.0" -requires "jester#head" +requires "jester#d7e2c85a6a72a541dfb" requires "bcrypt#head" -requires "recaptcha >= 1.0.0" +requires "recaptcha 1.0.2" requires "sass" -requires "karax" +requires "https://github.com/dom96/karax#7a884fb" requires "webdriver#a2be578" From 54a7060dbaec6b5c9eeb1308db56f366adb35c1a Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 19 May 2018 15:41:11 +0100 Subject: [PATCH 223/451] Refactors and improves profile settings tab. --- src/frontend/post.nim | 11 ++ src/frontend/postbutton.nim | 1 + src/frontend/profile.nim | 138 ++--------------------- src/frontend/profilesettings.nim | 184 +++++++++++++++++++++++++++++++ 4 files changed, 204 insertions(+), 130 deletions(-) create mode 100644 src/frontend/profilesettings.nim diff --git a/src/frontend/post.nim b/src/frontend/post.nim index 7e46a6b..c32b490 100644 --- a/src/frontend/post.nim +++ b/src/frontend/post.nim @@ -45,6 +45,17 @@ proc isLikedBy*(post: Post, user: Option[User]): bool = return false +type + Profile* = object + user*: User + joinTime*: int64 + threads*: seq[PostLink] + posts*: seq[PostLink] + postCount*: int + threadCount*: int + # Information that only admins should see. + email*: Option[string] + when defined(js): import karaxutils diff --git a/src/frontend/postbutton.nim b/src/frontend/postbutton.nim index 4e146b6..292c136 100644 --- a/src/frontend/postbutton.nim +++ b/src/frontend/postbutton.nim @@ -62,6 +62,7 @@ when defined(js): }, "btn btn-secondary" ), + `type`="button", onClick=(e: Event, n: VNode) => (onClick(e, n, state))): if state.posted: if state.error.isNone(): diff --git a/src/frontend/profile.nim b/src/frontend/profile.nim index 0794c9d..322e0ec 100644 --- a/src/frontend/profile.nim +++ b/src/frontend/profile.nim @@ -1,58 +1,30 @@ -import options, httpcore, json, sugar, times, strformat +import options, httpcore, json, sugar, times, strformat, strutils import threadlist, post, category, error, user -type - Profile* = object - user*: User - joinTime*: int64 - threads*: seq[PostLink] - posts*: seq[PostLink] - postCount*: int - threadCount*: int - # Information that only admins should see. - email*: Option[string] - when defined(js): include karax/prelude - import karax/[kajax] - import karaxutils, postbutton + import karax/[kajax, kdom] + import karaxutils, postbutton, delete, profilesettings type ProfileTab* = enum Overview, Settings - ProfileSettings* = object - email: kstring - rank: Rank - ProfileState* = ref object profile: Option[Profile] - settings: ProfileSettings + settings: Option[ProfileSettings] currentTab: ProfileTab loading: bool status: HttpCode - resetPassword: Option[PostButton] proc newProfileState*(): ProfileState = ProfileState( loading: false, status: Http200, - currentTab: Overview, - settings: ProfileSettings( - email: "", - rank: Spammer - ) + currentTab: Overview ) - proc resetSettings(state: ProfileState) = - let profile = state.profile.get() - if profile.email.isSome(): - state.settings = ProfileSettings( - email: profile.email.get(), - rank: profile.user.rank - ) - proc onProfile(httpStatus: int, response: kstring, state: ProfileState) = # TODO: Try to abstract these. state.loading = false @@ -63,9 +35,7 @@ when defined(js): let profile = to(parsed, Profile) state.profile = some(profile) - resetSettings(state) - if profile.email.isSome(): - state.resetPassword = some(newResetPasswordButton(profile.email.get())) + state.settings = some(newProfileSettings(profile)) proc genPostLink(link: PostLink): VNode = let url = renderPostUrl(link) @@ -81,14 +51,6 @@ when defined(js): p(title=title): text renderActivity(link.creation) - proc onEmailChange(event: Event, node: VNode, state: ProfileState) = - state.settings.email = node.value - - if state.settings.email != state.profile.get().email.get(): - state.settings.rank = EmailUnconfirmed - else: - state.settings.rank = state.profile.get().user.rank - proc render*( state: ProfileState, username: string, @@ -104,37 +66,6 @@ when defined(js): return buildHtml(tdiv(class="loading loading-lg")) let profile = state.profile.get() - let isAdmin = currentUser.isSome() and currentUser.get().rank == Admin - - let rankSelect = buildHtml(tdiv()): - if isAdmin: - select(class="form-select", value = $state.settings.rank): - for r in Rank: - option(text $r) - p(class="form-input-hint text-warning"): - text "As an admin you can modify anyone's rank. Remember: with " & - "great power comes great responsibility." - else: - input(class="form-input", - `type`="text", value = $state.settings.rank, disabled="") - p(class="form-input-hint"): - text "Your rank determines the actions you can perform " & - "on the forum." - case state.settings.rank: - of Spammer, Troll: - p(class="form-input-hint text-warning"): - text "Your account was banned." - of EmailUnconfirmed: - p(class="form-input-hint text-warning"): - text "You cannot post until you confirm your email." - of Moderated: - p(class="form-input-hint text-warning"): - text "Your account is under moderation. This is a spam prevention "& - "measure. You can write posts but only moderators and admins "& - "will see them until your account is verified by them." - else: - discard - result = buildHtml(): section(class="container grid-xl"): tdiv(class="profile"): @@ -205,58 +136,5 @@ when defined(js): for thread in profile.threads: genPostLink(thread) of Settings: - tdiv(class="columns"): - tdiv(class="column col-6"): - form(class="form-horizontal"): - tdiv(class="form-group"): - tdiv(class="col-3 col-sm-12"): - label(class="form-label"): - text "Username" - tdiv(class="col-9 col-sm-12"): - input(class="form-input", - `type`="text", - value=profile.user.name, - disabled="") - p(class="form-input-hint"): - text fmt("Users can refer to you by writing" & - " @{profile.user.name} in their posts.") - tdiv(class="form-group"): - tdiv(class="col-3 col-sm-12"): - label(class="form-label"): - text "Email" - tdiv(class="col-9 col-sm-12"): - input(class="form-input", - `type`="text", value=state.settings.email, - oninput=(e: Event, n: VNode) => - onEmailChange(e, n, state) - ) - p(class="form-input-hint"): - text "Your avatar is linked to this email and can be " & - "changed at " - a(href="https://gravatar.com/emails"): - text "gravatar.com" - text ". Note that any changes to your email will " & - "require email verification." - tdiv(class="form-group"): - tdiv(class="col-3 col-sm-12"): - label(class="form-label"): - text "Rank" - tdiv(class="col-9 col-sm-12"): - rankSelect - if state.resetPassword.isSome(): - tdiv(class="form-group"): - tdiv(class="col-3 col-sm-12"): - label(class="form-label"): - text "Password" - tdiv(class="col-9 col-sm-12"): - render(state.resetPassword.get(), - disabled=state.settings.rank==EmailUnconfirmed) - - tdiv(class="float-right"): - button(class="btn btn-link", - onClick=(e: Event, n: VNode) => (resetSettings(state))): - text "Cancel" - - button(class="btn btn-primary"): - italic(class="fas fa-check") - text " Save" \ No newline at end of file + if state.settings.isSome(): + render(state.settings.get(), currentUser) \ No newline at end of file diff --git a/src/frontend/profilesettings.nim b/src/frontend/profilesettings.nim new file mode 100644 index 0000000..6884df9 --- /dev/null +++ b/src/frontend/profilesettings.nim @@ -0,0 +1,184 @@ +when defined(js): + import httpcore, options, sugar, json, strutils, strformat + + include karax/prelude + import karax/[kajax, kdom] + + import replybox, post, karaxutils, postbutton, error, delete, user + + type + ProfileSettings* = ref object + loading: bool + status: HttpCode + error: Option[PostError] + email: kstring + rank: Rank + deleteModal: DeleteModal + resetPassword: PostButton + profile: Profile + + proc onUserDelete(user: User) = + window.location.href = makeUri("/") + + proc resetSettings(state: ProfileSettings) = + let profile = state.profile + if profile.email.isSome(): + state.email = profile.email.get() + state.rank = profile.user.rank + + proc newProfileSettings*(profile: Profile): ProfileSettings = + result = ProfileSettings( + status: Http200, + deleteModal: newDeleteModal(nil, nil, onUserDelete), + resetPassword: newResetPasswordButton(profile.email.get()), + profile: profile + ) + resetSettings(result) + + proc onProfilePost(httpStatus: int, response: kstring, + state: ProfileSettings) = + postFinished: + discard + + proc onEmailChange(event: Event, node: VNode, state: ProfileSettings) = + state.email = node.value + + if state.profile.user.rank != Admin: + if state.email != state.profile.email.get(): + state.rank = EmailUnconfirmed + else: + state.rank = state.profile.user.rank + + proc onRankChange(event: Event, node: VNode, state: ProfileSettings) = + state.rank = parseEnum[Rank]($node.value) + + proc save(state: ProfileSettings) = + if state.loading: + return + state.loading = true + state.error = none[PostError]() + + let formData = newFormData() + formData.append("email", state.email) + formData.append("rank", $state.rank) + formData.append("username", $state.profile.user.name) + let uri = makeUri("/saveProfile") + ajaxPost(uri, @[], cast[cstring](formData), + (s: int, r: kstring) => onProfilePost(s, r, state)) + + proc render*(state: ProfileSettings, + currentUser: Option[User]): VNode = + if state.status != Http200: + return renderError("Couldn't save profile") + + let isAdmin = currentUser.isSome() and currentUser.get().rank == Admin + let canResetPassword = state.profile.user.rank > EmailUnconfirmed + + let rankSelect = buildHtml(tdiv()): + if isAdmin: + select(id="rank-field", + class="form-select", value = $state.rank, + onchange=(e: Event, n: VNode) => onRankChange(e, n, state)): + for r in Rank: + option(text $r) + p(class="form-input-hint text-warning"): + text "As an admin you can modify anyone's rank. Remember: with " & + "great power comes great responsibility." + else: + input(id="rank-field", class="form-input", + `type`="text", disabled="", value = $state.rank) + p(class="form-input-hint"): + text "Your rank determines the actions you can perform " & + "on the forum." + case state.rank: + of Spammer, Troll: + p(class="form-input-hint text-warning"): + text "Your account was banned." + of EmailUnconfirmed: + p(class="form-input-hint text-warning"): + text "You cannot post until you confirm your email." + of Moderated: + p(class="form-input-hint text-warning"): + text "Your account is under moderation. This is a spam prevention "& + "measure. You can write posts but only moderators and admins "& + "will see them until your account is verified by them." + else: + discard + + result = buildHtml(): + tdiv(class="columns"): + tdiv(class="column col-6"): + form(class="form-horizontal"): + tdiv(class="form-group"): + tdiv(class="col-3 col-sm-12"): + label(class="form-label"): + text "Username" + tdiv(class="col-9 col-sm-12"): + input(class="form-input", + `type`="text", + value=state.profile.user.name, + disabled="") + p(class="form-input-hint"): + text fmt("Users can refer to you by writing" & + " @{state.profile.user.name} in their posts.") + tdiv(class="form-group"): + tdiv(class="col-3 col-sm-12"): + label(class="form-label"): + text "Email" + tdiv(class="col-9 col-sm-12"): + input(id="email-input", class="form-input", + `type`="text", value=state.email, + oninput=(e: Event, n: VNode) => + onEmailChange(e, n, state) + ) + p(class="form-input-hint"): + text "Your avatar is linked to this email and can be " & + "changed at " + a(href="https://gravatar.com/emails"): + text "gravatar.com" + text ". Note that any changes to your email will " & + "require email verification." + tdiv(class="form-group"): + tdiv(class="col-3 col-sm-12"): + label(class="form-label"): + text "Rank" + tdiv(class="col-9 col-sm-12"): + rankSelect + tdiv(class="form-group"): + tdiv(class="col-3 col-sm-12"): + label(class="form-label"): + text "Password" + tdiv(class="col-9 col-sm-12"): + render(state.resetPassword, + disabled=not canResetPassword) + tdiv(class="form-group"): + tdiv(class="col-3 col-sm-12"): + label(class="form-label"): + text "Account" + tdiv(class="col-9 col-sm-12"): + button(class="btn btn-secondary", `type`="button", + onClick=(e: Event, n: VNode) => + (state.deleteModal.show(state.profile.user))): + italic(class="fas fa-times") + text " Delete account" + + tdiv(class="float-right"): + button(class="btn btn-link", + onClick=(e: Event, n: VNode) => (resetSettings(state))): + text "Cancel" + + button(class="btn btn-primary", + onClick=(e: Event, n: VNode) => save(state)): + italic(class="fas fa-check") + text " Save" + + render(state.deleteModal) + + # TODO: I really should just be able to set the `value` attr. + # TODO: This doesn't work when settings are reset for some reason. + let rankField = getVNodeById("rank-field") + if not rankField.isNil: + rankField.setInputText($state.rank) + let emailField = getVNodeById("email-field") + if not emailField.isNil: + emailField.setInputText($state.email) \ No newline at end of file From b4f96c8071f1a60ca6ecd419b6a227492addec6d Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 19 May 2018 15:47:18 +0100 Subject: [PATCH 224/451] Attempt to run on xenial so that we can install libsass. --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index ea2ba65..b258c22 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,5 @@ +dist: xenial + os: - linux @@ -12,6 +14,8 @@ addons: firefox: "60.0.1" before_install: + - sudo apt-get -qq update + - sudo apt-get install -y libsass-dev - wget https://github.com/mozilla/geckodriver/releases/download/v0.20.1/geckodriver-v0.20.1-linux64.tar.gz - mkdir geckodriver - tar -xzf geckodriver-v0.20.1-linux64.tar.gz -C geckodriver From dc72aeb677bcc546f95c90c38c0c5f266136ba8f Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 19 May 2018 17:04:43 +0100 Subject: [PATCH 225/451] Prevent stalls due to no `-y` flag. --- .travis.yml | 2 +- nimforum.nimble | 4 ++-- tests/browsertester.nim | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index b258c22..d76eab8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -dist: xenial +dist: xenial # Needed for libsass-dev :/ os: - linux diff --git a/nimforum.nimble b/nimforum.nimble index 01b1285..a0f9cae 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -45,8 +45,8 @@ task devdb, "Creates a test DB": exec "./src/setup_nimforum --dev" task test, "Runs tester": - exec "nimble c src/forum.nim" - exec "nimble c -r tests/browsertester" + exec "nimble c -y src/forum.nim" + exec "nimble c -y -r tests/browsertester" task fasttest, "Runs tester without recompiling backend": exec "nimble c -r tests/browsertester" \ No newline at end of file diff --git a/tests/browsertester.nim b/tests/browsertester.nim index 017003b..1258bf9 100644 --- a/tests/browsertester.nim +++ b/tests/browsertester.nim @@ -24,7 +24,7 @@ template withBackend(body: untyped): untyped = ## Starts a new backend instance with a fresh DB. doAssert(execCmd("nimble testdb") == QuitSuccess) - spawn runProcess("nimble runbackend") + spawn runProcess("nimble -y runbackend") defer: discard execCmd("killall " & backend) @@ -51,7 +51,7 @@ when isMainModule: defer: discard execCmd("killall geckodriver") - doAssert(execCmd("nimble frontend") == QuitSuccess) + doAssert(execCmd("nimble -y frontend") == QuitSuccess) echo("Waiting for geckodriver to startup...") sleep(5000) From 2ab20bf7a5d8f3e8404acc3837dcb0ce0afd1d05 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 19 May 2018 18:47:03 +0100 Subject: [PATCH 226/451] Implements deleteUser and updateProfile in backend. --- src/forum.nim | 107 +++++++++++++++++++++++++++---- src/frontend/profile.nim | 3 +- src/frontend/profilesettings.nim | 26 ++++++-- 3 files changed, 117 insertions(+), 19 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index 6c423b0..86b95d0 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -1144,6 +1144,19 @@ proc executeLogin(c: TForumData, username, password: string): string = raise newForumError("Invalid username or password") +proc sendEmailActivation(c: TForumData, name, password, + email, salt: string) {.async.} = + let epoch = $int(epochTime()) + let activateUrl = c.req.makeUri("/activateEmail?nick=$1&epoch=$2&ident=$3" % + [encodeUrl(name), encodeUrl(epoch), + encodeUrl(makeIdentHash(name, password, epoch, salt))]) + + let emailSentFut = sendEmailActivation(c.config, email, name, activateUrl) + yield emailSentFut + if emailSentFut.failed: + echo("[WARNING] Couldn't send activation email: ", emailSentFut.error.msg) + raise newForumError("Couldn't send activation email", @["email"]) + proc executeRegister(c: TForumData, name, pass, antibot, userIp, email: string): Future[string] {.async.} = ## Registers a new user and returns a new session key for that user's @@ -1158,7 +1171,7 @@ proc executeRegister(c: TForumData, name, pass, antibot, userIp, raise newForumError("Email already exists", @["email"]) # Username validation: - if name.len == 0 or not allCharsInSet(name, UsernameIdent): + if name.len == 0 or not allCharsInSet(name, UsernameIdent) or name.len > 20: raise newForumError("Invalid username", @["username"]) if getValue(db, sql"select name from person where name = ?", name).len > 0: raise newForumError("Username already exists", @["username"]) @@ -1181,16 +1194,7 @@ proc executeRegister(c: TForumData, name, pass, antibot, userIp, let password = makePassword(pass, salt) # Send activation email. - let epoch = $int(epochTime()) - let activateUrl = c.req.makeUri("/activateEmail?nick=$1&epoch=$2&ident=$3" % - [encodeUrl(name), encodeUrl(epoch), - encodeUrl(makeIdentHash(name, password, epoch, salt))]) - - let emailSentFut = sendEmailActivation(c.config, email, name, activateUrl) - yield emailSentFut - if emailSentFut.failed: - echo("[WARNING] Couldn't send activation email: ", emailSentFut.error.msg) - raise newForumError("Couldn't send activation email", @["email"]) + await sendEmailActivation(c, name, password, email, salt) # Add account to person table exec(db, sql""" @@ -1254,6 +1258,42 @@ proc executeDeleteThread(c: TForumData, threadId: int) = # Set the `isDeleted` flag. exec(db, crud(crUpdate, "thread", "isDeleted"), "1", threadId) +proc executeDeleteUser(c: TForumData, username: string) = + # Verify that the current user has the permissions to do this. + if username != c.username and c.rank < Admin: + raise newForumError("You cannot delete this user.") + + # Set the `isDeleted` flag. + exec(db, sql"update person set isDeleted = 1 where name = ?;", username) + +proc updateProfile( + c: TForumData, username, email: string, rank: Rank +) {.async.} = + if c.rank < rank: + raise newForumError("You cannot set a rank that is higher than yours.") + + if c.username != username and c.rank < Moderator: + raise newForumError("You can't change this profile.") + + # Make sure the rank is set to EmailUnconfirmed when the email changes. + if c.rank < Moderator: + let row = getRow( + db, + sql"select name, password, email, salt from person where name = ?", + username + ) + if row[2] != email: + if rank != EmailUnconfirmed: + raise newForumError("Rank needs a change when setting new email.") + + await sendEmailActivation(c, row[0], row[1], row[2], row[3]) + + exec( + db, + sql"update person set status = ?, email = ? where name = ?;", + $rank, email, username + ) + initialise() routes: @@ -1706,6 +1746,51 @@ routes: except ForumError as exc: resp Http400, $(%exc.data), "application/json" + post re"/deleteUser": + createTFD() + if not c.loggedIn(): + let err = PostError( + errorFields: @[], + message: "Not logged in." + ) + resp Http401, $(%err), "application/json" + + let formData = request.formData + cond "username" in formData + + let username = formData["username"].body + + try: + executeDeleteUser(c, username) + resp Http200, "{}", "application/json" + except ForumError as exc: + resp Http400, $(%exc.data), "application/json" + + post re"/saveProfile": + createTFD() + if not c.loggedIn(): + let err = PostError( + errorFields: @[], + message: "Not logged in." + ) + resp Http401, $(%err), "application/json" + + let formData = request.formData + cond "username" in formData + cond "email" in formData + cond "rank" in formData + + let username = formData["username"].body + let email = formData["email"].body + let rank = parseEnum[Rank](formData["rank"].body) + + try: + await updateProfile(c, username, email, rank) + resp Http200, "{}", "application/json" + except ForumError: + let exc = (ref ForumError)(getCurrentException()) + resp Http400, $(%exc.data), "application/json" + get "/t/@id": cond "id" in request.params diff --git a/src/frontend/profile.nim b/src/frontend/profile.nim index 322e0ec..56e0d43 100644 --- a/src/frontend/profile.nim +++ b/src/frontend/profile.nim @@ -35,7 +35,8 @@ when defined(js): let profile = to(parsed, Profile) state.profile = some(profile) - state.settings = some(newProfileSettings(profile)) + if profile.email.isSome(): + state.settings = some(newProfileSettings(profile)) proc genPostLink(link: PostLink): VNode = let url = renderPostUrl(link) diff --git a/src/frontend/profilesettings.nim b/src/frontend/profilesettings.nim index 6884df9..382d11e 100644 --- a/src/frontend/profilesettings.nim +++ b/src/frontend/profilesettings.nim @@ -26,6 +26,8 @@ when defined(js): state.email = profile.email.get() state.rank = profile.user.rank + state.error = none[PostError]() + proc newProfileSettings*(profile: Profile): ProfileSettings = result = ProfileSettings( status: Http200, @@ -38,7 +40,8 @@ when defined(js): proc onProfilePost(httpStatus: int, response: kstring, state: ProfileSettings) = postFinished: - discard + state.profile.email = some($state.email) + state.profile.user.rank = state.rank proc onEmailChange(event: Event, node: VNode, state: ProfileSettings) = state.email = node.value @@ -66,11 +69,12 @@ when defined(js): ajaxPost(uri, @[], cast[cstring](formData), (s: int, r: kstring) => onProfilePost(s, r, state)) + proc needsSave(state: ProfileSettings): bool = + state.email != state.profile.email.get() or + state.rank != state.profile.user.rank + proc render*(state: ProfileSettings, currentUser: Option[User]): VNode = - if state.status != Http200: - return renderError("Couldn't save profile") - let isAdmin = currentUser.isSome() and currentUser.get().rank == Admin let canResetPassword = state.profile.user.rank > EmailUnconfirmed @@ -163,13 +167,21 @@ when defined(js): text " Delete account" tdiv(class="float-right"): - button(class="btn btn-link", + if state.error.isSome(): + span(class="text-error"): + text state.error.get().message + + button(class=class( + {"disabled": not needsSave(state)}, "btn btn-link" + ), onClick=(e: Event, n: VNode) => (resetSettings(state))): text "Cancel" - button(class="btn btn-primary", + button(class=class( + {"disabled": not needsSave(state)}, "btn btn-primary" + ), onClick=(e: Event, n: VNode) => save(state)): - italic(class="fas fa-check") + italic(class="fas fa-save") text " Save" render(state.deleteModal) From aa9f24e1d752a9598f9959f4648b2871bc0305f5 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 19 May 2018 19:05:34 +0100 Subject: [PATCH 227/451] Switch permissions around. EmailUnconfirmed's are now visible but cannot post. --- src/forum.nim | 14 ++++++++++++++ src/frontend/user.nim | 9 ++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/forum.nim b/src/forum.nim index 86b95d0..24606a1 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -1020,6 +1020,13 @@ proc executeReply(c: TForumData, threadId: int, content: string, # TODO: Refactor TForumData. assert c.loggedIn() + if not canPost(c.rank): + case c.rank + of EmailUnconfirmed: + raise newForumError("You need to confirm your email before you can post") + else: + raise newForumError("You are not allowed to post") + if rateLimitCheck(c): raise newForumError("You're posting too fast!") @@ -1097,6 +1104,13 @@ proc executeNewThread(c: TForumData, subject, msg: string): (int64, int64) = assert c.loggedIn() + if not canPost(c.rank): + case c.rank + of EmailUnconfirmed: + raise newForumError("You need to confirm your email before you can post") + else: + raise newForumError("You are not allowed to post") + if subject.len <= 2: raise newForumError("Subject is too short", @["subject"]) if subject.len > 100: diff --git a/src/frontend/user.nim b/src/frontend/user.nim index 7daa27e..4b2d212 100644 --- a/src/frontend/user.nim +++ b/src/frontend/user.nim @@ -5,9 +5,12 @@ type Spammer ## spammer: every post is invisible Troll ## troll: cannot write new posts Banned ## A non-specific ban - EmailUnconfirmed ## member with unconfirmed email address Moderated ## new member: posts manually reviewed before everybody ## can see them + EmailUnconfirmed ## member with unconfirmed email address. Their posts + ## are visible, but cannot make new posts. This is so that + ## when a user with existing posts changes their email, + ## their posts don't disappear. User ## Ordinary user Moderator ## Moderator: can change a user's rank Admin ## Admin: can do everything @@ -24,6 +27,10 @@ proc isOnline*(user: User): bool = proc `==`*(u1, u2: User): bool = u1.name == u2.name +proc canPost*(rank: Rank): bool = + ## Determines whether the specified rank can make new posts. + rank >= Rank.User or rank == Moderated + when defined(js): include karax/prelude import karaxutils From 2fbebfa3f9b8d4fd25852e7f4b4eba869f120037 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 19 May 2018 19:50:46 +0100 Subject: [PATCH 228/451] Take deleted accounts into account. --- src/forum.nim | 42 +++++++++++++++++++++++++------------ src/frontend/editbox.nim | 2 +- src/frontend/error.nim | 39 ++++++++++++++++++---------------- src/frontend/forum.nim | 4 ++-- src/frontend/postlist.nim | 2 +- src/frontend/profile.nim | 2 +- src/frontend/threadlist.nim | 2 +- src/frontend/user.nim | 1 + 8 files changed, 57 insertions(+), 37 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index 24606a1..4dafeca 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -888,20 +888,26 @@ template createTFD() = #[ DB functions. TODO: Move to another module? ]# proc selectUser(userRow: seq[string], avatarSize: int=80): User = - return User( + result = User( name: userRow[0], avatarUrl: userRow[1].getGravatarUrl(avatarSize), lastOnline: userRow[2].parseInt, - rank: parseEnum[Rank](userRow[3]) + rank: parseEnum[Rank](userRow[3]), + isDeleted: userRow[4] == "1" ) + # Don't give data about a deleted user. + if result.isDeleted: + result.name = "DeletedUser" + result.avatarUrl = getGravatarUrl(result.name & userRow[1], avatarSize) + proc selectPost(postRow: seq[string], skippedPosts: seq[int], replyingTo: Option[PostLink], history: seq[PostInfo], likes: seq[User]): Post = return Post( id: postRow[0].parseInt, replyingTo: replyingTo, - author: selectUser(@[postRow[5], postRow[6], postRow[7], postRow[8]]), + author: selectUser(postRow[5..9]), likes: likes, seen: false, # TODO: history: history, @@ -918,6 +924,7 @@ proc selectReplyingTo(replyingTo: string): Option[PostLink] = const replyingToQuery = sql""" select p.id, strftime('%s', p.creation), p.thread, u.name, u.email, strftime('%s', u.lastOnline), u.status, + u.isDeleted, t.name from post p, person u, thread t where p.thread = t.id and p.author = u.id and p.id = ? and p.isDeleted = 0; @@ -931,7 +938,7 @@ proc selectReplyingTo(replyingTo: string): Option[PostLink] = topic: row[^1], threadId: row[2].parseInt(), postId: row[0].parseInt(), - author: some(selectUser(@[row[3], row[4], row[5], row[6]])) + author: some(selectUser(row[3..7])) )) proc selectHistory(postId: int): seq[PostInfo] = @@ -950,7 +957,8 @@ proc selectHistory(postId: int): seq[PostInfo] = proc selectLikes(postId: int): seq[User] = const likeQuery = sql""" - select u.name, u.email, strftime('%s', u.lastOnline), u.status + select u.name, u.email, strftime('%s', u.lastOnline), u.status, + u.isDeleted from like h, person u where h.post = ? and h.author = u.id order by h.creation asc; @@ -963,7 +971,7 @@ proc selectLikes(postId: int): seq[User] = proc selectThreadAuthor(threadId: int): User = const authorQuery = sql""" - select name, email, strftime('%s', lastOnline), status + select name, email, strftime('%s', lastOnline), status, isDeleted from person where id in ( select author from post where thread = ? @@ -981,7 +989,8 @@ proc selectThread(threadRow: seq[string]): Thread = order by creation asc limit 1;""" const usersListQuery = sql""" - select name, email, strftime('%s', lastOnline), status, count(*) + select name, email, strftime('%s', lastOnline), status, u.isDeleted, + count(*) from person u, post p where p.author = u.id and p.thread = ? group by name order by count(*) desc limit 5; """ @@ -1142,7 +1151,7 @@ proc executeLogin(c: TForumData, username, password: string): string = const query = sql""" select id, name, password, email, salt - from person where name = ? or email = ? + from person where (name = ? or email = ?) and isDeleted = 0 """ if username.len == 0: raise newForumError("Username cannot be empty", @["username"]) @@ -1280,6 +1289,8 @@ proc executeDeleteUser(c: TForumData, username: string) = # Set the `isDeleted` flag. exec(db, sql"update person set isDeleted = 1 where name = ?;", username) + logout(c) + proc updateProfile( c: TForumData, username, email: string, rank: Rank ) {.async.} = @@ -1367,7 +1378,8 @@ routes: sql( """select p.id, p.content, strftime('%s', p.creation), p.author, p.replyingTo, - u.name, u.email, strftime('%s', u.lastOnline), u.status + u.name, u.email, strftime('%s', u.lastOnline), u.status, + u.isDeleted from post p, person u where u.id = p.author and p.thread = ? and p.isDeleted = 0 order by p.id""" @@ -1410,7 +1422,8 @@ routes: let postsQuery = sql(""" select p.id, p.content, strftime('%s', p.creation), p.author, p.replyingTo, - u.name, u.email, strftime('%s', u.lastOnline), u.status + u.name, u.email, strftime('%s', u.lastOnline), u.status, + u.isDeleted from post p, person u where u.id = p.author and p.id in ($#) order by p.id; @@ -1477,10 +1490,10 @@ routes: """ % postsFrom) let userQuery = sql(""" - select name, email, strftime('%s', lastOnline), status, + select name, email, strftime('%s', lastOnline), status, isDeleted, strftime('%s', creation), id from person - where name = ? + where name = ? and isDeleted = 0 """) var profile = Profile( @@ -1491,8 +1504,11 @@ routes: let userRow = db.getRow(userQuery, username) let userID = userRow[^1] + if userID.len == 0: + halt() + profile.user = selectUser(userRow, avatarSize=200) - profile.joinTime = userRow[4].parseInt() + profile.joinTime = userRow[^2].parseInt() profile.postCount = getValue(db, sql("select count(*) " & postsFrom), username).parseInt() profile.threadCount = diff --git a/src/frontend/editbox.nim b/src/frontend/editbox.nim index e6c6c62..c05c03d 100644 --- a/src/frontend/editbox.nim +++ b/src/frontend/editbox.nim @@ -59,7 +59,7 @@ when defined(js): proc render*(state: EditBox, post: Post): VNode = if state.status != Http200: - return renderError("Couldn't retrieve raw post") + return renderError("Couldn't retrieve raw post", state.status) if state.rawContent.isNone() or state.post.id != post.id: state.post = post diff --git a/src/frontend/error.nim b/src/frontend/error.nim index 6a68df4..a70708d 100644 --- a/src/frontend/error.nim +++ b/src/frontend/error.nim @@ -1,4 +1,4 @@ -import options +import options, httpcore type PostError* = object errorFields*: seq[string] ## IDs of the fields with an error. @@ -11,7 +11,25 @@ when defined(js): import karaxutils - proc renderError*(message: string): VNode = + proc render404*(): VNode = + result = buildHtml(): + tdiv(class="empty error"): + tdiv(class="empty icon"): + italic(class="fas fa-bug fa-5x") + p(class="empty-title h5"): + text "404 Not Found" + p(class="empty-subtitle"): + text "Cannot find what you are looking for, it might have been " & + "deleted. Sorry!" + tdiv(class="empty-action"): + a(href="/", onClick=anchorCB): + button(class="btn btn-primary"): + text "Go back home" + + proc renderError*(message: string, status: HttpCode): VNode = + if status == Http404: + return render404() + result = buildHtml(): tdiv(class="empty error"): tdiv(class="empty icon"): @@ -61,19 +79,4 @@ when defined(js): state.error = some(PostError( errorFields: @[], message: "Unknown error occurred." - )) - - proc render404*(): VNode = - result = buildHtml(): - tdiv(class="empty error"): - tdiv(class="empty icon"): - italic(class="fas fa-bug fa-5x") - p(class="empty-title h5"): - text "404 Not Found" - p(class="empty-subtitle"): - text "Cannot find what you are looking for, it might have been " & - "deleted. Sorry!" - tdiv(class="empty-action"): - a(href="/", onClick=anchorCB): - button(class="btn btn-primary"): - text "Go back home" \ No newline at end of file + )) \ No newline at end of file diff --git a/src/frontend/forum.nim b/src/frontend/forum.nim index 1ae644e..031590b 100644 --- a/src/frontend/forum.nim +++ b/src/frontend/forum.nim @@ -1,4 +1,4 @@ -import strformat, times, options, json, tables, sugar +import strformat, times, options, json, tables, sugar, httpcore from dom import window, Location include karax/prelude @@ -46,7 +46,7 @@ proc route(routes: openarray[Route]): VNode = if matched: return route.p(params) - return renderError("Unmatched route: " & path) + return renderError("Unmatched route: " & path, Http500) proc render(): VNode = result = buildHtml(tdiv()): diff --git a/src/frontend/postlist.nim b/src/frontend/postlist.nim index 68f2e85..94be2d2 100644 --- a/src/frontend/postlist.nim +++ b/src/frontend/postlist.nim @@ -308,7 +308,7 @@ when defined(js): proc renderPostList*(threadId: int, postId: Option[int], currentUser: Option[User]): VNode = if state.status != Http200: - return renderError("Couldn't retrieve posts.") + return renderError("Couldn't retrieve posts.", state.status) if state.list.isNone or state.list.get().thread.id != threadId: var params = @[("id", $threadId)] diff --git a/src/frontend/profile.nim b/src/frontend/profile.nim index 56e0d43..e26a56d 100644 --- a/src/frontend/profile.nim +++ b/src/frontend/profile.nim @@ -58,7 +58,7 @@ when defined(js): currentUser: Option[User] ): VNode = if state.status != Http200: - return renderError("Couldn't retrieve profile.") + return renderError("Couldn't retrieve profile.", state.status) if state.profile.isNone or state.profile.get().user.name != username: let uri = makeUri("profile.json", ("username", username)) diff --git a/src/frontend/threadlist.nim b/src/frontend/threadlist.nim index 68391ff..e8b252b 100644 --- a/src/frontend/threadlist.nim +++ b/src/frontend/threadlist.nim @@ -161,7 +161,7 @@ when defined(js): proc genThreadList(currentUser: Option[User]): VNode = if state.status != Http200: - return renderError("Couldn't retrieve threads.") + return renderError("Couldn't retrieve threads.", state.status) if state.list.isNone: if not state.loading: diff --git a/src/frontend/user.nim b/src/frontend/user.nim index 4b2d212..c43288d 100644 --- a/src/frontend/user.nim +++ b/src/frontend/user.nim @@ -20,6 +20,7 @@ type avatarUrl*: string lastOnline*: int64 rank*: Rank + isDeleted*: bool proc isOnline*(user: User): bool = return getTime().toUnix() - user.lastOnline < (60*5) From 0a53392258a9a464ae5122c1f1ea989e8789120d Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 19 May 2018 20:27:29 +0100 Subject: [PATCH 229/451] Reset states properly when navigating to new url. --- src/frontend/editbox.nim | 6 +++++- src/frontend/forum.nim | 22 +++++++++++++++++++--- src/frontend/postlist.nim | 6 +++++- src/frontend/profile.nim | 6 +++++- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/frontend/editbox.nim b/src/frontend/editbox.nim index c05c03d..4b10ccb 100644 --- a/src/frontend/editbox.nim +++ b/src/frontend/editbox.nim @@ -58,10 +58,14 @@ when defined(js): (s: int, r: kstring) => onEditPost(s, r, state)) proc render*(state: EditBox, post: Post): VNode = + if state.post.id != post.id: + state.rawContent = none[kstring]() + state.status = Http200 + if state.status != Http200: return renderError("Couldn't retrieve raw post", state.status) - if state.rawContent.isNone() or state.post.id != post.id: + if state.rawContent.isNone(): state.post = post state.rawContent = none[kstring]() var params = @[("id", $post.id)] diff --git a/src/frontend/forum.nim b/src/frontend/forum.nim index 031590b..5c952cc 100644 --- a/src/frontend/forum.nim +++ b/src/frontend/forum.nim @@ -13,9 +13,22 @@ type profile: ProfileState newThread: NewThread +proc copyLocation(loc: Location): Location = + # TODO: It sucks that I had to do this. We need a nice way to deep copy in JS. + Location( + hash: loc.hash, + host: loc.host, + hostname: loc.hostname, + href: loc.href, + pathname: loc.pathname, + port: loc.port, + protocol: loc.protocol, + search: loc.search + ) + proc newState(): State = State( - url: window.location, + url: copyLocation(window.location), profile: newProfileState(), newThread: newNewThread() ) @@ -25,8 +38,11 @@ proc onPopState(event: dom.Event) = # This event is usually only called when the user moves back in their # history. I fire it in karaxutils.anchorCB as well to ensure the URL is # always updated. This should be moved into Karax in the future. - kout(kstring"New URL: ", window.location.href) - state.url = window.location + kout(kstring"New URL: ", window.location.href, " ", state.url.href) + if state.url.href != window.location.href: + state = newState() # Reload the state to remove stale data. + state.url = copyLocation(window.location) + redraw() type Params = Table[string, string] diff --git a/src/frontend/postlist.nim b/src/frontend/postlist.nim index 94be2d2..f568df0 100644 --- a/src/frontend/postlist.nim +++ b/src/frontend/postlist.nim @@ -307,10 +307,14 @@ when defined(js): proc renderPostList*(threadId: int, postId: Option[int], currentUser: Option[User]): VNode = + if state.list.isSome() and state.list.get().thread.id != threadId: + state.list = none[PostList]() + state.status = Http200 + if state.status != Http200: return renderError("Couldn't retrieve posts.", state.status) - if state.list.isNone or state.list.get().thread.id != threadId: + if state.list.isNone: var params = @[("id", $threadId)] if postId.isSome(): params.add(("anchor", $postId.get())) diff --git a/src/frontend/profile.nim b/src/frontend/profile.nim index e26a56d..5bf24c6 100644 --- a/src/frontend/profile.nim +++ b/src/frontend/profile.nim @@ -57,10 +57,14 @@ when defined(js): username: string, currentUser: Option[User] ): VNode = + if state.profile.isSome() and state.profile.get().user.name != username: + state.profile = none[Profile]() + state.status = Http200 + if state.status != Http200: return renderError("Couldn't retrieve profile.", state.status) - if state.profile.isNone or state.profile.get().user.name != username: + if state.profile.isNone: let uri = makeUri("profile.json", ("username", username)) ajaxGet(uri, @[], (s: int, r: kstring) => onProfile(s, r, state)) From c4431ebf2e69df68400215b929dc5d4c67417849 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 19 May 2018 20:57:45 +0100 Subject: [PATCH 230/451] Fixes editing box regression. --- src/frontend/editbox.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/editbox.nim b/src/frontend/editbox.nim index 4b10ccb..3f8753e 100644 --- a/src/frontend/editbox.nim +++ b/src/frontend/editbox.nim @@ -58,7 +58,7 @@ when defined(js): (s: int, r: kstring) => onEditPost(s, r, state)) proc render*(state: EditBox, post: Post): VNode = - if state.post.id != post.id: + if (not state.post.isNil) and state.post.id != post.id: state.rawContent = none[kstring]() state.status = Http200 From ff70dce9e8ffc68823a2f63a4cd8ec4ed4d3dff3 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 19 May 2018 22:47:42 +0100 Subject: [PATCH 231/451] Attempt to build libsass manually for travis. --- .travis.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index d76eab8..8b3eb96 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,3 @@ -dist: xenial # Needed for libsass-dev :/ - os: - linux @@ -15,7 +13,19 @@ addons: before_install: - sudo apt-get -qq update - - sudo apt-get install -y libsass-dev + - sudo apt-get install autoconf libtool + - git clone -b 3.5.4 https://github.com/sass/libsass.git + - cd libsass + - autoreconf --force --install + - | + ./configure \ + --disable-tests \ + --disable-static \ + --enable-shared \ + --prefix=/usr + - sudo make -j5 install + - cd .. + - wget https://github.com/mozilla/geckodriver/releases/download/v0.20.1/geckodriver-v0.20.1-linux64.tar.gz - mkdir geckodriver - tar -xzf geckodriver-v0.20.1-linux64.tar.gz -C geckodriver From 1592be05037e2f6fba7bb2360a0e18b3992a711b Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 19 May 2018 23:43:10 +0100 Subject: [PATCH 232/451] Implements /about/license --- public/license.rst | 36 +++++++++++++++------------------- src/forum.nim | 14 +++++++++----- src/frontend/about.nim | 44 ++++++++++++++++++++++++++++++++++++++++++ src/frontend/forum.nim | 9 +++++++-- src/setup_nimforum.nim | 4 ++-- src/utils.nim | 27 +++++++++++++------------- 6 files changed, 92 insertions(+), 42 deletions(-) create mode 100644 src/frontend/about.nim diff --git a/public/license.rst b/public/license.rst index 3a15ec1..beebae3 100644 --- a/public/license.rst +++ b/public/license.rst @@ -1,30 +1,30 @@ -Forum content license -===================== +Content license +=============== -All the content contributed to the Nim Forum is `cc-wiki (aka cc-by-sa) +All the content contributed to $hostname is `cc-wiki (aka cc-by-sa) `_ licensed, intended to be -**shared and remixed**. In the future we may even provide all this data as a -convenient data dump. +**shared and remixed**. -But our cc-wiki licensing, while intentionally permissive, does **require -attribution**:: +The cc-wiki licensing, while intentionally permissive, does require +attribution: - **Attribution** — You must attribute the work in the manner specified by - the author or licensor (but not in any way that suggests that they endorse - you or your use of the work). +**Attribution** — You must attribute the work in the manner specified by +the author or licensor (but not in any way that suggests that they endorse +you or your use of the work). -Let us clarify what we mean by attribution. If you republish this content, we -require that you: +This means that if you republish this content, you are +required to: -* **Visually indicate that the content is from the Nim Forum**. It doesn’t +* **Visually indicate that the content is from the $name**. It doesn’t have to be obnoxious; a discreet text blurb is fine. * **Hyperlink directly to the original post** (e.g., - http://forum.nim-lang.org/t/186) + https://$hostname/t/186/1#908) * **Show the author names** for every post. * **Hyperlink each author name** directly back to their user profile page - (e.g., http://forum.nim-lang.org/profile/Araq) + (e.g., http://$hostname/profile/Araq) -By “directly”, we mean each hyperlink must point directly to our domain in +To be more specific, each hyperlink must +point directly to the $hostname domain in standard HTML visible even with JavaScript disabled, and not use a tinyurl or any other form of obfuscation or redirection. Furthermore, the links must not be `nofollowed @@ -36,7 +36,3 @@ time to create that content in the first place! Feel free to remix and reuse to your heart’s content, as long as a good faith effort is made to attribute the content! - -Content previous to the forum license change of -http://forum.nim-lang.org/t/186 remains under the original authors' -copyright, and therefore you cannot reuse it. diff --git a/src/forum.nim b/src/forum.nim index 4dafeca..02f3b34 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -1848,6 +1848,15 @@ routes: get "/404": resp Http404, readFile("public/karax.html") + get "/about/license.html": + let content = readFile("public/license.rst").multiReplace( + { + "$hostname": config.hostname, + "$name": config.name + } + ) + resp content.rstToHtml() + get re"/(.+)?": resp readFile("public/karax.html") @@ -1992,11 +2001,6 @@ routes: else: resp genMain(c, genFormResetPassword(c), "Reset Password - Nim Forum") - get "/license": - createTFD() - resp genMain(c, rstToHtml(readFile("static/license.rst")), - "Content license - Nim Forum") - post "/search/?@page?": cond isFTSAvailable createTFD() diff --git a/src/frontend/about.nim b/src/frontend/about.nim new file mode 100644 index 0000000..51c6f81 --- /dev/null +++ b/src/frontend/about.nim @@ -0,0 +1,44 @@ +when defined(js): + import sugar, httpcore, options, json + import dom except Event + + include karax/prelude + import karax / [kajax, kdom] + + import error, replybox, threadlist, post + import karaxutils + + type + About* = ref object + loading: bool + status: HttpCode + content: kstring + page: string + + proc newAbout*(): About = + About( + status: Http200 + ) + + proc onContent(status: int, response: kstring, state: About) = + state.status = status.HttpCode + state.content = response + + proc render*(state: About, page: string): VNode = + if state.status != Http200: + return renderError($state.content, state.status) + + if page != state.page: + if not state.loading: + state.page = page + state.loading = true + state.status = Http200 + let uri = makeUri("/about/" & page & ".html") + ajaxGet(uri, @[], (s: int, r: kstring) => onContent(s, r, state)) + + return buildHtml(tdiv(class="loading")) + + result = buildHtml(): + section(class="container grid-xl"): + tdiv(id="about"): + verbatim(state.content) \ No newline at end of file diff --git a/src/frontend/forum.nim b/src/frontend/forum.nim index 5c952cc..c8210db 100644 --- a/src/frontend/forum.nim +++ b/src/frontend/forum.nim @@ -4,7 +4,7 @@ from dom import window, Location include karax/prelude import jester/patterns -import threadlist, postlist, header, profile, newthread, error +import threadlist, postlist, header, profile, newthread, error, about import karaxutils type @@ -12,6 +12,7 @@ type url: Location profile: ProfileState newThread: NewThread + about: About proc copyLocation(loc: Location): Location = # TODO: It sucks that I had to do this. We need a nice way to deep copy in JS. @@ -30,7 +31,8 @@ proc newState(): State = State( url: copyLocation(window.location), profile: newProfileState(), - newThread: newNewThread() + newThread: newNewThread(), + about: newAbout() ) var state = newState() @@ -87,6 +89,9 @@ proc render(): VNode = ) ) ), + r("/about/?@page?", + (params: Params) => (render(state.about, params["page"])) + ), r("/404", (params: Params) => render404() ), diff --git a/src/setup_nimforum.nim b/src/setup_nimforum.nim index 2cb51f0..fe27bad 100644 --- a/src/setup_nimforum.nim +++ b/src/setup_nimforum.nim @@ -235,7 +235,7 @@ when isMainModule: echo("Initialising nimforum for development...") initialiseConfig( "Development Forum", - "localhost.local", + "localhost", recaptcha=("", ""), smtp=("", "", ""), isDev=true, @@ -251,7 +251,7 @@ when isMainModule: echo("Initialising nimforum for testing...") initialiseConfig( "Test Forum", - "localhost.local", + "localhost", recaptcha=("", ""), smtp=("", "", ""), isDev=true, diff --git a/src/utils.nim b/src/utils.nim index f8cdbbb..918ed32 100644 --- a/src/utils.nim +++ b/src/utils.nim @@ -26,6 +26,8 @@ type recaptchaSiteKey*: string isDev*: bool dbPath*: string + hostname*: string + name*: string var docConfig: StringTableRef @@ -36,19 +38,18 @@ docConfig["doc.listing_end"] = "

_;vtA}lQoQQ}qP3*`qO(sKh6w*9 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 N002ovPDHLkV1kn-m2Ch3 literal 0 HcmV?d00001 From 17fd398c8c78562139991bd1de83c1c2e4b34f18 Mon Sep 17 00:00:00 2001 From: Hans Raaf Date: Sat, 21 Feb 2015 23:40:11 +0100 Subject: [PATCH 010/451] Updated the config file I updated the config file to use the syntax of the nim.cfg file. --- forum.nim.cfg | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/forum.nim.cfg b/forum.nim.cfg index 9beea07..429dc5b 100644 --- a/forum.nim.cfg +++ b/forum.nim.cfg @@ -1,5 +1,4 @@ # we need the documentation generator of the compiler: ---path:"$nimrod/lib/packages/docutils" - ---path:"$nimrod" +path="$lib/packages/docutils" +path="$nim" From 05a4861212a24e1707e3345873f99526561bbb9f Mon Sep 17 00:00:00 2001 From: lamonte Date: Sun, 22 Feb 2015 12:06:49 -0600 Subject: [PATCH 011/451] Updated css to show which forum links were visited --- public/css/style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/public/css/style.css b/public/css/style.css index b09a2f4..8fe4698 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -272,6 +272,7 @@ pre .EscapeSequence font-weight:bold; white-space: nowrap; } + #talk-threads > div > .topic > div > a:visited { color: #1a1a1a; } #talk-threads > div > .detail > div { float:left; margin:0; } #talk-threads > div > .detail > div > div { margin-left:15px; padding: 5px 5px 5px 22px; } #talk-threads > div > .detail > div { width:50%; } From 4c64cbed1480bd77ea7646c38c54e923ca0ff504 Mon Sep 17 00:00:00 2001 From: Pradeep Gowda Date: Sun, 1 Mar 2015 19:33:41 -0500 Subject: [PATCH 012/451] Remove `text-justify` on `p#content` Justified text is arguably harder to read than left-aligned text. --- public/css/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/css/style.css b/public/css/style.css index b09a2f4..4e7b98a 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -162,7 +162,7 @@ pre .EscapeSequence #content.page { width:680px; min-height:800px; padding-left:20px; } #content h1 { font-size:20pt; letter-spacing:1px; color:rgba(0,0,0,.75); } #content h2 { font-size:16pt; letter-spacing:1px; color:rgba(0,0,0,.7); margin-top:40px; } - #content p { text-align:justify; color: #1D1D1D; margin: 5pt 0pt; } + #content p { color: #1D1D1D; margin: 5pt 0pt; } #content a { color:#CEDAE9; text-decoration:none; } #content a:hover { color:#fff; } #content ul { padding-left:20px; } From b7584de4407499ea30d1c5253ae797dcc5f7bc3a Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 16 Mar 2015 20:01:19 +0000 Subject: [PATCH 013/451] Added a way for admins to reset passwords. --- forum.nim | 152 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 85 insertions(+), 67 deletions(-) diff --git a/forum.nim b/forum.nim index 4aa0338..6a70b51 100644 --- a/forum.nim +++ b/forum.nim @@ -50,7 +50,7 @@ type totalPosts: int search: string noPagenumumNav: bool - + TStyledButton = tuple[text: string, link: string] TForumStats = object @@ -72,23 +72,23 @@ var db: TDbConn docConfig: StringTableRef isFTSAvailable: bool - -proc init(c: var TForumData) = + +proc init(c: var TForumData) = c.userPass = "" c.userName = "" c.threadId = unselectedThread c.postId = -1 - + c.userid = "" c.actionContent = "" c.errorMsg = "" c.loginErrorMsg = "" c.invalidField = "" c.currentPost = (subject: "", content: "") - + c.search = "" -proc loggedIn(c: TForumData): bool = +proc loggedIn(c: TForumData): bool = result = c.userName.len > 0 # --------------- HTML widgets ------------------------------------------------ @@ -98,7 +98,7 @@ proc loggedIn(c: TForumData): bool = const reuseText = "\1" -proc TextWidget(c: TForumData, name, defaultText: string, +proc TextWidget(c: TForumData, name, defaultText: string, maxlength = 30, size = -1): string = let x = if defaultText != reuseText: defaultText else: xmlEncode(c.req.params[name]) @@ -116,8 +116,8 @@ proc TextAreaWidget(c: TForumData, name, defaultText: string): string = return """""" % [ name, x] -proc FieldValid(c: TForumData, name, text: string): string = - if name == c.invalidField: +proc FieldValid(c: TForumData, name, text: string): string = + if name == c.invalidField: result = """$1""" % text else: result = text @@ -146,14 +146,14 @@ proc UrlButton(c: var TForumData, text, url: string): string = proc genButtons(c: var TForumData, btns: seq[TStyledButton]): string = if btns.len == 1: var anchor = "" - + result = ("""
$2""") % [ btns[0].link, btns[0].text, anchor] else: - result = "" + result = "" for i, btn in pairs(btns): var anchor = "" - + var class = "" if i == 0: class = "left " elif i == btns.len()-1: class = "right " @@ -200,7 +200,7 @@ proc getGravatarUrl(email: string, size = 80): string = "&d=identicon") proc genGravatar(email: string, size: int = 80): string = - result = "" % + result = "" % [$size, $size, getGravatarUrl(email, size)] proc randomSalt(): string = @@ -250,17 +250,17 @@ proc makePassword(password, salt: string, comparingTo = ""): string = template `||`(x: expr): expr = (if not isNil(x): x else: "") proc validThreadId(c: TForumData): bool = - result = getValue(db, sql"select id from thread where id = ?", + result = getValue(db, sql"select id from thread where id = ?", $c.threadId).len > 0 - -proc antibot(c: var TForumData): string = + +proc antibot(c: var TForumData): string = let a = math.random(10)+1 let b = math.random(1000)+1 let answer = $(a+b) - + exec(db, sql"delete from antibot where ip = ?", c.req.ip) - let captchaId = tryInsertID(db, - sql"insert into antibot(ip, answer) values (?, ?)", c.req.ip, + let captchaId = tryInsertID(db, + sql"insert into antibot(ip, answer) values (?, ?)", c.req.ip, answer).int mod 10_000 let captchaFile = getCaptchaFilename(captchaId) createCaptcha(captchaFile, $a & "+" & $b) @@ -274,27 +274,27 @@ proc setError(c: var TForumData, field, msg: string): bool {.inline.} = c.errorMsg = "Error: " & msg return false -proc register(c: var TForumData, name, pass, antibot, email: string): bool = +proc register(c: var TForumData, name, pass, antibot, email: string): bool = # Username validation: if name.len == 0 or not allCharsInSet(name, SecureChars): return setError(c, "name", "Invalid username!") if getValue(db, sql"select name from person where name = ?", name).len > 0: return setError(c, "name", "Username already exists!") - + # Password validation: if pass.len < 4: return setError(c, "new_password", "Invalid password!") # antibot validation: - let correctRes = getValue(db, + let correctRes = getValue(db, sql"select answer from antibot where ip = ?", c.req.ip) if antibot != correctRes: return setError(c, "antibot", "You seem to be a bot!") - + # email validation if not validEmailAddress(email): return setError(c, "email", "Invalid email address") - + # perform registration: var salt = makeSalt() exec(db, @@ -304,18 +304,18 @@ proc register(c: var TForumData, name, pass, antibot, email: string): bool = # return setError(c, "", "Could not create your account!") return true -proc checkLoggedIn(c: var TForumData) = +proc checkLoggedIn(c: var TForumData) = let pass = c.req.cookies["sid"] if pass.len == 0: return - if execAffectedRows(db, + if execAffectedRows(db, sql("update session set lastModified = DATETIME('now') " & - "where ip = ? and password = ?"), + "where ip = ? and password = ?"), c.req.ip, pass) > 0: c.userpass = pass - c.userid = getValue(db, - sql"select userid from session where ip = ? and password = ?", + c.userid = getValue(db, + sql"select userid from session where ip = ? and password = ?", c.req.ip, pass) - + let row = getRow(db, sql"select name, email, admin from person where id = ?", c.userid) c.username = ||row[0] @@ -324,7 +324,7 @@ proc checkLoggedIn(c: var TForumData) = # Update lastOnline db.exec(sql"update person set lastOnline = DATETIME('now') where id = ?", c.userid) - + else: echo("SID not found in sessions. Assuming logged out.") @@ -334,7 +334,7 @@ proc logout(c: var TForumData) = c.userpass = "" exec(db, query, c.req.ip, c.req.cookies["sid"]) -proc incrementViews(c: var TForumData) = +proc incrementViews(c: var TForumData) = const query = sql"update thread set views = views + 1 where id = ?" exec(db, query, $c.threadId) @@ -345,7 +345,7 @@ proc isDelete(c: TForumData): bool = result = c.req.params["delete"].len > 0 proc rstToHtml(content: string): string = - result = rstgen.rstToHtml(content, {roSupportSmilies, roSupportMarkdown}, + result = rstgen.rstToHtml(content, {roSupportSmilies, roSupportMarkdown}, docConfig) proc validateRst(c: var TForumData, content: string): bool = @@ -358,10 +358,10 @@ proc validateRst(c: var TForumData, content: string): bool = proc crud(c: TCrud, table: string, data: varargs[string]): TSqlQuery = case c of crCreate: - var fields = "insert into " & table & "(" + var fields = "insert into " & table & "(" var vals = "" for i, d in data: - if i > 0: + if i > 0: fields.add(", ") vals.add(", ") fields.add(d) @@ -403,7 +403,7 @@ template checkLogin(c: expr) = template checkOwnership(c, postId: expr) = if not c.isAdmin: - let x = getValue(db, sql"select author from post where id = ?", + let x = getValue(db, sql"select author from post where id = ?", postId) if x != c.userId: return setError(c, "", "You are not the owner of this post") @@ -421,7 +421,7 @@ template writeToDb(c, cr, setPostId: expr) = c.postId = retID.int proc edit(c: var TForumData, postId: int): bool = - checkLogin(c) + checkLogin(c) if c.isPreview: retrPost(c) setPreviewData(c) @@ -458,19 +458,19 @@ proc edit(c: var TForumData, postId: int): bool = if rows[0][0] == $postId: exec(db, crud(crUpdate, "thread", "name"), subject, $c.threadId) result = true - -proc reply(c: var TForumData): bool = + +proc reply(c: var TForumData): bool = checkLogin(c) retrPost(c) if c.isPreview: setPreviewData(c) else: writeToDb(c, crCreate, true) - + exec(db, sql"update thread set modified = DATETIME('now') where id = ?", $c.threadId) result = true - + proc newThread(c: var TForumData): bool = const query = sql"insert into thread(name, views, modified) values (?, 0, DATETIME('now'))" checkLogin(c) @@ -488,9 +488,9 @@ proc newThread(c: var TForumData): bool = discard tryExec(db, sql"insert into post_fts(thread_fts) values('optimize')") result = true -proc login(c: var TForumData, name, pass: string): bool = +proc login(c: var TForumData, name, pass: string): bool = # get form data: - const query = + const query = sql"select id, name, password, email, salt, admin, ban from person where name = ?" if name.len == 0: return c.setError("name", "Username cannot be nil.") @@ -513,7 +513,7 @@ proc login(c: var TForumData, name, pass: string): bool = if success: # create session: exec(db, - sql"insert into session (ip, password, userid) values (?, ?, ?)", + sql"insert into session (ip, password, userid) values (?, ?, ?)", c.req.ip, c.userpass, c.userid) return true else: @@ -524,6 +524,12 @@ proc setBan(c: var TForumData, nick, reason: string): bool = sql("update person set ban = ? where name = ?") return tryExec(db, query, reason, nick) +proc setPassword(c: var TForumData, nick, pass: string): bool = + const query = + sql("update person set password = ?, salt = ? where name = ?") + var salt = makeSalt() + result = tryExec(db, query, makePassword(pass, salt), salt, nick) + proc hasReplyBtn(c: var TForumData): bool = result = c.req.pathInfo != "/donewthread" and c.req.pathInfo != "/doreply" result = result and c.req.params["action"] != "reply" @@ -543,14 +549,14 @@ proc genActionMenu(c: var TForumData): string = if c.loggedIn: let hasReplyBtn = c.req.pathInfo != "/donewthread" and c.req.pathInfo != "/doreply" if c.threadId >= 0 and hasReplyBtn: - let replyUrl = c.genThreadUrl(action = "reply", + let replyUrl = c.genThreadUrl(action = "reply", pageNum = $(ceil(c.totalPosts / PostsPerPage).int)) & "#reply" btns.add(("Reply", replyUrl)) btns.add(("New Thread", c.req.makeUri("/newthread", false))) result = c.genButtons(btns) proc getStats(c: var TForumData, simple: bool): TForumStats = - const totalUsersQuery = + const totalUsersQuery = sql"select count(*) from person" result.totalUsers = getValue(db, totalUsersQuery).parseInt const totalPostsQuery = @@ -576,13 +582,13 @@ proc getStats(c: var TForumData, simple: bool): TForumStats = proc genPagenumNav(c: var TForumData, stats: TForumStats): string = result = "" - var + var firstUrl = "" prevUrl = "" totalPages = 0 lastUrl = "" nextUrl = "" - + if c.isThreadsList: firstUrl = c.req.makeUri("/") prevUrl = c.req.makeUri(if c.pageNum == 1: "/" else: "/page/" & $(c.pageNum-1)) @@ -593,15 +599,15 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string = firstUrl = c.req.makeUri("/t/" & $c.threadId) if c.pageNum == 1: prevUrl = firstUrl - else: + else: prevUrl = c.req.makeUri(firstUrl & "/" & $(c.pageNum-1)) totalPages = ceil(c.totalPosts / PostsPerPage).int lastUrl = c.req.makeUri(firstUrl & "/" & $(totalPages)) nextUrl = c.req.makeUri(firstUrl & "/" & $(c.pageNum+1)) - + if totalPages <= 1: return "" - + var firstTag = "" var prevTag = "" if c.pageNum == 1: @@ -613,7 +619,7 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string = prevTag.add(htmlgen.link(rel="previous", href=prevUrl)) result.add(firstTag) result.add(prevTag) - + # Numbers var pages = "" # Tags # cutting numbers to the left and to the right tp MaxPagesFromCurrent @@ -629,7 +635,7 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string = pageUrl = c.req.makeUri("/page/" & $(i)) else: pageUrl = c.req.makeUri(firstUrl & "/" & $(i)) - + pages.add(htmlgen.a(href = pageUrl, $(i))) if lastToShow < totalPages: pages.add(span("...")) result.add(pages) @@ -702,7 +708,7 @@ proc gatherUserInfo(c: var TForumData, nick: string, ui: var TUserInfo): bool = const totalThreadsQuery = sql("select count(*) from thread where id in (select thread from post where" & " author = ? and post.id in (select min(id) from post group by thread))") - + ui.threads = getValue(db, totalThreadsQuery, uid).parseInt const lastOnlineQuery = sql"select strftime('%s', lastOnline) from person where id = ?" @@ -729,10 +735,10 @@ proc genProfile(c: var TForumData, ui: TUserInfo): string = ) ) result.add(htmlgen.`div`(id = "avatar", genGravatar(ui.email, 250))) - let t2 = if ui.lastOnline != -1: getGMTime(Time(ui.lastOnline)) + let t2 = if ui.lastOnline != -1: getGMTime(Time(ui.lastOnline)) else: getGMTime(getTime()) - - result.add(htmlgen.`div`(id = "info", + + result.add(htmlgen.`div`(id = "info", htmlgen.table( tr( th("Nickname"), @@ -788,7 +794,7 @@ proc genProfile(c: var TForumData, ui: TUserInfo): string = ) ) )) - + result = htmlgen.`div`(id = "profile", htmlgen.`div`(id = "left", result)) @@ -797,7 +803,7 @@ include "main.tmpl" proc prependRe(s: string): string = result = if s.len == 0: - "" + "" elif s.startswith("Re:"): s else: "Re: " & s @@ -852,7 +858,7 @@ routes: case @"action" of "reply": let subject = getValue(db, - sql"select header from post where id = (select max(id) from post where thread = ?)", + sql"select header from post where id = (select max(id) from post where thread = ?)", $c.threadId).prependRe body = genPostsList(c, $c.threadId, count) cond count != 0 @@ -917,16 +923,16 @@ routes: if (@"postid").len > 0: parseInt(@"postid", c.postId, -1..1000_000) - template finishLogin(): stmt = + template finishLogin(): stmt = setCookie("sid", c.userpass, daysForward(7)) redirect(uri("/")) template handleError(action: string, topText: string, isEdit: bool): stmt = if c.isPreview: - body.add genPostPreview(c, @"subject", @"content", + body.add genPostPreview(c, @"subject", @"content", c.userName, $getGMTime(getTime())) body.add genFormPost(c, action, topText, reuseText, reuseText, isEdit) - resp genMain(c, body(), "Nim Forum - " & + resp genMain(c, body(), "Nim Forum - " & (if c.isPreview: "Preview" else: "Error")) post "/dologin": @@ -1042,6 +1048,18 @@ routes: resp genMain(c, "Failed to change the ban status of user.", "Error - Nim Forum") + get "/setpassword/?": + createTFD() + cond (@"nick" != "") + cond (@"pass" != "") + if not c.isAdmin: + resp genMain(c, "You cannot change this user's pass.", "Error - Nim Forum") + let res = setPassword(c, @"nick", @"pass") + if res: + resp genMain(c, "Success", "Nim Forum") + else: + resp genMain(c, "Failure", "Nim Forum") + const licenseRst = slurp("static/license.rst") get "/license": createTFD() @@ -1078,7 +1096,7 @@ routes: if existsFile(path): page = readFile(path) else: - let basePath = + let basePath = if path[path.high] == '/': path & "index" elif path.endsWith(".html"): path[-5 .. -1] else: path @@ -1095,7 +1113,7 @@ when isMainModule: docConfig = rstgen.defaultConfig() docConfig["doc.smiley_format"] = "/images/smilieys/$1.png" math.randomize() - db = open(connection="nimforum.db", user="postgres", password="", + db = open(connection="nimforum.db", user="postgres", password="", database="nimforum") isFTSAvailable = db.getAllRows(sql("SELECT name FROM sqlite_master WHERE " & "type='table' AND name='post_fts'")).len == 1 @@ -1103,9 +1121,9 @@ when isMainModule: if paramCount() > 0: if paramStr(1) == "scgi": http = false - + #run("", port = TPort(9000), http = http) - + runForever() db.close() From 7e32c8135800adb49ba4c2831989141f141896b1 Mon Sep 17 00:00:00 2001 From: Nycto Date: Thu, 23 Apr 2015 19:47:10 -0700 Subject: [PATCH 014/451] Fix deprecated negative index warning --- forum.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forum.nim b/forum.nim index 6a70b51..3eff43d 100644 --- a/forum.nim +++ b/forum.nim @@ -1098,7 +1098,7 @@ routes: else: let basePath = if path[path.high] == '/': path & "index" - elif path.endsWith(".html"): path[-5 .. -1] + elif path.endsWith(".html"): path[^5 .. ^1] else: path if existsFile(basePath & ".html"): page = readFile(basePath & ".html") From aacd7639a32af5abe3e01e28010f7c2f660c08a0 Mon Sep 17 00:00:00 2001 From: Nycto Date: Thu, 23 Apr 2015 19:58:15 -0700 Subject: [PATCH 015/451] Position glow arrow with CSS --- main.tmpl | 20 +-- public/css/arrow.js | 12 -- public/css/forum.js | 5 - public/css/style.css | 302 ++++++++++++++++++++++--------------------- public/js/arrow.js | 12 -- public/js/forum.js | 5 - 6 files changed, 157 insertions(+), 199 deletions(-) delete mode 100644 public/css/arrow.js delete mode 100644 public/css/forum.js delete mode 100644 public/js/arrow.js delete mode 100644 public/js/forum.js diff --git a/main.tmpl b/main.tmpl index 89e301c..03b28c0 100644 --- a/main.tmpl +++ b/main.tmpl @@ -32,14 +32,7 @@ - - -