From 9beee9848c0be6a7fd7ea890818eca4aa0ebf3e8 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 4 Jun 2012 19:44:55 +0100 Subject: [PATCH 001/560] Implemented paging. --- forms.tmpl | 24 +++--- forum.nim | 186 +++++++++++++++++++++++++++++++++++-------- main.tmpl | 16 +++- nimrod.cfg | 2 +- public/css/style.css | 43 ++++++++++ 5 files changed, 224 insertions(+), 47 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index 039b435..928e497 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -5,13 +5,14 @@ #end template # # -#proc genThreadsList(c: var TForumData): string = -# const query = sql"select id, name, views, modified from thread order by modified desc" +#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 threadId = 0 # const name = 1 # const views = 2 # # result = "" +# count = 0 @@ -20,7 +21,8 @@ -# for row in Rows(db, query): +# for row in Rows(db, query, $((c.pageNum-1) * ThreadsPerPage), $ThreadsPerPage): +# inc(count) #let authorName = getValue(db, sql("select name from person where id = " & @@ -48,6 +50,7 @@ #proc genPostPreview(c: var TForumData, # title, content, author, date: string): string = # result = "" +
TopicsViews Last reply
${UrlButton(c, XMLencode(%name), c.genThreadUrl(threadid = %threadid))}
+ #if useCaptcha: - - + + + #end if
@@ -71,8 +74,9 @@ #end proc # # -#proc genPostsList(c: var TForumData, threadId: string): 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" +#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 ?, ?""" # const postId = 0 # const userName = 1 # const postHeader = 2 @@ -81,7 +85,10 @@ # const postAuthor = 5 # const userEmail = 6 # result = "" -# for row in FastRows(db, query, threadId): +# count = 0 +# for row in FastRows(db, query, threadId, $((c.pageNum-1) * PostsPerPage), $PostsPerPage): +# inc(count) + - + #let authorName = getValue(db, sql("select name from person where id = " & # "(select author from post where id = " & # "(select min(id) from post where thread = ?))"), %threadId) diff --git a/forum.nim b/forum.nim index 9cc83ce..911b65d 100644 --- a/forum.nim +++ b/forum.nim @@ -525,6 +525,44 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string = result = htmlgen.`div`(id = "pagenumbers", result) +proc gatherTotalPostsByID(c: var 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) = + 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 = + c.gatherTotalPosts() # Get total post count + result = ceil(c.totalPosts / postsPerPage).int-1 + +proc getPagesInThreadByID(c: var TForumData, thrid: int): int = + result = ceil(c.gatherTotalPostsByID(thrid) / postsPerPage).int + +proc genPagenumLocalNav(c: var TForumData, thrid: int): string = + result = "" + const maxPostPages = 6 # Maximum links to pages shown. + const hmpp = maxPostPages div 2 + # 1 2 3 ... 10 11 12 + var currentThrURL = "/t/" & $thrid & "/" + let totalPagesInThread = c.getPagesInThreadByID(thrid) + if totalPagesInThread <= 1: return + var i = 1 + while i <= totalPagesInThread: + result.add(a(href=c.req.makeUri(currentThrURL & $i), $i)) + if i == hmpp and totalPagesInThread-i > hmpp: + result.add(span("...")) + # skip to the last 3 + i = totalPagesInThread-(hmpp-1) + else: + inc(i) + + result = htmlgen.`div`(class = "localnums", result) + include "forms.tmpl" include "main.tmpl" @@ -544,13 +582,6 @@ template createTFD(): stmt = if request.cookies.len > 0: checkLoggedIn(c) -proc gatherData(c: var 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 - get "/": createTFD() c.isThreadsList = true @@ -567,7 +598,7 @@ get "/t/@threadid/?@page?/?": parseInt(@"postid", c.postId, -1..1000_000) var count = 0 cond validThreadId(c) - gatherData(c) + gatherTotalPosts(c) if (@"action").len > 0: case @"action" of "reply": @@ -623,10 +654,6 @@ template readIDs(): stmt = if (@"postid").len > 0: parseInt(@"postid", c.postId, -1..1000_000) -proc getTotalPosts(c: var TForumData): int = - c.gatherData() # Get total post count - result = ceil(c.totalPosts / postsPerPage).int-1 - template finishLogin(): stmt = setCookie("sid", c.userpass, daysForward(7)) redirect(uri("/")) @@ -665,11 +692,11 @@ post "/doreply": createTFD() readIDs() if reply(c): - redirect(c.genThreadUrl(pageNum = $(c.getTotalPosts+1)) & "#" & $c.postId) + redirect(c.genThreadUrl(pageNum = $(c.getPagesInThread+1)) & "#" & $c.postId) else: var count = 0 if c.isPreview: - c.pageNum = c.getTotalPosts+1 + c.pageNum = c.getPagesInThread+1 body = genPostsList(c, $c.threadId, count) handleError("doreply", "Reply", false) diff --git a/main.tmpl b/main.tmpl index 9a60908..6d7483d 100644 --- a/main.tmpl +++ b/main.tmpl @@ -50,7 +50,7 @@ #if c.isThreadsList: ${c.genListOnline(stats)} #end if - +
diff --git a/forum.nim b/forum.nim index eda2ed1..b05c56b 100644 --- a/forum.nim +++ b/forum.nim @@ -1,9 +1,9 @@ # # # The Nimrod Forum -# (c) Copyright 2012 Andreas Rumpf -# -# All rights reserved. +# (c) Copyright 2012 Andreas Rumpf, Dominik Picheta +# Look at license.txt for more info. +# All rights reserved. # import @@ -451,6 +451,7 @@ proc getStats(c: var TForumData, simple: bool): TForumStats = if not simple: var newestMemberCreation = 0 result.activeUsers = @[] + result.newestMember = ("", -1, false) const getUsersQuery = sql"select id, name, admin, strftime('%s', lastOnline), strftime('%s', creation) from person" for row in fastRows(db, getUsersQuery): From 5889ede99e7b95f6721f7235bc03d1501fa0326b Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 13 Jan 2013 22:55:53 +0000 Subject: [PATCH 009/560] Added license. --- README.md | 2 ++ license.txt | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 license.txt diff --git a/README.md b/README.md index abacfe4..65cff23 100644 --- a/README.md +++ b/README.md @@ -8,4 +8,6 @@ the Nimrod compiler. Copyright (c) 2012 Andreas Rumpf. All rights reserved. +# License +Nimforum is licensed under the MIT license. diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..a7c088e --- /dev/null +++ b/license.txt @@ -0,0 +1,18 @@ +Copyright (C) 2013 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 +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +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 From d1d9c69489c1254cd4d509df274c781fac64e54d Mon Sep 17 00:00:00 2001 From: Araq Date: Sun, 17 Mar 2013 20:44:42 +0100 Subject: [PATCH 010/560] updated nimrod.cfg --- nimrod.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nimrod.cfg b/nimrod.cfg index 7c05ad9..c4baa08 100644 --- a/nimrod.cfg +++ b/nimrod.cfg @@ -1,6 +1,6 @@ # we need the documentation generator of the compiler: ---path:"$nimrod/packages/docutils" +--path:"$nimrod/lib/packages/docutils" --path:"$nimrod" ---path:"/home/dom/code/nimrod/jester" +--path:"../jester" From 4c2b5d1ed26fbb7ea46611400262088023dcc742 Mon Sep 17 00:00:00 2001 From: Grzegorz Adam Hankiewicz Date: Fri, 22 Mar 2013 22:47:29 +0100 Subject: [PATCH 011/560] Creates a .gitignore to avoid generated files. --- .gitignore | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7c88dfb --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Wildcard patterns. +*.swp +nimcache/ + +# Specific paths +/createdb +/forum +/nimforum.db From 01f480a791215010f593b852c658cd00b864199a Mon Sep 17 00:00:00 2001 From: Grzegorz Adam Hankiewicz Date: Fri, 22 Mar 2013 01:15:07 +0100 Subject: [PATCH 012/560] Adds hyperlinks to profile pages. --- forms.tmpl | 25 ++++++++++++++++++------- main.tmpl | 6 ++++-- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index 67dd402..1ef6ac1 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -31,7 +31,8 @@ #let authorName = getValue(db, sql("select name from person where id = " & # "(select author from post where id = " & # "(select min(id) from post where thread = ?))"), %threadId) - + #let profileUrl = c.req.makeUri("profile/", false) & XMLEncode(authorName) + # let posts = GetValue(db, sql"select count(*) from post where thread = ?", %threadId) @@ -42,7 +43,9 @@ # "(select creation from post where id = (select max(id) from post where thread = ?)))"), %threadId) # end for @@ -101,9 +104,10 @@ -# for row in Rows(db, query, $((c.pageNum-1) * ThreadsPerPage), $ThreadsPerPage): +# for row in rows(db, query, $((c.pageNum-1) * ThreadsPerPage), $ThreadsPerPage): # inc(count) #let authorName = getValue(db, sql("select name from person where id = " & # "(select author from post where id = " & # "(select min(id) from post where thread = ?))"), %threadId) - #let profileUrl = c.req.makeUri("profile/", false) & XMLEncode(authorName) + #let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(authorName) -# let posts = GetValue(db, sql"select count(*) from post where thread = ?", %threadId) +# let posts = getValue(db, sql"select count(*) from post where thread = ?", %threadId) - + #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) @@ -44,7 +44,7 @@ @@ -60,13 +60,13 @@
@@ -123,7 +130,7 @@
${topText}
-
+ ${FieldValid(c, "subject", "Subject:")} ${TextWidget(c, "subject", title, maxlength=100)}
@@ -189,9 +196,8 @@ #end proc # # -#proc genListOnline(c: var TForumData): string = +#proc genListOnline(c: var TForumData, stats: TForumStats): string = # result = "" -# let stats = c.getStats()
Who is online? diff --git a/forum.nim b/forum.nim index 1055b43..f27d160 100644 --- a/forum.nim +++ b/forum.nim @@ -8,12 +8,17 @@ import os, strutils, times, md5, strtabs, cgi, math, db_sqlite, matchers, - rst, rstgen, captchas, sockets, scgi, jester + rst, rstgen, captchas, sockets, scgi, jester, htmlgen const unselectedThread = -1 transientThread = 0 + ThreadsPerPage = 15 + PostsPerPage = 10 + noPageNums = ["/login", "/register", "/dologin", "/doregister"] + noHomeBtn = ["/", "/login", "/register", "/dologin", "/doregister"] + type TCrud = enum crCreate, crRead, crUpdate, crDelete @@ -33,7 +38,10 @@ type invalidField: string currentPost: TPost startTime: float - + isThreadsList: bool + pageNum: int + totalPosts: int + TStyledButton = tuple[text: string, link: string] TForumStats = object @@ -90,8 +98,10 @@ proc FieldValid(c: TForumData, name, text: string): string = else: result = text -proc genThreadUrl(c: TForumData, postId = "", action = "", threadid = ""): string = +proc genThreadUrl(c: TForumData, postId = "", action = "", threadid = "", pageNum = ""): string = result = "/t/" & (if threadid == "": $c.threadId else: threadid) + if pageNum != "": + result.add("/" & pageNum) if action != "": result.add("?action=" & action) if postId != "": @@ -328,9 +338,11 @@ template setPreviewData(c: expr) = c.currentPost.subject = subject c.currentPost.content = content -template writeToDb(c, cr, postId: expr) = - exec(db, crud(cr, "post", "author", "ip", "header", "content", "thread"), - c.userId, c.req.ip, subject, content, $c.threadId, postId) +template writeToDb(c, cr, setPostId: expr) = + let retID = insertID(db, crud(cr, "post", "author", "ip", "header", "content", "thread"), + c.userId, c.req.ip, subject, content, $c.threadId, "") + if setPostId: + c.postId = retID.int proc edit(c: var TForumData, postId: int): bool = checkLogin(c) @@ -360,7 +372,8 @@ proc reply(c: var TForumData): bool = if c.isPreview: setPreviewData(c) else: - writeToDb(c, crCreate, "") + writeToDb(c, crCreate, true) + exec(db, sql"update thread set modified = DATETIME('now') where id = ?", $c.threadId) result = true @@ -375,7 +388,7 @@ proc newThread(c: var TForumData): bool = else: c.threadID = TryInsertID(db, query, c.req.params["subject"]).int if c.threadID < 0: return setError(c, "subject", "Subject already exists") - writeToDb(c, crCreate, "") + writeToDb(c, crCreate, false) result = true proc login(c: var TForumData, name, pass: string): bool = @@ -407,17 +420,18 @@ proc genActionMenu(c: var TForumData): string = result = "" var btns: seq[TStyledButton] = @[] # TODO: Make this detection better? - if c.req.pathInfo notin ["/", "/login", "/register", "/dologin", "/doregister"]: + if c.req.pathInfo.normalizeUri notin noHomeBtn and not c.isThreadsList: btns.add(("Thread List", c.req.makeUri("/", false))) if c.loggedIn: let hasReplyBtn = c.req.pathInfo != "/donewthread" and c.req.pathInfo != "/doreply" if c.threadId >= 0 and hasReplyBtn: - let replyUrl = c.genThreadUrl("", "reply") & "#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): TForumStats = +proc getStats(c: var TForumData, simple: bool): TForumStats = const totalUsersQuery = sql"select count(*) from person" result.totalUsers = getValue(db, totalUsersQuery).parseInt @@ -427,19 +441,89 @@ proc getStats(c: var TForumData): TForumStats = const totalThreadsQuery = sql"select count(*) from thread" result.totalThreads = getValue(db, totalThreadsQuery).parseInt + if not simple: + var newestMemberCreation = 0 + result.activeUsers = @[] + const getUsersQuery = + sql"select id, name, admin, 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() - TTime(secs) + if lastOnlineSeconds < (60 * 5): # 5 minutes + result.activeUsers.add((row[1], row[0].parseInt, row[2].parseBool)) + if row[4].parseInt > newestMemberCreation: + result.newestMember = (row[1], row[0].parseInt, row[2].parseBool) + newestMemberCreation = row[4].parseInt + +proc genPagenumNav(c: var TForumData, stats: TForumStats): string = + result = "" + var + firstUrl = "" + prevUrl = "" + totalPages = 0 + lastUrl = "" + nextUrl = "" - var newestMemberCreation = 0 - result.activeUsers = @[] - const getUsersQuery = - sql"select id, name, admin, 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() - TTime(secs) - if lastOnlineSeconds < (60 * 5): # 5 minutes - result.activeUsers.add((row[1], row[0].parseInt, row[2].parseBool)) - if row[4].parseInt > newestMemberCreation: - result.newestMember = (row[1], row[0].parseInt, row[2].parseBool) - newestMemberCreation = row[4].parseInt + if c.isThreadsList: + firstUrl = c.req.makeUri("/") + prevUrl = c.req.makeUri(if c.pageNum == 1: "/" else: "/page/" & $(c.pageNum-1)) + totalPages = ceil(stats.totalThreads / ThreadsPerPage).int + lastUrl = c.req.makeUri("/page/" & $(totalPages)) + nextUrl = c.req.makeUri("/page/" & $(c.pageNum+1)) + else: + firstUrl = c.req.makeUri("/t/" & $c.threadId) + if c.pageNum == 1: + prevUrl = firstUrl + 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: + firstTag = span("First") + prevTag = span("Prev") + else: + firstTag = a(href=firstUrl, "First") + prevTag = a(href=prevUrl, "Prev") + result.add(htmlgen.`div`(class = "left", + firstTag, + prevTag)) + # Right + var lastTag = "" + var nextTag = "" + if c.pageNum == totalPages: + lastTag = span("Last") + nextTag = span("Next") + else: + lastTag = a(href=lastUrl, "Last") + nextTag = a(href=nextUrl, "Next") + result.add(htmlgen.`div`(class = "right", + nextTag, + lastTag)) + + # Numbers + var pages = "" # Tags + for i in 1..totalPages: + if i == c.pageNum: + pages.add(span($(i))) + else: + var pageUrl = "" + if c.isThreadsList: + pageUrl = c.req.makeUri("/page/" & $(i)) + else: + pageUrl = c.req.makeUri(firstUrl & "/" & $(i)) + + pages.add(a(href = pageUrl, $(i))) + result.add(htmlgen.`div`(class = "middle", + pages)) + + result = htmlgen.`div`(id = "pagenumbers", result) include "forms.tmpl" include "main.tmpl" @@ -455,27 +539,43 @@ template createTFD(): stmt = init(c) c.req = request c.startTime = epochTime() + c.isThreadsList = false + c.pageNum = 1 if request.cookies.len > 0: checkLoggedIn(c) +proc gatherData(c: var 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 + get "/": createTFD() - resp genMain(c, genThreadsList(c), true) + c.isThreadsList = true + var count = 0 + resp genMain(c, genThreadsList(c, count)) -get "/t/@threadid/?": +get "/t/@threadid/?@page?/?": createTFD() + if @"page".len > 0: + parseInt(@"page", c.pageNum, 0..1000_000) + cond (c.pageNum > 0) parseInt(@"threadid", c.threadId, -1..1000_000) if (@"postid").len > 0: parseInt(@"postid", c.postId, -1..1000_000) - + var count = 0 + cond validThreadId(c) + gatherData(c) if (@"action").len > 0: 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) - echo(c.threadId) + body = genPostsList(c, $c.threadId, count) + cond count != 0 body.add genFormPost(c, "doreply", "Reply", subject, "", false) of "edit": cond c.postId != -1 @@ -486,9 +586,22 @@ get "/t/@threadid/?": body = genFormPost(c, "doedit", "Edit", header, content, true) resp c.genMain(body) else: - cond validThreadId(c) incrementViews(c) - resp genMain(c, genPostsList(c, $c.threadId)) + let posts = genPostsList(c, $c.threadId, count) + cond count != 0 + resp genMain(c, posts) + +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) get "/login/?": createTFD() @@ -504,12 +617,16 @@ get "/register/?": resp genMain(c, genFormRegister(c)) template readIDs(): stmt = - # Retrieve the threadid and postid + # 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) +proc getTotalPosts(c: var TForumData): int = + c.gatherData() # Get total post count + result = ceil(c.totalPosts / postsPerPage).int-1 + template finishLogin(): stmt = setCookie("sid", c.userpass, daysForward(7)) redirect(uri("/")) @@ -548,9 +665,12 @@ post "/doreply": createTFD() readIDs() if reply(c): - redirect(c.genThreadUrl()) + redirect(c.genThreadUrl(pageNum = $(c.getTotalPosts+1)) & "#" & $c.postId) else: - body = genPostsList(c, $c.threadId) + var count = 0 + if c.isPreview: + c.pageNum = c.getTotalPosts+1 + body = genPostsList(c, $c.threadId, count) handleError("doreply", "Reply", false) post "/doedit": diff --git a/main.tmpl b/main.tmpl index 964f1a0..9a60908 100644 --- a/main.tmpl +++ b/main.tmpl @@ -1,6 +1,11 @@ #! stdtmpl -#proc genMain(c: var TForumData, content: string, mainPage = false): string = +#proc genMain(c: var TForumData, content: string): string = # result = "" +# var stats: TForumStats +# if c.isThreadsList: stats = c.getStats(false) +# else: +# stats = c.getStats(true) +# end if @@ -35,12 +40,15 @@ $content $c.errorMsg
+ #if c.req.pathInfo.normalizeUri notin noPageNums: + ${c.genPagenumNav(stats)} + #end if
${c.genActionMenu}
- - #if mainPage: - ${c.genListOnline} + + #if c.isThreadsList: + ${c.genListOnline(stats)} #end if
diff --git a/nimrod.cfg b/nimrod.cfg index 3ed332c..7c05ad9 100644 --- a/nimrod.cfg +++ b/nimrod.cfg @@ -3,4 +3,4 @@ --path:"$nimrod/packages/docutils" --path:"$nimrod" ---path:"/home/dominik/code/nimrod/jester" \ No newline at end of file +--path:"/home/dom/code/nimrod/jester" diff --git a/public/css/style.css b/public/css/style.css index 888f8f2..4632566 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -240,6 +240,49 @@ div#replywrapper form { padding: 8pt; } +div#pagenumbers { + font-size: 11pt; + height: 21px; + margin: 5.9pt; + padding: 2pt; + padding-left: 4pt; + padding-right: 4pt; + border-top: 1px solid #9d9d9d; + border-bottom: 1px solid #9d9d9d; + background-color: #eee; +} + +div#pagenumbers div.left { + float: left; +} + +div#pagenumbers div.middle { + text-align: center; +} + +div#pagenumbers div.middle a, div#pagenumbers div.middle span { + padding-right: 4pt; +} + +div#pagenumbers div.middle span { + font-weight: bold; +} + +div#pagenumbers div.left span, div#pagenumbers div.left a { + padding-right: 8pt; +} + +div#pagenumbers div.right span, div#pagenumbers div.right a { + padding-left: 8pt; +} + + +div#pagenumbers div.right { + float: right; +} + + + /* For RST nimrod syntax highlighter */ span.DecNumber {color: blue} span.BinNumber {color: blue} From 0009e627f5fc27f954c21ef238019fc3ba341ad8 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 28 Sep 2012 18:43:21 +0100 Subject: [PATCH 002/560] Compiles with 0.9.0 --- captchas.nim | 4 ++-- forum.nim | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/captchas.nim b/captchas.nim index 0d56022..a78e39c 100644 --- a/captchas.nim +++ b/captchas.nim @@ -15,7 +15,7 @@ proc getCaptchaUrl*(req: var TRequest, i: int): string = result = req.makeUri("/captchas/capture_" & $i & ".png", absolute = false) proc createCaptcha*(file, text: string) = - var surface = imageSurfaceCreate(FORMAT_ARGB32, 10*text.len, 10) + var surface = imageSurfaceCreate(FORMAT_ARGB32, int32(10*text.len), int32(10)) var cr = create(surface) selectFontFace(cr, "serif", FONT_SLANT_NORMAL, FONT_WEIGHT_BOLD) @@ -34,6 +34,6 @@ proc createCaptcha*(file, text: string) = destroy(surface) when isMainModule: - createCapture("test.png", "1+33") + createCaptcha("test.png", "1+33") diff --git a/forum.nim b/forum.nim index f27d160..9cc83ce 100644 --- a/forum.nim +++ b/forum.nim @@ -284,7 +284,7 @@ proc validateRst(c: var TForumData, content: string): bool = except EParseError: result = setError(c, "", getCurrentExceptionMsg()) -proc crud(c: TCrud, table: string, data: openArray[string]): TSqlQuery = +proc crud(c: TCrud, table: string, data: varargs[string]): TSqlQuery = case c of crCreate: var fields = "insert into " & table & "(" @@ -313,11 +313,11 @@ proc crud(c: TCrud, table: string, data: openArray[string]): TSqlQuery = result = sql("delete from " & table & " where id = ?") template retrSubject(c: expr) = - let subject = c.req.params["subject"] + let subject {.inject.} = c.req.params["subject"] if subject.len < 3: return setError(c, "subject", "Subject not long enough") template retrContent(c: expr) = - let content = c.req.params["content"] + let content {.inject.} = c.req.params["content"] if not validateRst(c, content): return false template retrPost(c: expr) = @@ -334,7 +334,7 @@ template checkOwnership(c, postId: expr) = if x != c.userId: return setError(c, "", "You are not the owner of this post") -template setPreviewData(c: expr) = +template setPreviewData(c: expr) {.immediate, dirty.} = c.currentPost.subject = subject c.currentPost.content = content @@ -535,7 +535,7 @@ proc prependRe(s: string): string = else: "Re: " & s template createTFD(): stmt = - var c: TForumData + var c {.inject.}: TForumData init(c) c.req = request c.startTime = epochTime() From d21f9217d5cb8a3af5f168ab79c91fac9d737f1f Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 29 Sep 2012 17:27:03 +0100 Subject: [PATCH 003/560] Fixes footer, it is now properly stuck to the bottom of the page. --- public/css/style.css | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/public/css/style.css b/public/css/style.css index 4632566..41a3872 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -4,6 +4,9 @@ html, body { #wrapper { min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -21pt; /* the bottom margin is the negative value of the footer's height */ } div#header { @@ -156,8 +159,11 @@ div#header a:hover, #nimbtn a:hover { background: -o-linear-gradient(top, #5D5D5D, #4d4d4d); color: #ffffff; + padding-top: 1.5pt; + padding-bottom: 1.5pt; padding-left: 5px; padding-right: 5px; + height: 18pt; } #footer a:link, #footer a:visited { @@ -281,7 +287,27 @@ div#pagenumbers div.right { float: right; } +#content #threads .localnums { + float: right; + border-top: 1px solid #9d9d9d; + border-bottom: 1px solid #9d9d9d; + padding-left: 4pt; + padding-right: 4pt; +} +#content #threads .localnums a:first-child { + padding-left: 0; +} + +#content #threads .localnums a { + padding-left: 6pt; + text-decoration: none; +} + +#content #threads .localnums span { + padding-left: 3pt; + margin-right: -3pt; +} /* For RST nimrod syntax highlighter */ span.DecNumber {color: blue} From 96da91fa3ebd1c6083a2fc65026cf9b9b3f38c04 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 30 Sep 2012 13:19:03 +0100 Subject: [PATCH 004/560] Implements local pager. Fixes footer once and for all. --- forms.tmpl | 5 +++- forum.nim | 55 +++++++++++++++++++++++++++++++++----------- main.tmpl | 2 +- public/css/style.css | 4 ++++ 4 files changed, 50 insertions(+), 16 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index 928e497..0768adf 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -24,7 +24,10 @@ # for row in Rows(db, query, $((c.pageNum-1) * ThreadsPerPage), $ThreadsPerPage): # inc(count)
${UrlButton(c, XMLencode(%name), c.genThreadUrl(threadid = %threadid))} + ${UrlButton(c, XMLencode(%name), c.genThreadUrl(threadid = %threadid))} + ${genPagenumLocalNav(c, (%threadid).parseInt)} + ${authorName}${authorName}$posts ${XMLencode(%views)} ${formatTimestamp(latestReplyDate.parseInt())}
- ${latestReplyAuthor} + #let replyProfileUrl = c.req.makeUri("profile/", false) & + # XMLEncode(latestReplyAuthor) + ${latestReplyAuthor}
- ${XMLencode(%userName)} + #let profileUrl = c.req.makeUri("profile/", false) & XMLencode(%userName) + ${XMLencode(%userName)}
- ${genGravatar(%userEmail)} + ${genGravatar(%userEmail)} #if c.userId == %postAuthor and c.currentPost.subject.len == 0:
${UrlButton(c, "Edit post", c.genThreadUrl(%postId, "edit"))} #elif c.isAdmin and c.currentPost.subject.len == 0: @@ -208,20 +212,27 @@
Out of ${stats.totalUsers} users ${stats.activeUsers.len} are online${if stats.activeUsers.len == 0: "." else: ":"} #for index, usr in stats.activeUsers: + # let profileHref = """""" + # let hrefEnd = """""" # if usr.isAdmin: #if index != 0: result.add ',' #end if - #result.add(""" """ & usr.nick & """""") + #result.add(""" """ & profileHref & + # usr.nick & hrefEnd & """""") # else: #if index != 0: result.add ',' #end if - #result.add(""" """ & usr.nick & """""") + #result.add(""" """ & profileHref & + # usr.nick & hrefEnd & """""") # end if #end for
#if stats.newestMember.nick != "": - Total threads: ${stats.totalThreads} | Total posts: ${stats.totalPosts} | Newest member: ${stats.newestMember.nick} + #let profileUrl = c.req.makeUri("profile/", false) & + # XMLEncode(stats.newestMember.nick) + Total threads: ${stats.totalThreads} | Total posts: ${stats.totalPosts} | Newest member: ${stats.newestMember.nick} #else: Total threads: ${stats.totalThreads} | Total posts: ${stats.totalPosts} #end if diff --git a/main.tmpl b/main.tmpl index 22f6525..368033c 100644 --- a/main.tmpl +++ b/main.tmpl @@ -25,8 +25,10 @@ Nimrod's Forum #if c.loggedIn: Logout - $c.username - ${genGravatar(c.email, 26)} + #let profileUrl = c.req.makeUri("profile/", false) & + # XMLencode(c.username) + $c.username + ${genGravatar(c.email, 26)} #else: Register Login From 9d9772e524e6d5973f0abaf15d5f10f17db117e6 Mon Sep 17 00:00:00 2001 From: Grzegorz Adam Hankiewicz Date: Sun, 24 Mar 2013 21:48:17 +0100 Subject: [PATCH 013/560] Changes forum's header links. The forum link is now the first item, the homepage is now second along with other useful links like documentation and github issues. --- main.tmpl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/main.tmpl b/main.tmpl index 22f6525..7cf7e75 100644 --- a/main.tmpl +++ b/main.tmpl @@ -16,13 +16,16 @@
+ #let frontQuery = c.req.makeUri("/") - +
${c.genActionMenu}
- +
$content $c.errorMsg @@ -60,3 +61,55 @@
+#end proc +# +#proc genRSSHeaders(c: var TForumData): string = +# result = "" + +#end proc +# +#proc genRSS(c: var TForumData): string = +# result = "" +# var stats: TForumStats +# let frontQuery = c.req.makeUri("/") +# if c.isThreadsList: stats = c.getStats(false) +# else: +# stats = c.getStats(true) +# end if +# const query = sql"""SELECT A.id, A.name, +# strftime('%Y-%m-%dT%H:%M:%S', (A.modified)), +# COUNT(B.id), C.name, B.content +# FROM thread AS A, post AS B, person AS C +# WHERE A.id = b.thread AND B.author = C.id +# GROUP BY B.thread +# ORDER BY modified DESC LIMIT ?""" +# const threadId = 0 +# const name = 1 +# const threadDate = 2 +# const postCount = 3 +# const postAuthor = 4 +# const postContent = 5 +# let recent = GetValue(db, sql"""SELECT +# strftime('%Y-%m-%dT%H:%M:%S', (modified)) FROM thread +# ORDER BY modified DESC LIMIT 1""") + + + Nimrod forum thread activity + + + ${frontQuery} + ${recent} +# for row in Rows(db, query, 10): + + ${XMLencode(%name)} + urn:entry:${%threadid} + + ${%threadDate} + Posts ${postCount}, ${XMLEncode(%postAuthor)} said: ${XMLEncode(%postContent)} + +# end for + +#end proc From ddd07883cbe4077729eca3c8e831ee8a811198fb Mon Sep 17 00:00:00 2001 From: Grzegorz Adam Hankiewicz Date: Thu, 28 Mar 2013 00:03:37 +0100 Subject: [PATCH 015/560] Implements hyperlinking to specific post from rss. --- main.tmpl | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/main.tmpl b/main.tmpl index 46f3979..95a8f58 100644 --- a/main.tmpl +++ b/main.tmpl @@ -79,7 +79,7 @@ # end if # const query = sql"""SELECT A.id, A.name, # strftime('%Y-%m-%dT%H:%M:%S', (A.modified)), -# COUNT(B.id), C.name, B.content +# COUNT(B.id), C.name, B.content, B.id # FROM thread AS A, post AS B, person AS C # WHERE A.id = b.thread AND B.author = C.id # GROUP BY B.thread @@ -90,6 +90,7 @@ # const postCount = 3 # const postAuthor = 4 # const postContent = 5 +# const postId = 6 # let recent = GetValue(db, sql"""SELECT # strftime('%Y-%m-%dT%H:%M:%S', (modified)) FROM thread # ORDER BY modified DESC LIMIT 1""") @@ -104,11 +105,14 @@ ${XMLencode(%name)} urn:entry:${%threadid} + # let url = c.genThreadUrl(threadid = %threadid, + # pageNum = $(ceil(parseInt(%postCount) / postsPerPage).int)) & + # "#" & %postId + href="${c.req.makeUri(url)}"/> ${%threadDate} Posts ${postCount}, ${XMLEncode(%postAuthor)} said: ${XMLEncode(%postContent)} +>Posts ${%postCount}, ${XMLEncode(%postAuthor)} said: ${XMLEncode(%postContent)} # end for From 82cc25bd6207339fead175c1cf766329adccee3b Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 27 Mar 2013 23:28:00 +0000 Subject: [PATCH 016/560] Features to improve cooperation with webcrawlers. Added proper titles to each page, page 1 of threads and posts lists now redirects to /. --- README.md | 2 +- forum.nim | 38 +++++++++++++++++++++++++------------- main.tmpl | 4 ++-- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 65cff23..ff09f6a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ This is Nimrod's forum. The code is not nice and depends on the RST parser of the Nimrod compiler. -Copyright (c) 2012 Andreas Rumpf. +Copyright (c) 2012 Andreas Rumpf, Dominik Picheta. All rights reserved. # License diff --git a/forum.nim b/forum.nim index c343f74..5736d30 100644 --- a/forum.nim +++ b/forum.nim @@ -36,7 +36,7 @@ type actionContent: string errorMsg, loginErrorMsg: string invalidField: string - currentPost: TPost + currentPost: TPost ## Only used for reply previews startTime: float isThreadsList: bool pageNum: int @@ -551,6 +551,11 @@ proc getPagesInThread(c: var TForumData): int = proc getPagesInThreadByID(c: var TForumData, thrid: int): int = result = ceil(c.gatherTotalPostsByID(thrid) / postsPerPage).int +proc getThreadTitle(thrid: int, pageNum: int): string = + result = GetValue(db, sql"select name from thread where id = ?", $thrid) + if pageNum notin {0,1}: + result.add(" - Page " & $pageNum) + proc genPagenumLocalNav(c: var TForumData, thrid: int): string = result = "" const maxPostPages = 6 # Maximum links to pages shown. @@ -645,23 +650,26 @@ get "/": createTFD() c.isThreadsList = true var count = 0 - resp genMain(c, genThreadsList(c, count), genRSSHeaders(c)) + resp genMain(c, genThreadsList(c, count), + additionalHeaders = genRSSHeaders(c)) get "/threadActivity.xml": createTFD() c.isThreadsList = true - var count = 0 resp genRSS(c), "application/atom+xml" get "/t/@threadid/?@page?/?": createTFD() + var title = "Nimrod Forum - " + parseInt(@"threadid", c.threadId, -1..1000_000) if @"page".len > 0: parseInt(@"page", c.pageNum, 0..1000_000) cond (c.pageNum > 0) - parseInt(@"threadid", c.threadId, -1..1000_000) + if c.pageNum == 1: redirect(uri("/t/" & $c.threadId)) if (@"postid").len > 0: parseInt(@"postid", c.postId, -1..1000_000) var count = 0 + var pSubject = getThreadTitle(c.threadid, c.pageNum) cond validThreadId(c) gatherTotalPosts(c) if (@"action").len > 0: @@ -673,6 +681,7 @@ get "/t/@threadid/?@page?/?": body = genPostsList(c, $c.threadId, count) cond count != 0 body.add genFormPost(c, "doreply", "Reply", subject, "", false) + title.add("Replying to thread: " & pSubject) of "edit": cond c.postId != -1 const query = sql"select header, content from post where id = ?" @@ -680,12 +689,13 @@ get "/t/@threadid/?@page?/?": let header = ||row[0] let content = ||row[1] body = genFormPost(c, "doedit", "Edit", header, content, true) - resp c.genMain(body) + title.add("Editing post") + resp c.genMain(body, title) else: incrementViews(c) let posts = genPostsList(c, $c.threadId, count) cond count != 0 - resp genMain(c, posts) + resp genMain(c, posts, title & pSubject) get "/page/@page/?": createTFD() @@ -693,25 +703,26 @@ get "/page/@page/?": cond (@"page" != "") parseInt(@"page", c.pageNum, 0..1000_000) cond (c.pageNum > 0) + if c.pageNum == 1: redirect(uri("/")) var count = 0 let list = genThreadsList(c, count) if count == 0: pass() - resp genMain(c, list, genRSSHeaders(c)) + resp genMain(c, list, "Nimrod Forum - Page " & $c.pageNum, genRSSHeaders(c)) get "/profile/@nick/?": createTFD() cond (@"nick" != "") var userinfo: TUserInfo if gatherUserInfo(c, @"nick", userinfo): - resp genMain(c, c.genProfile(userinfo)) + resp genMain(c, c.genProfile(userinfo), + "Nimrod Forum - " & @"nick" & "'s profile") else: halt() - get "/login/?": createTFD() - resp genMain(c, genFormLogin(c)) + resp genMain(c, genFormLogin(c), "Nimrod Forum - Log in") get "/logout/?": createTFD() @@ -720,7 +731,7 @@ get "/logout/?": get "/register/?": createTFD() - resp genMain(c, genFormRegister(c)) + resp genMain(c, genFormRegister(c), "Nimrod Forum - Register") template readIDs(): stmt = # Retrieve the threadid, postid and pagenum @@ -738,7 +749,7 @@ template handleError(action: string, topText: string, isEdit: bool): stmt = body.add genPostPreview(c, @"subject", @"content", c.userName, $getGMTime(getTime())) body.add genFormPost(c, action, topText, reuseText, reuseText, isEdit) - resp genMain(c, body) + resp genMain(c, body, "Nimrod Forum - Error") post "/dologin": createTFD() @@ -786,7 +797,8 @@ post "/doedit": get "/newthread/?": createTFD() - resp genMain(c, genFormPost(c, "donewthread", "New thread", "", "", false)) + resp genMain(c, genFormPost(c, "donewthread", "New thread", "", "", false), + "Nimrod Forum - New Thread") when isMainModule: docConfig = rstgen.defaultConfig() diff --git a/main.tmpl b/main.tmpl index c87b821..d30168e 100644 --- a/main.tmpl +++ b/main.tmpl @@ -1,6 +1,6 @@ #! stdtmpl #proc genMain(c: var TForumData, content: string, -# additional_headers = ""): string = +# title = "Nimrod Forum", additional_headers = ""): string = # result = "" # var stats: TForumStats # if c.isThreadsList: stats = c.getStats(false) @@ -10,7 +10,7 @@ - Nimrod Forum + ${XmlEncode(title)} ${additional_headers} From f9fa9b45033a9f67ee01098b59ec0806c7335f88 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 27 Mar 2013 23:44:57 +0000 Subject: [PATCH 017/560] Reverted back some risky changes. --- README.md | 7 ++++--- forum.nim | 2 -- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ff09f6a..9e89676 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ nimforum ======== -This is Nimrod's forum. The code is not nice and depends on the RST parser of -the Nimrod compiler. +This is Nimrod's forum. The code depends on the RST parser of +the Nimrod compiler and on Jester. -Copyright (c) 2012 Andreas Rumpf, Dominik Picheta. +Copyright (c) 2012-2013 Andreas Rumpf, Dominik Picheta. + All rights reserved. # License diff --git a/forum.nim b/forum.nim index 5736d30..4173eb3 100644 --- a/forum.nim +++ b/forum.nim @@ -665,7 +665,6 @@ get "/t/@threadid/?@page?/?": if @"page".len > 0: parseInt(@"page", c.pageNum, 0..1000_000) cond (c.pageNum > 0) - if c.pageNum == 1: redirect(uri("/t/" & $c.threadId)) if (@"postid").len > 0: parseInt(@"postid", c.postId, -1..1000_000) var count = 0 @@ -703,7 +702,6 @@ get "/page/@page/?": cond (@"page" != "") parseInt(@"page", c.pageNum, 0..1000_000) cond (c.pageNum > 0) - if c.pageNum == 1: redirect(uri("/")) var count = 0 let list = genThreadsList(c, count) if count == 0: From 7d4015321717ed53061d76ede2f73f6dc3dbce7b Mon Sep 17 00:00:00 2001 From: Grzegorz Adam Hankiewicz Date: Thu, 28 Mar 2013 23:03:50 +0100 Subject: [PATCH 018/560] Removes unneeded stats calls during rss generation. --- forum.nim | 3 +-- main.tmpl | 9 ++------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/forum.nim b/forum.nim index c343f74..29d9d36 100644 --- a/forum.nim +++ b/forum.nim @@ -650,8 +650,7 @@ get "/": get "/threadActivity.xml": createTFD() c.isThreadsList = true - var count = 0 - resp genRSS(c), "application/atom+xml" + resp genThreadsRSS(c), "application/atom+xml" get "/t/@threadid/?@page?/?": createTFD() diff --git a/main.tmpl b/main.tmpl index 95a8f58..9a05826 100644 --- a/main.tmpl +++ b/main.tmpl @@ -69,14 +69,8 @@ type="application/atom+xml" rel="alternate"> #end proc # -#proc genRSS(c: var TForumData): string = +#proc genThreadsRSS(c: var TForumData): string = # result = "" -# var stats: TForumStats -# let frontQuery = c.req.makeUri("/") -# if c.isThreadsList: stats = c.getStats(false) -# else: -# stats = c.getStats(true) -# end if # const query = sql"""SELECT A.id, A.name, # strftime('%Y-%m-%dT%H:%M:%S', (A.modified)), # COUNT(B.id), C.name, B.content, B.id @@ -91,6 +85,7 @@ # const postAuthor = 4 # const postContent = 5 # const postId = 6 +# let frontQuery = c.req.makeUri("/") # let recent = GetValue(db, sql"""SELECT # strftime('%Y-%m-%dT%H:%M:%S', (modified)) FROM thread # ORDER BY modified DESC LIMIT 1""") From ccd7b8c59e1f13e4837569c85b77e7368d27a9e5 Mon Sep 17 00:00:00 2001 From: Grzegorz Adam Hankiewicz Date: Thu, 28 Mar 2013 23:53:13 +0100 Subject: [PATCH 019/560] Implements post activity rss feed for main page. --- forum.nim | 4 ++++ main.tmpl | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/forum.nim b/forum.nim index 29d9d36..3a9640e 100644 --- a/forum.nim +++ b/forum.nim @@ -652,6 +652,10 @@ get "/threadActivity.xml": c.isThreadsList = true resp genThreadsRSS(c), "application/atom+xml" +get "/postActivity.xml": + createTFD() + resp genPostsRSS(c), "application/atom+xml" + get "/t/@threadid/?@page?/?": createTFD() if @"page".len > 0: diff --git a/main.tmpl b/main.tmpl index 9a05826..34d93b6 100644 --- a/main.tmpl +++ b/main.tmpl @@ -65,7 +65,9 @@ # #proc genRSSHeaders(c: var TForumData): string = # result = "" - + #end proc # @@ -112,3 +114,50 @@ # end for #end proc +# +#proc genPostsRSS(c: var TForumData): string = +# result = "" +# const query = sql"""SELECT A.id, B.name, A.content, A.thread, +# A.header, strftime('%Y-%m-%dT%H:%M:%S', A.creation), +# A.creation, COUNT(C.id) +# FROM post AS A, person AS B, post AS C +# WHERE A.author = B.id AND A.thread = C.thread AND C.id <= A.id +# GROUP BY A.id +# ORDER BY A.creation DESC LIMIT ?""" +# const postId = 0 +# const postAuthor = 1 +# const postContent = 2 +# const postThread = 3 +# const postHeader = 4 +# const postRssDate = 5 +# const postHumanDate = 6 +# const postPosition = 7 +# let frontQuery = c.req.makeUri("/") +# let recent = GetValue(db, sql"""SELECT +# strftime('%Y-%m-%dT%H:%M:%S', creation) FROM post +# ORDER BY creation DESC LIMIT 1""") + + + Nimrod forum post activity + + + ${frontQuery} + ${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 + + ${%postRssDate} + On ${XMLEncode(%postHumanDate)}, ${XMLEncode(%postAuthor)} said: + +${XMLEncode(%postContent)} + +# end for + +#end proc From 9a917fe1902d9b8bc1234243ba358fe22075bfd3 Mon Sep 17 00:00:00 2001 From: Grzegorz Adam Hankiewicz Date: Fri, 29 Mar 2013 01:27:20 +0100 Subject: [PATCH 020/560] Adds visible links to rss feeds along with svg icon. --- forum.nim | 5 +++-- main.tmpl | 16 +++++++++++++++- public/css/style.css | 6 ++++++ public/images/Feed-icon.svg | 18 ++++++++++++++++++ 4 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 public/images/Feed-icon.svg diff --git a/forum.nim b/forum.nim index 3a9640e..e9b7a59 100644 --- a/forum.nim +++ b/forum.nim @@ -645,7 +645,8 @@ get "/": createTFD() c.isThreadsList = true var count = 0 - resp genMain(c, genThreadsList(c, count), genRSSHeaders(c)) + resp genMain(c, genThreadsList(c, count), genRSSHeaders(c), + showRssLinks = true) get "/threadActivity.xml": createTFD() @@ -700,7 +701,7 @@ get "/page/@page/?": let list = genThreadsList(c, count) if count == 0: pass() - resp genMain(c, list, genRSSHeaders(c)) + resp genMain(c, list, genRSSHeaders(c), showRssLinks = true) get "/profile/@nick/?": createTFD() diff --git a/main.tmpl b/main.tmpl index 34d93b6..0b2829f 100644 --- a/main.tmpl +++ b/main.tmpl @@ -1,6 +1,6 @@ #! stdtmpl #proc genMain(c: var TForumData, content: string, -# additional_headers = ""): string = +# additional_headers = "", showRssLinks = false): string = # result = "" # var stats: TForumStats # if c.isThreadsList: stats = c.getStats(false) @@ -47,6 +47,20 @@ #end if
${c.genActionMenu} + #if showRssLinks: + + Thread activity + Posts activity + + #end if
#if c.isThreadsList: diff --git a/public/css/style.css b/public/css/style.css index 8f72112..b27b066 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -182,6 +182,12 @@ div#header a:hover, #nimbtn a:hover { margin: 5pt; } +div#topbar span#rss { + float: right; + padding: 0; + padding-right: 7pt; +} + #content .post { border: #4d4d4d solid 2px; width: 100%; diff --git a/public/images/Feed-icon.svg b/public/images/Feed-icon.svg new file mode 100644 index 0000000..b325149 --- /dev/null +++ b/public/images/Feed-icon.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + From 849a4142068f6cbf176c5128e74adeed2faee3ff Mon Sep 17 00:00:00 2001 From: Grzegorz Adam Hankiewicz Date: Fri, 29 Mar 2013 12:37:41 +0100 Subject: [PATCH 021/560] Corrects developer documentation header link. --- main.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.tmpl b/main.tmpl index d30168e..b4d0b64 100644 --- a/main.tmpl +++ b/main.tmpl @@ -25,7 +25,7 @@ #end proc diff --git a/main.tmpl b/main.tmpl index 6b4036d..108532f 100644 --- a/main.tmpl +++ b/main.tmpl @@ -23,9 +23,9 @@
+
+
+ + + + +
+
+
$content $c.errorMsg From aae5609bbbfa3c745c3a58f253086b66be1a614e Mon Sep 17 00:00:00 2001 From: Grzegorz Adam Hankiewicz Date: Sun, 6 Apr 2014 12:19:55 +0200 Subject: [PATCH 034/560] Adds published tag as duplicate of updated for old rss clients. --- main.tmpl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main.tmpl b/main.tmpl index 108532f..e9b1b51 100644 --- a/main.tmpl +++ b/main.tmpl @@ -129,6 +129,7 @@ # "#" & %postId + ${%threadDate} ${%threadDate} ${XMLEncode(%postAuthor)} # "#" & %postId + ${%postRssDate} ${%postRssDate} ${XMLEncode(%postAuthor)} Date: Thu, 10 Jul 2014 00:19:22 +0200 Subject: [PATCH 035/560] compiles with the latest compiler version --- forum.nim | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/forum.nim b/forum.nim index 7251c23..9a62841 100644 --- a/forum.nim +++ b/forum.nim @@ -8,7 +8,9 @@ import os, strutils, times, md5, strtabs, cgi, math, db_sqlite, matchers, - rst, rstgen, captchas, sockets, scgi, jester, htmlgen + rst, rstgen, captchas, sockets, scgi, jester + +from htmlgen import tr, th, td, span const unselectedThread = -1 @@ -500,8 +502,8 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string = firstTag = span("First") prevTag = span("Prev") else: - firstTag = a(href=firstUrl, "First") - prevTag = a(href=prevUrl, "Prev") + firstTag = htmlgen.a(href=firstUrl, "First") + prevTag = htmlgen.a(href=prevUrl, "Prev") result.add(htmlgen.`div`(class = "left", firstTag, prevTag)) @@ -512,8 +514,8 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string = lastTag = span("Last") nextTag = span("Next") else: - lastTag = a(href=lastUrl, "Last") - nextTag = a(href=nextUrl, "Next") + lastTag = htmlgen.a(href=lastUrl, "Last") + nextTag = htmlgen.a(href=nextUrl, "Next") result.add(htmlgen.`div`(class = "right", nextTag, lastTag)) @@ -530,7 +532,7 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string = else: pageUrl = c.req.makeUri(firstUrl & "/" & $(i)) - pages.add(a(href = pageUrl, $(i))) + pages.add(htmlgen.a(href = pageUrl, $(i))) result.add(htmlgen.`div`(class = "middle", pages)) @@ -569,7 +571,7 @@ proc genPagenumLocalNav(c: var TForumData, thrid: int): string = if totalPagesInThread <= 1: return var i = 1 while i <= totalPagesInThread: - result.add(a(href=c.req.makeUri(currentThrURL & $i), $i)) + result.add(htmlgen.a(href=c.req.makeUri(currentThrURL & $i), $i)) if i == hmpp and totalPagesInThread-i > hmpp: result.add(span("...")) # skip to the last 3 @@ -606,7 +608,7 @@ proc genProfile(c: var TForumData, ui: TUserInfo): string = else: getGMTime(GetTime()) result.add(htmlgen.`div`(id = "info", - table( + htmlgen.table( tr( th("Nickname"), td(ui.nick) @@ -755,7 +757,7 @@ template handleError(action: string, topText: string, isEdit: bool): stmt = body.add genPostPreview(c, @"subject", @"content", c.userName, $getGMTime(getTime())) body.add genFormPost(c, action, topText, reuseText, reuseText, isEdit) - resp genMain(c, body, "Nimrod Forum - Error") + resp genMain(c, body(), "Nimrod Forum - Error") post "/dologin": createTFD() From fbd8f057bb8ac12f9ad26a6378454f3708d1bd1c Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 13 Jul 2014 10:45:26 +0100 Subject: [PATCH 036/560] Fixes for new_async version of Jester. --- captchas.nim | 2 +- forum.nim | 314 +++++++++++++++++++++++++------------------------ nimforum.babel | 11 ++ nimrod.cfg | 1 - 4 files changed, 172 insertions(+), 156 deletions(-) create mode 100644 nimforum.babel diff --git a/captchas.nim b/captchas.nim index a3616f3..d95767b 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: var TRequest, i: int): string = +proc getCaptchaUrl*(req: PRequest, i: int): string = result = req.makeUri("/captchas/capture_" & $i & ".png", absolute = false) proc createCaptcha*(file, text: string) = diff --git a/forum.nim b/forum.nim index 9a62841..3983d0c 100644 --- a/forum.nim +++ b/forum.nim @@ -8,7 +8,7 @@ import os, strutils, times, md5, strtabs, cgi, math, db_sqlite, matchers, - rst, rstgen, captchas, sockets, scgi, jester + rst, rstgen, captchas, scgi, jester, asyncdispatch, asyncnet from htmlgen import tr, th, td, span @@ -33,7 +33,7 @@ type TPost = tuple[subject, content: string] TForumData = object of TSession - req: TRequest + req: PRequest userid: string actionContent: string errorMsg, loginErrorMsg: string @@ -651,167 +651,168 @@ template createTFD(): stmt = if request.cookies.len > 0: checkLoggedIn(c) -get "/": - createTFD() - c.isThreadsList = true - var count = 0 - resp genMain(c, genThreadsList(c, count), - additionalHeaders = genRSSHeaders(c), showRssLinks = true) +routes: + get "/": + createTFD() + c.isThreadsList = true + var count = 0 + resp genMain(c, genThreadsList(c, count), + additionalHeaders = genRSSHeaders(c), showRssLinks = true) -get "/threadActivity.xml": - createTFD() - c.isThreadsList = true - resp genThreadsRSS(c), "application/atom+xml" + get "/threadActivity.xml": + createTFD() + c.isThreadsList = true + resp genThreadsRSS(c), "application/atom+xml" -get "/postActivity.xml": - createTFD() - resp genPostsRSS(c), "application/atom+xml" + get "/postActivity.xml": + createTFD() + resp genPostsRSS(c), "application/atom+xml" -get "/t/@threadid/?@page?/?": - createTFD() - parseInt(@"threadid", c.threadId, -1..1000_000) - if @"page".len > 0: + get "/t/@threadid/?@page?/?": + createTFD() + parseInt(@"threadid", c.threadId, -1..1000_000) + if @"page".len > 0: + parseInt(@"page", c.pageNum, 0..1000_000) + cond (c.pageNum > 0) + if (@"postid").len > 0: + parseInt(@"postid", c.postId, -1..1000_000) + 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" + resp c.genMain(body, title & " - Nimrod Forum") + else: + incrementViews(c) + let posts = genPostsList(c, $c.threadId, count) + cond count != 0 + resp genMain(c, posts, pSubject & " - Nimrod Forum") + + get "/page/@page/?": + createTFD() + c.isThreadsList = true + cond (@"page" != "") parseInt(@"page", c.pageNum, 0..1000_000) cond (c.pageNum > 0) - if (@"postid").len > 0: - parseInt(@"postid", c.postId, -1..1000_000) - 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" - resp c.genMain(body, title & " - Nimrod Forum") - else: - incrementViews(c) - let posts = genPostsList(c, $c.threadId, count) - cond count != 0 - resp genMain(c, posts, pSubject & " - Nimrod 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 & " - Nimrod 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 - Nimrod Forum") - else: - halt() - -get "/login/?": - createTFD() - resp genMain(c, genFormLogin(c), "Log in - Nimrod Forum") - -get "/logout/?": - createTFD() - logout(c) - redirect(uri("/")) - -get "/register/?": - createTFD() - resp genMain(c, genFormRegister(c), "Register - Nimrod Forum") - -template readIDs(): stmt = - # 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 = - 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", - c.userName, $getGMTime(getTime())) - body.add genFormPost(c, action, topText, reuseText, reuseText, isEdit) - resp genMain(c, body(), "Nimrod Forum - Error") - -post "/dologin": - createTFD() - if login(c, @"name", @"password"): - finishLogin() - else: - resp c.genMain(genFormLogin(c)) - -post "/doregister": - createTFD() - if c.register(@"name", @"new_password", @"antibot", @"email"): - discard c.login(@"name", @"new_password") - finishLogin() - else: - resp c.genMain(genFormRegister(c)) - -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 + let list = genThreadsList(c, count) + if count == 0: + pass() + resp genMain(c, list, "Page " & $c.pageNum & " - Nimrod 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 - Nimrod Forum") + else: + halt() + + get "/login/?": + createTFD() + resp genMain(c, genFormLogin(c), "Log in - Nimrod Forum") + + get "/logout/?": + createTFD() + logout(c) + redirect(uri("/")) + + get "/register/?": + createTFD() + resp genMain(c, genFormRegister(c), "Register - Nimrod Forum") + + template readIDs(): stmt = + # 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 = + setCookie("sid", c.userpass, daysForward(7)) + redirect(uri("/")) + + template handleError(action: string, topText: string, isEdit: bool): stmt = if c.isPreview: - c.pageNum = c.getPagesInThread+1 - body = genPostsList(c, $c.threadId, count) - handleError("doreply", "Reply", false) + body.add genPostPreview(c, @"subject", @"content", + c.userName, $getGMTime(getTime())) + body.add genFormPost(c, action, topText, reuseText, reuseText, isEdit) + resp genMain(c, body(), "Nimrod Forum - Error") -post "/doedit": - createTFD() - readIDs() - if edit(c, c.postId): - redirect(c.genThreadUrl()) - else: - body = "" - handleError("doedit", "Edit", true) + post "/dologin": + createTFD() + if login(c, @"name", @"password"): + finishLogin() + else: + resp c.genMain(genFormLogin(c)) -get "/newthread/?": - createTFD() - resp genMain(c, genFormPost(c, "donewthread", "New thread", "", "", false), - "New Thread - Nimrod Forum") + post "/doregister": + createTFD() + if c.register(@"name", @"new_password", @"antibot", @"email"): + discard c.login(@"name", @"new_password") + finishLogin() + else: + resp c.genMain(genFormRegister(c)) -const licenseRst = slurp("static/license.rst") -get "/license": - createTFD() - resp genMain(c, rstToHtml(licenseRst), "Content license - Nimrod Forum") + 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()) + else: + body = "" + handleError("doedit", "Edit", true) + + get "/newthread/?": + createTFD() + resp genMain(c, genFormPost(c, "donewthread", "New thread", "", "", false), + "New Thread - Nimrod Forum") + + const licenseRst = slurp("static/license.rst") + get "/license": + createTFD() + resp genMain(c, rstToHtml(licenseRst), "Content license - Nimrod Forum") when isMainModule: docConfig = rstgen.defaultConfig() @@ -822,6 +823,11 @@ when isMainModule: if paramCount() > 0: if paramStr(1) == "scgi": http = false - run("", port = TPort(9000), http = http) + + #run("", port = TPort(9000), http = http) + var settings = newSettings() + settings.port = TPort(8080) + jester.serve(settings, match) + runForever() db.close() diff --git a/nimforum.babel b/nimforum.babel new file mode 100644 index 0000000..6c6f161 --- /dev/null +++ b/nimforum.babel @@ -0,0 +1,11 @@ +[Package] +name = "nimforum" +version = "0.1.0" +author = "Dominik Picheta" +description = "Nimrod forum" +license = "MIT" + +bin = "forum" + +[Deps] +Requires: "nimrod >= 0.9.2, cairo#head, jester#head" diff --git a/nimrod.cfg b/nimrod.cfg index c4baa08..9beea07 100644 --- a/nimrod.cfg +++ b/nimrod.cfg @@ -3,4 +3,3 @@ --path:"$nimrod/lib/packages/docutils" --path:"$nimrod" ---path:"../jester" From d2942cc0c339e9ce021cf603073748ab0ed437ba Mon Sep 17 00:00:00 2001 From: Clement Date: Wed, 16 Jul 2014 21:08:53 +0200 Subject: [PATCH 037/560] 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 038/560] 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 da1876f7d012797df1d21564b3379a0ec83ea675 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 11 Oct 2014 18:39:45 +0100 Subject: [PATCH 039/560] Fixes CS problems. --- forms.tmpl | 32 +++++++++++------------ forum.nim | 74 +++++++++++++++++++++++++++++------------------------- main.tmpl | 32 +++++++++++------------ 3 files changed, 72 insertions(+), 66 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index c74a0fe..7427f55 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -21,21 +21,21 @@
Views Last reply
- ${UrlButton(c, XMLencode(%name), c.genThreadUrl(threadid = %threadid))} + ${UrlButton(c, xmlEncode(%name), c.genThreadUrl(threadid = %threadid))} ${genPagenumLocalNav(c, (%threadid).parseInt)} ${authorName}$posts${XMLencode(%views)}${xmlEncode(%views)} ${formatTimestamp(latestReplyDate.parseInt())}
#let replyProfileUrl = c.req.makeUri("profile/", false) & - # XMLEncode(latestReplyAuthor) + # xmlEncode(latestReplyAuthor) ${latestReplyAuthor}
+ #if useCaptcha: - - + + + #end if
- ${XMLEncode(title)} - ${XMLencode(date)} + ${xmlEncode(title)} + ${xmlEncode(date)}
- ${XMLencode(author)} + ${xmlEncode(author)} #try: @@ -92,20 +92,20 @@ # const userEmail = 6 # result = "" # count = 0 -# for row in FastRows(db, query, threadId, $((c.pageNum-1) * PostsPerPage), $PostsPerPage): +# for row in fastRows(db, query, threadId, $((c.pageNum-1) * PostsPerPage), $PostsPerPage): # inc(count) - + - + @@ -476,4 +476,4 @@
- ${XMLencode(%postHeader)} - ${XMLencode(%postCreation)} + ${xmlEncode(%postHeader)} + ${xmlEncode(%postCreation)}
- #let profileUrl = c.req.makeUri("profile/", false) & XMLencode(%userName) - ${XMLencode(%userName)} + #let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(%userName) + ${xmlEncode(%userName)}
${genGravatar(%userEmail)} #if c.userId == %postAuthor and c.currentPost.subject.len == 0: @@ -213,7 +213,7 @@ Out of ${stats.totalUsers} users ${stats.activeUsers.len} are online${if stats.activeUsers.len == 0: "." else: ":"} #for index, usr in stats.activeUsers: # let profileHref = """""" + # xmlEncode(usr.nick) & """">""" # let hrefEnd = """""" # if usr.isAdmin: #if index != 0: result.add ',' @@ -231,7 +231,7 @@
#if stats.newestMember.nick != "": #let profileUrl = c.req.makeUri("profile/", false) & - # XMLEncode(stats.newestMember.nick) + # xmlEncode(stats.newestMember.nick) Total threads: ${stats.totalThreads} | Total posts: ${stats.totalPosts} | Newest member: ${stats.newestMember.nick} #else: Total threads: ${stats.totalThreads} | Total posts: ${stats.totalPosts} diff --git a/forum.nim b/forum.nim index 3983d0c..04b28d9 100644 --- a/forum.nim +++ b/forum.nim @@ -9,7 +9,6 @@ import os, strutils, times, md5, strtabs, cgi, math, db_sqlite, matchers, rst, rstgen, captchas, scgi, jester, asyncdispatch, asyncnet - from htmlgen import tr, th, td, span const @@ -90,14 +89,14 @@ 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[name]) return """""" % [ name, $maxlength, x, if size != -1: "size=\"" & $size & "\"" else: ""] proc TextAreaWidget(c: TForumData, name, defaultText: string, width = 80, height = 20): string = let x = if defaultText != reuseText: defaultText - else: XMLencode(c.req.params[name]) + else: xmlEncode(c.req.params[name]) return """""" % [ name, $width, $height, x] @@ -189,7 +188,7 @@ proc makePassword(password, salt: string): 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 = @@ -197,12 +196,12 @@ proc antibot(c: var TForumData): string = 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, + 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) + let captchaFile = getCaptchaFilename(captchaId) + createCaptcha(captchaFile, $a & "+" & $b) result = """""" % c.req.getCaptchaUrl(captchaId) const @@ -217,7 +216,7 @@ 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: + if getValue(db, sql"select name from person where name = ?", name).len > 0: return setError(c, "name", "Username already exists!") # Password validation: @@ -225,7 +224,7 @@ proc register(c: var TForumData, name, pass, antibot, email: string): bool = 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!") @@ -236,7 +235,7 @@ proc register(c: var TForumData, name, pass, antibot, email: string): bool = # perform registration: var salt = makeSalt() - Exec(db, sql("INSERT INTO person(name, password, email, salt, status, lastOnline) " & + exec(db, sql("INSERT INTO person(name, password, email, salt, status, lastOnline) " & "VALUES (?, ?, ?, ?, 'user', DATETIME('now'))"), name, makePassword(pass, salt), email, salt) # return setError(c, "", "Could not create your account!") @@ -245,12 +244,12 @@ proc register(c: var TForumData, name, pass, antibot, email: string): bool = 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 = ?"), c.req.ip, pass) > 0: c.userpass = pass - c.userid = GetValue(db, + c.userid = getValue(db, sql"select userid from session where ip = ? and password = ?", c.req.ip, pass) @@ -270,11 +269,11 @@ 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"]) + 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) + exec(db, query, $c.threadId) proc isPreview(c: TForumData): bool = result = c.req.params["previewBtn"].len > 0 # TODO: Could be wrong? @@ -363,10 +362,10 @@ proc edit(c: var TForumData, postId: int): bool = setPreviewData(c) elif c.isDelete: checkOwnership(c, $postId) - if not TryExec(db, crud(crDelete, "post"), $postId): + if not tryExec(db, crud(crDelete, "post"), $postId): return setError(c, "", "database error") # delete corresponding thread: - if ExecAffectedRows(db, + if execAffectedRows(db, sql"delete from thread where id not in (select thread from post)") > 0: # whole thread has been deleted, so: c.threadId = unselectedThread @@ -398,7 +397,7 @@ proc newThread(c: var TForumData): bool = setPreviewData(c) c.threadID = transientThread else: - c.threadID = TryInsertID(db, query, c.req.params["subject"]).int + c.threadID = tryInsertID(db, query, c.req.params["subject"]).int if c.threadID < 0: return setError(c, "subject", "Subject already exists") writeToDb(c, crCreate, false) result = true @@ -410,7 +409,7 @@ proc login(c: var TForumData, name, pass: string): bool = if name.len == 0: return c.setError("name", "Username cannot be nil.") var success = false - for row in FastRows(db, query, name): + for row in fastRows(db, query, name): if row[2] == makePassword(pass, row[4]): c.userid = row[0] c.username = row[1] @@ -421,7 +420,7 @@ proc login(c: var TForumData, name, pass: string): bool = break if success: # create session: - Exec(db, + exec(db, sql"insert into session (ip, password, userid) values (?, ?, ?)", c.req.ip, c.userpass, c.userid) return true @@ -434,11 +433,12 @@ proc genActionMenu(c: var TForumData): string = # 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" + pageNum = $(ceil(c.totalPosts / PostsPerPage).int)) & "#reply" btns.add(("Reply", replyUrl)) btns.add(("New Thread", c.req.makeUri("/newthread", false))) result = c.genButtons(btns) @@ -489,7 +489,7 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string = prevUrl = firstUrl else: prevUrl = c.req.makeUri(firstUrl & "/" & $(c.pageNum-1)) - totalPages = ceil(c.totalPosts / postsPerPage).int + totalPages = ceil(c.totalPosts / PostsPerPage).int lastUrl = c.req.makeUri(firstUrl & "/" & $(totalPages)) nextUrl = c.req.makeUri(firstUrl & "/" & $(c.pageNum+1)) @@ -540,7 +540,7 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string = proc gatherTotalPostsByID(c: var TForumData, thrid: int): int = ## Gets the total post count of a thread. - result = GetValue(db, sql"select count(*) from post where thread = ?", $thrid).parseInt + result = getValue(db, sql"select count(*) from post where thread = ?", $thrid).parseInt proc gatherTotalPosts(c: var TForumData) = if c.totalPosts > 0: return @@ -551,13 +551,13 @@ proc gatherTotalPosts(c: var TForumData) = proc getPagesInThread(c: var TForumData): int = c.gatherTotalPosts() # Get total post count - result = ceil(c.totalPosts / postsPerPage).int-1 + result = ceil(c.totalPosts / PostsPerPage).int-1 proc getPagesInThreadByID(c: var TForumData, thrid: int): int = - result = ceil(c.gatherTotalPostsByID(thrid) / postsPerPage).int + result = ceil(c.gatherTotalPostsByID(thrid) / PostsPerPage).int proc getThreadTitle(thrid: int, pageNum: int): string = - result = GetValue(db, sql"select name from thread where id = ?", $thrid) + result = getValue(db, sql"select name from thread where id = ?", $thrid) if pageNum notin {0,1}: result.add(" - Page " & $pageNum) @@ -605,7 +605,7 @@ proc genProfile(c: var TForumData, ui: TUserInfo): string = result = "" result.add(htmlgen.`div`(id = "avatar", genGravatar(ui.email, 250))) let t2 = if ui.lastOnline != -1: getGMTime(TTime(ui.lastOnline)) - else: getGMTime(GetTime()) + else: getGMTime(getTime()) result.add(htmlgen.`div`(id = "info", htmlgen.table( @@ -651,13 +651,21 @@ template createTFD(): stmt = if request.cookies.len > 0: checkLoggedIn(c) +var cached = "" + +#var settings = newSettings() +#settings.port = Port(5000) + routes: get "/": createTFD() c.isThreadsList = true var count = 0 - resp genMain(c, genThreadsList(c, count), - additionalHeaders = genRSSHeaders(c), showRssLinks = true) + discard genThreadsList(c, count) + if cached == "": + cached = genMain(c, genThreadsList(c, count), + additionalHeaders = genRSSHeaders(c), showRssLinks = true) + resp cached get "/threadActivity.xml": createTFD() @@ -684,7 +692,7 @@ routes: var title = "" case @"action" of "reply": - let subject = GetValue(db, + 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) @@ -817,7 +825,7 @@ routes: when isMainModule: docConfig = rstgen.defaultConfig() math.randomize() - db = Open(connection="nimforum.db", user="postgres", password="", + db = open(connection="nimforum.db", user="postgres", password="", database="nimforum") var http = true if paramCount() > 0: @@ -825,9 +833,7 @@ when isMainModule: http = false #run("", port = TPort(9000), http = http) - var settings = newSettings() - settings.port = TPort(8080) - jester.serve(settings, match) + runForever() db.close() diff --git a/main.tmpl b/main.tmpl index e9b1b51..8f6c288 100644 --- a/main.tmpl +++ b/main.tmpl @@ -10,7 +10,7 @@ - ${XmlEncode(title)} + ${xmlEncode(title)} ${additional_headers} @@ -30,7 +30,7 @@ #if c.loggedIn: Logout #let profileUrl = c.req.makeUri("profile/", false) & - # XMLencode(c.username) + # xmlEncode(c.username) $c.username ${genGravatar(c.email, 26)} #else: @@ -110,7 +110,7 @@ # const postContent = 5 # const postId = 6 # let frontQuery = c.req.makeUri("/") -# let recent = GetValue(db, sql"""SELECT +# let recent = getValue(db, sql"""SELECT # strftime('%Y-%m-%dT%H:%M:%SZ', (modified)) FROM thread # ORDER BY modified DESC LIMIT 1""") @@ -120,22 +120,22 @@ ${frontQuery} ${recent} -# for row in Rows(db, query, 10): +# for row in rows(db, query, 10): - ${XMLencode(%name)} + ${xmlEncode(%name)} urn:entry:${%threadid} # let url = c.genThreadUrl(threadid = %threadid, - # pageNum = $(ceil(parseInt(%postCount) / postsPerPage).int)) & + # pageNum = $(ceil(parseInt(%postCount) / PostsPerPage).int)) & # "#" & %postId ${%threadDate} ${%threadDate} - ${XMLEncode(%postAuthor)} + ${xmlEncode(%postAuthor)} Posts ${%postCount}, ${XMLEncode(%postAuthor)} said: +>Posts ${%postCount}, ${xmlEncode(%postAuthor)} said: <p> -${XMLEncode(rstToHtml(%postContent))} +${xmlEncode(rstToHtml(%postContent))} # end for @@ -159,7 +159,7 @@ ${XMLEncode(rstToHtml(%postContent))} # const postHumanDate = 6 # const postPosition = 7 # let frontQuery = c.req.makeUri("/") -# let recent = GetValue(db, sql"""SELECT +# let recent = getValue(db, sql"""SELECT # strftime('%Y-%m-%dT%H:%M:%SZ', creation) FROM post # ORDER BY creation DESC LIMIT 1""") @@ -169,22 +169,22 @@ ${XMLEncode(rstToHtml(%postContent))} ${frontQuery} ${recent} -# for row in Rows(db, query, 10): +# for row in rows(db, query, 10): - ${XMLencode(%postHeader)} + ${xmlEncode(%postHeader)} urn:entry:${%postId} # let url = c.genThreadUrl(threadid = %postThread, - # pageNum = $(ceil(parseInt(%postPosition) / postsPerPage).int)) & + # pageNum = $(ceil(parseInt(%postPosition) / PostsPerPage).int)) & # "#" & %postId ${%postRssDate} ${%postRssDate} - ${XMLEncode(%postAuthor)} + ${xmlEncode(%postAuthor)} On ${XMLEncode(%postHumanDate)}, ${XMLEncode(%postAuthor)} said: +>On ${xmlEncode(%postHumanDate)}, ${xmlEncode(%postAuthor)} said: <p> -${XMLEncode(rstToHtml(%postContent))} +${xmlEncode(rstToHtml(%postContent))} # end for From 3af3de2ea3555aff23ec84beb9c61ae8c0981b15 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 14 Oct 2014 17:22:02 +0100 Subject: [PATCH 040/560] Fix createdb and config filename. --- createdb.nim | 18 +++++++++--------- nimrod.cfg => forum.nim.cfg | 0 2 files changed, 9 insertions(+), 9 deletions(-) rename nimrod.cfg => forum.nim.cfg (100%) diff --git a/createdb.nim b/createdb.nim index 80a0815..860cda9 100644 --- a/createdb.nim +++ b/createdb.nim @@ -8,7 +8,7 @@ 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 @@ -16,7 +16,7 @@ const TPassword = "varchar(32)" TEmail = "varchar(30)" -db.Exec(sql""" +db.exec(sql""" create table if not exists thread( id integer primary key, name varchar(100) not null, @@ -24,11 +24,11 @@ create table if not exists thread( modified timestamp not null default (DATETIME('now')) );""", []) -db.Exec(sql""" +db.exec(sql""" create unique index if not exists ThreadNameIx on thread (name); """, []) -db.Exec(sql(""" +db.exec(sql(""" create table if not exists person( id integer primary key, name $# not null, @@ -42,14 +42,14 @@ create table if not exists person( );""" % [TUserName, TPassword, TEmail]), []) # echo "person table already exists" -db.Exec(sql""" +db.exec(sql""" create unique index if not exists UserNameIx on person (name); """, []) # ----------------------- Forum ------------------------------------------------ -if not db.TryExec(sql""" +if not db.tryExec(sql""" create table if not exists post( id integer primary key, author integer not null, @@ -66,7 +66,7 @@ create table if not exists post( # -------------------- Session ------------------------------------------------- -if not db.TryExec(sql(""" +if not db.tryExec(sql(""" create table if not exists session( id integer primary key, ip inet not null, @@ -77,7 +77,7 @@ create table if not exists session( );""" % [TPassword]), []): echo "session table already exists" -if not db.TryExec(sql""" +if not db.tryExec(sql""" create table if not exists antibot( id integer primary key, ip inet not null, @@ -88,6 +88,6 @@ create table if not exists antibot( #discard stdin.readline() -Close(db) +close(db) diff --git a/nimrod.cfg b/forum.nim.cfg similarity index 100% rename from nimrod.cfg rename to forum.nim.cfg From 630831f772ee95bad4c8fa92ec7574e5ddbae124 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 17 Oct 2014 18:35:49 +0100 Subject: [PATCH 041/560] Fixed caching. --- cache.nim | 34 ++++++++++++++++++++++++++++++++++ forum.nim | 21 +++++++++++---------- 2 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 cache.nim diff --git a/cache.nim b/cache.nim new file mode 100644 index 0000000..a67f278 --- /dev/null +++ b/cache.nim @@ -0,0 +1,34 @@ +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.mget(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 = + ## 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. + echo(cache.caches) + mixin normalizePath + let nName = name.normalizePath() + if not (cache.caches.hasKey(nName) and cache.caches[nName].valid): + echo "Resetting cache." + cache.caches[nName] = CacheInfo(valid: true, value: grabValue) + cache.caches[nName].value diff --git a/forum.nim b/forum.nim index 04b28d9..cfa1c1b 100644 --- a/forum.nim +++ b/forum.nim @@ -8,7 +8,7 @@ import os, strutils, times, md5, strtabs, cgi, math, db_sqlite, matchers, - rst, rstgen, captchas, scgi, jester, asyncdispatch, asyncnet + rst, rstgen, captchas, scgi, jester, asyncdispatch, asyncnet, cache from htmlgen import tr, th, td, span const @@ -651,21 +651,18 @@ template createTFD(): stmt = if request.cookies.len > 0: checkLoggedIn(c) -var cached = "" - -#var settings = newSettings() -#settings.port = Port(5000) +var cacheHolder = newCacheHolder() routes: get "/": createTFD() c.isThreadsList = true var count = 0 - discard genThreadsList(c, count) - if cached == "": - cached = genMain(c, genThreadsList(c, count), - additionalHeaders = genRSSHeaders(c), showRssLinks = true) - resp cached + let threadList = genThreadsList(c, count) + let data = cacheHolder.get("/", + genMain(c, threadList, + additionalHeaders = genRSSHeaders(c), showRssLinks = true)) + resp data get "/threadActivity.xml": createTFD() @@ -744,6 +741,7 @@ routes: get "/logout/?": createTFD() logout(c) + cacheHolder.invalidateAll() redirect(uri("/")) get "/register/?": @@ -772,6 +770,7 @@ routes: createTFD() if login(c, @"name", @"password"): finishLogin() + cacheHolder.invalidateAll() else: resp c.genMain(genFormLogin(c)) @@ -780,12 +779,14 @@ routes: if c.register(@"name", @"new_password", @"antibot", @"email"): discard c.login(@"name", @"new_password") finishLogin() + cacheHolder.invalidateAll() else: resp c.genMain(genFormRegister(c)) post "/donewthread": createTFD() if newThread(c): + cacheHolder.invalidate("/") redirect(uri("/")) else: body = "" From e520df80dae6aa9e1e6dd92e1aaaa58cd9587f0e Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 17 Oct 2014 18:39:09 +0100 Subject: [PATCH 042/560] Remove debug code. --- cache.nim | 2 -- 1 file changed, 2 deletions(-) diff --git a/cache.nim b/cache.nim index a67f278..8bc4274 100644 --- a/cache.nim +++ b/cache.nim @@ -25,10 +25,8 @@ proc invalidateAll*(cache: CacheHolder) = template get*(cache: CacheHolder, name: string, grabValue: expr): expr = ## 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. - echo(cache.caches) mixin normalizePath let nName = name.normalizePath() if not (cache.caches.hasKey(nName) and cache.caches[nName].valid): - echo "Resetting cache." cache.caches[nName] = CacheInfo(valid: true, value: grabValue) cache.caches[nName].value From 4de7e58511d3c64902432fbf603ba9c57b5ff74f Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 29 Oct 2014 22:12:05 +0000 Subject: [PATCH 043/560] First steps towards new forum design. --- forms.tmpl | 120 ++-- forum.nim | 42 +- main.tmpl | 163 +++-- public/css/arrow.js | 12 + public/css/forum.js | 5 + public/css/normalize.css | 284 --------- public/css/style.css | 952 +++++++++++++++++------------- public/images/bg.jpg | Bin 0 -> 94894 bytes public/images/forum-posts.png | Bin 0 -> 206 bytes public/images/forum-views.png | Bin 0 -> 424 bytes public/images/glow-arrow.png | Bin 0 -> 8657 bytes public/images/glow-line.png | Bin 0 -> 2261 bytes public/images/glow-line2.png | Bin 0 -> 2297 bytes public/images/head-link.png | Bin 0 -> 203 bytes public/images/head-link_hover.png | Bin 0 -> 799 bytes public/images/head.png | Bin 0 -> 171 bytes public/images/logo.png | Bin 0 -> 116562 bytes public/js/arrow.js | 12 + public/js/forum.js | 5 + 19 files changed, 736 insertions(+), 859 deletions(-) create mode 100644 public/css/arrow.js create mode 100644 public/css/forum.js delete mode 100644 public/css/normalize.css create mode 100644 public/images/bg.jpg create mode 100644 public/images/forum-posts.png create mode 100644 public/images/forum-views.png create mode 100644 public/images/glow-arrow.png create mode 100644 public/images/glow-line.png create mode 100644 public/images/glow-line2.png create mode 100644 public/images/head-link.png create mode 100644 public/images/head-link_hover.png create mode 100644 public/images/head.png create mode 100644 public/images/logo.png create mode 100644 public/js/arrow.js create mode 100644 public/js/forum.js diff --git a/forms.tmpl b/forms.tmpl index 7427f55..2b31609 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -13,43 +13,61 @@ # # result = "" # count = 0 - - - - - - - - +
+
Topic
+
Last
+
Details
+
Author
+
+
# for row in rows(db, query, $((c.pageNum-1) * ThreadsPerPage), $ThreadsPerPage): # inc(count) -
- - #let authorName = getValue(db, sql("select name from person where id = " & - # "(select author from post where id = " & - # "(select min(id) from post where thread = ?))"), %threadId) - #let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(authorName) - -# let posts = getValue(db, sql"select count(*) from post where thread = ?", %threadId) - - +
+
+
+ ${UrlButton(c, xmlEncode(%name), c.genThreadUrl(threadid = %threadid))} + ${genPagenumLocalNav(c, (%threadid).parseInt)} +
+
+ #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) #let latestReplyDate = getValue(db, sql("SELECT strftime('%s', " & # "(select creation from post where id = (select max(id) from post where thread = ?)))"), %threadId) -
- + #let timeStr = formatTimestamp(latestReplyDate.parseInt()) + #let replyProfileUrl = c.req.makeUri("profile/", false) & + # xmlEncode(latestReplyAuthor) + + +# let posts = getValue(db, sql"select count(*) from post where thread = ?", %threadId) +
+
${xmlEncode(%views)}
+
$posts
+
+ + #let authorName = getValue(db, sql("select name from person where id = " & + # "(select author from post where id = " & + # "(select min(id) from post where thread = ?))"), %threadId) + #let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(authorName) + + # end for -
TopicsAuthorPostsViewsLast reply
- ${UrlButton(c, xmlEncode(%name), c.genThreadUrl(threadid = %threadid))} - ${genPagenumLocalNav(c, (%threadid).parseInt)} - ${authorName}$posts${xmlEncode(%views)} - ${formatTimestamp(latestReplyDate.parseInt())}
- #let replyProfileUrl = c.req.makeUri("profile/", false) & - # xmlEncode(latestReplyAuthor) - ${latestReplyAuthor} -
+ + + + #end proc # # @@ -205,38 +223,14 @@ # #proc genListOnline(c: var TForumData, stats: TForumStats): string = # result = "" -
-
- Who is online? -
-
- Out of ${stats.totalUsers} users ${stats.activeUsers.len} are online${if stats.activeUsers.len == 0: "." else: ":"} - #for index, usr in stats.activeUsers: - # let profileHref = """""" - # let hrefEnd = """""" - # if usr.isAdmin: - #if index != 0: result.add ',' - #end if - #result.add(""" """ & profileHref & - # usr.nick & hrefEnd & """""") - # else: - #if index != 0: result.add ',' - #end if - #result.add(""" """ & profileHref & - # usr.nick & hrefEnd & """""") - # end if - #end for - -
- #if stats.newestMember.nick != "": - #let profileUrl = c.req.makeUri("profile/", false) & - # xmlEncode(stats.newestMember.nick) - Total threads: ${stats.totalThreads} | Total posts: ${stats.totalPosts} | Newest member: ${stats.newestMember.nick} - #else: - Total threads: ${stats.totalThreads} | Total posts: ${stats.totalPosts} - #end if -
-
- +# var active: seq[string] = @[] +# for i in stats.activeUsers: +# active.add(i.nick) +# end for +# let profileUrl = c.req.makeUri("profile/", false) & +# xmlEncode(stats.newestMember.nick) +  |  + ${stats.totalThreads} threads  |  ${stats.totalPosts} posts  |  + newest member: ${stats.newestMember.nick} #end proc diff --git a/forum.nim b/forum.nim index cfa1c1b..2247bf0 100644 --- a/forum.nim +++ b/forum.nim @@ -485,7 +485,7 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string = nextUrl = c.req.makeUri("/page/" & $(c.pageNum+1)) else: firstUrl = c.req.makeUri("/t/" & $c.threadId) - if c.pageNum == 1: + if c.pageNum == 1: prevUrl = firstUrl else: prevUrl = c.req.makeUri(firstUrl & "/" & $(c.pageNum-1)) @@ -499,26 +499,13 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string = var firstTag = "" var prevTag = "" if c.pageNum == 1: - firstTag = span("First") - prevTag = span("Prev") + firstTag = span("<<") + prevTag = span("<••") else: - firstTag = htmlgen.a(href=firstUrl, "First") - prevTag = htmlgen.a(href=prevUrl, "Prev") - result.add(htmlgen.`div`(class = "left", - firstTag, - prevTag)) - # Right - var lastTag = "" - var nextTag = "" - if c.pageNum == totalPages: - lastTag = span("Last") - nextTag = span("Next") - else: - lastTag = htmlgen.a(href=lastUrl, "Last") - nextTag = htmlgen.a(href=nextUrl, "Next") - result.add(htmlgen.`div`(class = "right", - nextTag, - lastTag)) + firstTag = htmlgen.a(href=firstUrl, "<<") + prevTag = htmlgen.a(href=prevUrl, "<••") + result.add(firstTag) + result.add(prevTag) # Numbers var pages = "" # Tags @@ -533,10 +520,19 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string = pageUrl = c.req.makeUri(firstUrl & "/" & $(i)) pages.add(htmlgen.a(href = pageUrl, $(i))) - result.add(htmlgen.`div`(class = "middle", - pages)) + result.add(pages) - result = htmlgen.`div`(id = "pagenumbers", result) + # Right + var lastTag = "" + var nextTag = "" + if c.pageNum == totalPages: + lastTag = span(">>") + nextTag = span("••>") + else: + lastTag = htmlgen.a(href=lastUrl, ">>") + nextTag = htmlgen.a(href=nextUrl, "••>") + result.add(lastTag) + result.add(nextTag) proc gatherTotalPostsByID(c: var TForumData, thrid: int): int = ## Gets the total post count of a thread. diff --git a/main.tmpl b/main.tmpl index 8f6c288..8a21565 100644 --- a/main.tmpl +++ b/main.tmpl @@ -11,76 +11,109 @@ ${xmlEncode(title)} - - ${additional_headers} + + ${additional_headers} -
- #let frontQuery = c.req.makeUri("/") -
- Forum + + #let frontQuery = c.req.makeUri("/") + + + +
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+ ${content} + #if c.isThreadsList: +
+
+
+ #if c.loggedIn: + #let profileUrl = c.req.makeUri("profile/", false) & + # xmlEncode(c.username) + Welcome ${c.username}! +  |  + + #end if + ${c.genListOnline(stats)} +
+
+
+
+ #if c.loggedIn: + new thread +  |  + logout + #else: + login +  |  + register + #end if +
+
+
+ #end if +
+ ${genPagenumNav(c, stats)} +
+
+
+
+ + + - - -
- ${c.genActionMenu} -
- -
- $content - $c.errorMsg -
- #if c.req.pathInfo.normalizeUri notin noPageNums: - ${c.genPagenumNav(stats)} - #end if -
- ${c.genActionMenu} -
- - #if c.isThreadsList: - ${c.genListOnline(stats)} - #end if - - #if showRssLinks: - - Thread activity - Posts activity - - #end if - -
-
- + + #end proc diff --git a/public/css/arrow.js b/public/css/arrow.js new file mode 100644 index 0000000..f3e1832 --- /dev/null +++ b/public/css/arrow.js @@ -0,0 +1,12 @@ +"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 new file mode 100644 index 0000000..7af672b --- /dev/null +++ b/public/css/forum.js @@ -0,0 +1,5 @@ +"use strict"; + +window.onload = function() { + positionGlowArrow(); +}; diff --git a/public/css/normalize.css b/public/css/normalize.css deleted file mode 100644 index 7dbb346..0000000 --- a/public/css/normalize.css +++ /dev/null @@ -1,284 +0,0 @@ -/* - * HTML5 Boilerplate - * - * What follows is the result of much research on cross-browser styling. - * Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal, - * Kroc Camen, and the H5BP dev community and team. - * - * Detailed information about this CSS: h5bp.com/css - * - * ==|== normalize ========================================================== - */ - - -/* ============================================================================= - HTML5 display definitions - ========================================================================== */ - -article, aside, details, figcaption, figure, footer, header, hgroup, nav, section { display: block; } -audio, canvas, video { display: inline-block; *display: inline; *zoom: 1; } -audio:not([controls]) { display: none; } -[hidden] { display: none; } - - -/* ============================================================================= - Base - ========================================================================== */ - -/* - * 1. Correct text resizing oddly in IE6/7 when body font-size is set using em units - * 2. Prevent iOS text size adjust on device orientation change, without disabling user zoom: h5bp.com/g - */ - -html { font-size: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } - -html, button, input, select, textarea { font-family: sans-serif; color: #222; } - -body { margin: 0; font-size: 1em; line-height: 1.4; } - - -/* ============================================================================= - Links - ========================================================================== */ - -a { color: #00e; } -a:visited { color: #551a8b; } -a:hover { color: #06e; } -a:focus { outline: thin dotted; } - -/* Improve readability when focused and hovered in all browsers: h5bp.com/h */ -a:hover, a:active { outline: 0; } - - -/* ============================================================================= - Typography - ========================================================================== */ - -abbr[title] { border-bottom: 1px dotted; } - -b, strong { font-weight: bold; } - -blockquote { margin: 1em 40px; } - -dfn { font-style: italic; } - -hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; } - -ins { background: #ff9; color: #000; text-decoration: none; } - -mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; } - -/* Redeclare monospace font family: h5bp.com/j */ -pre, code, kbd, samp { font-family: monospace, serif; _font-family: 'courier new', monospace; font-size: 1em; } - -/* Improve readability of pre-formatted text in all browsers */ -pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; } - -q { quotes: none; } -q:before, q:after { content: ""; content: none; } - -small { font-size: 85%; } - -/* Position subscript and superscript content without affecting line-height: h5bp.com/k */ -sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } -sup { top: -0.5em; } -sub { bottom: -0.25em; } - - -/* ============================================================================= - Lists - ========================================================================== */ - -ul, ol { margin: 1em 0; padding: 0 0 0 40px; } -dd { margin: 0 0 0 40px; } -nav ul, nav ol { list-style: none; list-style-image: none; margin: 0; padding: 0; } - - -/* ============================================================================= - Embedded content - ========================================================================== */ - -/* - * 1. Improve image quality when scaled in IE7: h5bp.com/d - * 2. Remove the gap between images and borders on image containers: h5bp.com/i/440 - */ - -img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; } - -/* - * Correct overflow not hidden in IE9 - */ - -svg:not(:root) { overflow: hidden; } - - -/* ============================================================================= - Figures - ========================================================================== */ - -figure { margin: 0; } - - -/* ============================================================================= - Forms - ========================================================================== */ - -form { margin: 0; } -fieldset { border: 0; margin: 0; padding: 0; } - -/* Indicate that 'label' will shift focus to the associated form element */ -label { cursor: pointer; } - -/* - * 1. Correct color not inheriting in IE6/7/8/9 - * 2. Correct alignment displayed oddly in IE6/7 - */ - -legend { border: 0; *margin-left: -7px; padding: 0; white-space: normal; } - -/* - * 1. Correct font-size not inheriting in all browsers - * 2. Remove margins in FF3/4 S5 Chrome - * 3. Define consistent vertical alignment display in all browsers - */ - -button, input, select, textarea { font-size: 100%; margin: 0; vertical-align: baseline; *vertical-align: middle; } - -/* - * 1. Define line-height as normal to match FF3/4 (set using !important in the UA stylesheet) - */ - -button, input { line-height: normal; } - -/* - * 1. Display hand cursor for clickable form elements - * 2. Allow styling of clickable form elements in iOS - * 3. Correct inner spacing displayed oddly in IE7 (doesn't effect IE6) - */ - -button, input[type="button"], input[type="reset"], input[type="submit"] { cursor: pointer; -webkit-appearance: button; *overflow: visible; } - -/* - * Re-set default cursor for disabled elements - */ - -button[disabled], input[disabled] { cursor: default; } - -/* - * Consistent box sizing and appearance - */ - -input[type="checkbox"], input[type="radio"] { box-sizing: border-box; padding: 0; *width: 13px; *height: 13px; } -input[type="search"] { -webkit-appearance: textfield; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; box-sizing: content-box; } -input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; } - -/* - * Remove inner padding and border in FF3/4: h5bp.com/l - */ - -button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; } - -/* - * 1. Remove default vertical scrollbar in IE6/7/8/9 - * 2. Allow only vertical resizing - */ - -textarea { overflow: auto; vertical-align: top; resize: vertical; } - -/* Colors for form validity */ -input:valid, textarea:valid { } -input:invalid, textarea:invalid { background-color: #f0dddd; } - - -/* ============================================================================= - Tables - ========================================================================== */ - -table { border-collapse: collapse; border-spacing: 0; } -td { vertical-align: top; } - - -/* ============================================================================= - Chrome Frame Prompt - ========================================================================== */ - -.chromeframe { margin: 0.2em 0; background: #ccc; color: black; padding: 0.2em 0; } - - -/* ==|== primary styles ===================================================== - Author: - ========================================================================== */ - - - - - - - - - - - - - - - - -/* ==|== media queries ====================================================== - EXAMPLE Media Query for Responsive Design. - This example overrides the primary ('mobile first') styles - Modify as content requires. - ========================================================================== */ - -@media only screen and (min-width: 35em) { - /* Style adjustments for viewports that meet the condition */ -} - - - -/* ==|== non-semantic helper classes ======================================== - Please define your styles before this section. - ========================================================================== */ - -/* For image replacement */ -.ir { display: block; border: 0; text-indent: -999em; overflow: hidden; background-color: transparent; background-repeat: no-repeat; text-align: left; direction: ltr; *line-height: 0; } -.ir br { display: none; } - -/* Hide from both screenreaders and browsers: h5bp.com/u */ -.hidden { display: none !important; visibility: hidden; } - -/* Hide only visually, but have it available for screenreaders: h5bp.com/v */ -.visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; } - -/* Extends the .visuallyhidden class to allow the element to be focusable when navigated to via the keyboard: h5bp.com/p */ -.visuallyhidden.focusable:active, .visuallyhidden.focusable:focus { clip: auto; height: auto; margin: 0; overflow: visible; position: static; width: auto; } - -/* Hide visually and from screenreaders, but maintain layout */ -.invisible { visibility: hidden; } - -/* Contain floats: h5bp.com/q */ -.clearfix:before, .clearfix:after { content: ""; display: table; } -.clearfix:after { clear: both; } -.clearfix { *zoom: 1; } - - - -/* ==|== print styles ======================================================= - Print styles. - Inlined to avoid required HTTP connection: h5bp.com/r - ========================================================================== */ - -@media print { - * { background: transparent !important; color: black !important; box-shadow:none !important; text-shadow: none !important; filter:none !important; -ms-filter: none !important; } /* Black prints faster: h5bp.com/s */ - a, a:visited { text-decoration: underline; } - a[href]:after { content: " (" attr(href) ")"; } - abbr[title]:after { content: " (" attr(title) ")"; } - .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } /* Don't show links for images, or javascript/internal links */ - pre, blockquote { border: 1px solid #999; page-break-inside: avoid; } - thead { display: table-header-group; } /* h5bp.com/t */ - tr, img { page-break-inside: avoid; } - img { max-width: 100% !important; } - @page { margin: 0.5cm; } - p, h2, h3 { orphans: 3; widows: 3; } - h2, h3 { page-break-after: avoid; } -} diff --git a/public/css/style.css b/public/css/style.css index 4501a32..030362c 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1,425 +1,529 @@ -html, body { - height: 100%; -} -#wrapper { - min-height: 100%; - height: auto !important; - height: 100%; - margin: 0 auto -21pt; /* the bottom margin is the negative value of the footer's height */ -} - -div#header { - background-color: #3d3d3d; - color: #ffffff; - padding-top: 3pt; - padding-bottom: 3pt; -} - -div#header span#welcome { - float: right; - padding: 0; - padding-right: 7pt; -} -div#header img { - float: right; - margin-top: -2pt; - padding-right: 7pt; -} - -div#header span, #nimbtn span { - margin: 0; - padding: 5pt; -} - -div#header a.right, #nimbtn a { - float: right; - - color: #ffffff; - margin-right: 6pt; -} - -div#header a { - color: #ffffff; -} - -div#header a:visited, #nimbtn a:visited { - color: #ffffff; -} - -div#header a:hover, #nimbtn a:hover { - text-decoration: none; -} - -#nimbtn a { - margin-left: 6pt; -} - -#nimbtn { - float: left; - background-color: #2a2a2a; - color: #ffffff; - padding-top: 3pt; - padding-bottom: 3pt; -} - -#content { - margin: 5pt; -} - -#content table#threads { - width: 100%; - border-collapse: separate; - text-align: center; - border: #ffffff solid 1px; - font-size: 10pt; -} - -#content table#threads th { - background-color: #5D5D5D; - background: -moz-linear-gradient(top, #5D5D5D, #4D4D4D); - background: -webkit-linear-gradient(top, #5D5D5D, #4d4d4d); - background: -o-linear-gradient(top, #5D5D5D, #4d4d4d); - color: #ffffff; - border-bottom: #2d2d2d solid 2px; - border-right: #2d2d2d solid 1px; -} - -#content table#threads tr:nth-child(even) { - background-color: #eee; -} - -#content table#threads td { - vertical-align: middle; - border-right: #9d9d9d solid 1px; - border-bottom: #9d9d9d solid 1px; -} - -#content table#threads>tbody>tr>td:first-child { - border-left: #9d9d9d solid 1px; - -} - -#content table#threads>tbody>tr>td:last-child { - border-right: #9d9d9d solid 1px; -} - -#content table#threads td:hover { - border-right-color: #9d9d9d; -} - -#content table#threads td.topic { - text-align: left; - padding: 5pt; -} -#content table#threads td.author { - width: 10%; -} -#content table#threads td.posts { - width: 10%; -} -#content table#threads td.views { - width: 10%; -} -#content table#threads td.lastreply { - width: 15%; -} - -span#rss { - margin: 5pt; - font-size: 9pt; -} - -img.rssfeed { - width: 1em; - height: 1em; - padding-right:3pt; - margin-top:-1pt; - -} - -#whoisonline { - margin: 5pt; - font-size: 9pt; -} - -#whoisonline .wioHeader { - background-color: #5D5D5D; - background: -moz-linear-gradient(top, #5D5D5D, #4D4D4D); - background: -webkit-linear-gradient(top, #5D5D5D, #4d4d4d); - background: -o-linear-gradient(top, #5D5D5D, #4d4d4d); - color: #ffffff; - border-bottom: #2d2d2d solid 1px; - padding: 3px; - padding-left: 5pt; -} - -#whoisonline .content { - border: #9d9d9d solid 1px; - border-top: #2D2D2D solid 1px; - padding: 5pt; -} - -#whoisonline .content hr { - margin-top: 5pt; - margin-bottom: 5pt; -} - -#footerPush { - height: 24pt; -} - -#footer { - background-color: #5D5D5D; - background: -moz-linear-gradient(top, #5D5D5D, #4D4D4D); - background: -webkit-linear-gradient(top, #5D5D5D, #4d4d4d); - background: -o-linear-gradient(top, #5D5D5D, #4d4d4d); - color: #ffffff; - - padding-top: 1.5pt; - padding-bottom: 1.5pt; - padding-left: 5px; - padding-right: 5px; - height: 18pt; -} - -#footer a:link, #footer a:visited { - color: #ffffff; -} - -#footer a:hover { - text-decoration: none; -} - -#topbar { - margin: 5pt; -} - -div#topbar span#rss { - float: right; - padding: 0; - padding-right: 7pt; -} - -#content .post { - border: #4d4d4d solid 2px; - width: 100%; - margin-bottom: 5pt; -} - -#content .post th { - background-color: #5D5D5D; - background: -moz-linear-gradient(top, #5D5D5D, #4D4D4D); - background: -webkit-linear-gradient(top, #5D5D5D, #4d4d4d); - background: -o-linear-gradient(top, #5D5D5D, #4d4d4d); - color: #ffffff; - padding-left: 5pt; - padding-right: 5pt; - padding-top: 3pt; - padding-bottom: 3pt; - text-align: left; - font-size: 9pt; -} - -#content .post .left { - border-left: #4d4d4d solid 2px; - background-color: #eee; - padding: 7pt; - width: 15%; - height: auto; -} - -#content .post .left hr { - margin: 0; - margin-bottom: 5pt; - margin-top: 2pt; -} - -#content .post .content { - padding: 6pt; -} - -div#replywrapper { - width: 70%; - margin-left: auto; - margin-right: auto; - border: #4d4d4d solid 2px; -} - -div#replywrapper div#replytop { - background-color: #5D5D5D; - background: -moz-linear-gradient(top, #5D5D5D, #4D4D4D); - background: -webkit-linear-gradient(top, #5D5D5D, #4d4d4d); - background: -o-linear-gradient(top, #5D5D5D, #4d4d4d); - font-size: 9pt; - color: #ffffff; - padding: 5pt; - -} - -div#replywrapper form textarea { - width: 99%; -} - -div#replywrapper form > input:first-child { - width: 80%; -} - -div#replywrapper form { - padding: 8pt; -} - -div#pagenumbers { - font-size: 11pt; - height: 21px; - margin: 5.9pt; - padding: 2pt; - padding-left: 4pt; - padding-right: 4pt; - border-top: 1px solid #9d9d9d; - border-bottom: 1px solid #9d9d9d; - background-color: #eee; -} - -div#pagenumbers div.left { - float: left; -} - -div#pagenumbers div.middle { - text-align: center; -} - -div#pagenumbers div.middle a, div#pagenumbers div.middle span { - padding-right: 4pt; -} - -div#pagenumbers div.middle span { - font-weight: bold; -} - -div#pagenumbers div.left span, div#pagenumbers div.left a { - padding-right: 8pt; -} - -div#pagenumbers div.right span, div#pagenumbers div.right a { - padding-left: 8pt; -} - - -div#pagenumbers div.right { - float: right; -} - -#content #threads .localnums { - display: inline; - float: right; - border-top: 1px solid #9d9d9d; - border-bottom: 1px solid #9d9d9d; - padding-left: 4pt; - padding-right: 4pt; -} - -#content #threads .localnums a:first-child { - padding-left: 0; -} - -#content #threads .localnums a { - padding-left: 6pt; - text-decoration: none; -} - -#content #threads .localnums span { - padding-left: 3pt; - margin-right: -3pt; -} - -#profile { - margin-left: 1.30em; -} - -#profile #left { - width: 250px; -} - -#profile #left #info table th { - text-align: left; - padding-right: 0.5em; -} - -#profile #left #info table { - font-size: 10.5pt; -} - - -#profile #left #avatar { - border-bottom: 2px solid #6d6d6d; - padding-bottom: 0.75em; - margin-bottom: 0.50em; -} - -/* For RST nimrod syntax highlighter */ -span.DecNumber {color: blue} -span.BinNumber {color: blue} -span.HexNumber {color: blue} -span.OctNumber {color: blue} -span.FloatNumber {color: blue} -span.Identifier {color: black} -span.Keyword {font-weight: bold} -span.StringLit {color: blue} -span.LongStringLit {color: blue} -span.CharLit {color: blue} -span.EscapeSequence {color: black} -span.Operator {color: black} -span.Punctation {color: black} -span.Comment, span.LongComment {font-style:italic; color: green} -span.RegularExpression {color: DarkViolet} -span.TagStart {color: DarkViolet} -span.TagEnd {color: DarkViolet} -span.Key {color: blue} -span.Value {color: black} -span.RawData {color: blue} -span.Assembler {color: blue} -span.Preprocessor {color: DarkViolet} -span.Directive {color: DarkViolet} -span.Command, span.Rule, span.Hyperlink, span.Label, span.Reference, -span.Other {color: black} - -/* Buttons */ -a.button { - border-radius: 2px 2px 2px 2px; - background: -moz-linear-gradient(top, #f7f7f7, #ebebeb); - background: -webkit-linear-gradient(top, #f7f7f7, #ebebeb); - background: -o-linear-gradient(top, #f7f7f7, #ebebeb); - text-decoration: none; - color: #3d3d3d; - padding: 5px; - border: solid 1px #9d9d9d; - display: inline-block; - position: relative; - text-align: center; - font-size: small; -} - -a.button.left { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} - -a.button.middle { - border-radius: 0; - border-left: 0; -} - -a.button.right { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - border-left: 0; -} - -a.button:hover { - background: -moz-linear-gradient(top, #0099c7, #0294C1); - background: -webkit-linear-gradient(top, #0099c7, #0294C1); - background: -o-linear-gradient(top, #0099c7, #0294C1); - border: solid 1px #077A9C; - color: #ffffff; -} +* { cursor:default; } +a, a * { cursor:pointer; } + +html { margin:0; overflow-x:auto; } +body { + overflow-x:hidden; + min-width:1030px; + margin:0; + font:13pt "arial"; + background:#152534 url("/images/bg.jpg") no-repeat fixed center top; } + +pre { color:#5997AF;} +pre, pre * { cursor:text; } +pre .cmt { color:#6D6D6D; font-style:italic; } +pre .kwd { color:#43A8CF; font-weight:bold; } +pre .typ { color:#128B7D; font-weight:bold; } +pre .atr { color:#128B7D; font-weight:bold; font-style:italic; } +pre .def { color:#CAD6E4; font-weight:bold; font-style:italic; } +pre .prg { color:#854D6A; font-weight:bold; font-style:italic; } +pre .val { color:#8AB647; font-style:italic; } +pre .tab { border-left:1px dotted rgba(67,168,207,0.4); } +pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } + +.tall { height:100%; } +.pre { padding:0 5px; font:11pt monospace; background:rgba(255,255,255,.15); 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; 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 { + 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; } + +#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; } + + #slideshow { position:absolute; top:10px; left:10px; width:700px; } + #slideshow > div { visibility:hidden; opacity:0; position:absolute; transition:visibility 0s linear 1s, opacity 1s ease-in-out; } + #slideshow > div.active { visibility:visible; opacity:1; transition-delay:0s; } + #slideshow > div.init { transition-delay:0s; } + #slideshow-nav { z-index:3; position:absolute; top:110px;; right:-12px; } + #slideshow-nav > div { margin:5px 0; width:23px; height:23px; background:url("/images/slideshow-nav.png") no-repeat; } + #slideshow-nav > div:hover { background-image:url("/images/slideshow-nav_active.png"); opacity:0.5; } + #slideshow-nav > div.active { background-image:url("/images/slideshow-nav_active.png"); opacity:1; } + + #slide0 { margin:30px 0 0 10px; } + #slide0 > div { float:left; width:320px; font:10pt monospace; } + #slide0 > div:first-child { margin:0 40px 0 0; } + #slide0 > div > h2 { margin:0 0 5px 0; color:rgba(162,198,223,.78); } + #slide0 > div > pre { + margin:0; + padding:15px 10px; + line-height:14pt; + background:rgba(0,0,0,.4); + border-left:8px solid rgba(0,0,0,.3); + box-shadow:1px 2px 16px rgba(28,180,236,.4); } + + #slide1 { margin-top:50px; } + #slide1 > p { + padding:40px 20px 0 20px; + font-style:italic; + color:rgba(162,198,223,.78); + letter-spacing:1px; + line-height:25pt; + background:url("/images/quotes.png") top left no-repeat; } + #slide1 > div { + float:right; + margin-right:40px; + font-style:italic; + font-weight:bold; + color:rgba(93,155,199,.44); } + + #sidebar { + z-index:2; + position:absolute; + top:5px; right:0; + width:275px; + height:726px; + padding:210px 0 0 0; + background:url("/images/sidebar.png") no-repeat; } + #sidebar > h3 { margin:0 30px 0 30px; color:rgba(255,255,255,.5); } + #sidebar > h3.blue { color:rgba(28,180,236,.5); } + #sidebar-links, + #sidebar-news { + margin:10px 30px 50px 30px; + padding:10px 0; + background:rgba(0,0,0,.6); } + #sidebar-links { box-shadow:1px 2px 12px rgba(255,255,255,.4); } + #sidebar-news { box-shadow:1px 2px 12px rgba(28,180,236,.6); } + #sidebar-links > a { + display:block; + margin-left:15px; + padding:12px 20px 12px 45px; + font-weight:bold; + text-decoration:none; + letter-spacing:1px; + color:rgba(255,255,255,.4); + transition: + color 0.1s ease-in-out, + text-shadow 0.2s ease-in-out; } + #sidebar-news > a { transition: color 0.3s ease-in-out; } + #sidebar-news > a > h4 { transition: color 0.1s ease-in-out, text-shadow 0.2s ease-in-out; } + #sidebar-links > a:hover { color:#fff; text-shadow:0 0 6px #fff; } + #sidebar-news > a { display:block; padding:15px; color:rgba(255,255,255,.4); text-decoration:none; } + #sidebar-news > a > h4 { margin:0 0 5px 0; color:rgba(28,180,236,.5); } + #sidebar-news > a:hover > h4 { margin:0 0 5px 0; color:rgba(28,180,236,.8); text-shadow:0 0 6px rgba(28,180,236,.6); } + #sidebar-news > a:hover { color:rgba(255,255,255,1); } + #sidebar-news > a.blue { color:rgba(28,180,236,.5); font-weight:bold; } + #sidebar-news > a.blue:hover { color:#fff; } + + #links-forum { background:url("/images/more-links_forum.png") no-repeat left center; } + #links-github { background:url("/images/more-links_github.png") no-repeat left center; } + #links-editors { background:url("/images/more-links_editors.png") no-repeat left center; } + #links-nimbuild { background:url("/images/more-links_nimbuild.png") no-repeat left center; } + + #overview-bg { + position:fixed; + top:0; + bottom:0; + left:0; + width:280px; + background:rgba(0,0,0,0.25); } + #overview { + z-index:3; + position:fixed; + overflow:auto; + top:115px; + bottom:20px; + left:20px; + width:245px; } + #overview::-webkit-scrollbar { width:5px; } + #overview::-webkit-scrollbar-track { border-radius:2px; background:rgba(255,255,255,.03); } + #overview::-webkit-scrollbar-thumb { border-radius:2px; background:rgba(28,179,236,.5); } + #overview > div { overflow:auto; margin-bottom:40px; } + #overview a { + display:block; + padding:0 10px; + margin:2px 5px 2px 0; + color:rgba(255,255,255,.6); + background:rgba(255,255,255,0.03); + border-radius:2px; + letter-spacing:1px; + text-decoration:none; } + #overview a:hover { color:#fff; background:rgba(255,255,255,0.05); } + #overview > .types a { border-left:2px solid rgba(28,179,236,.4); } + #overview > .procs a { border-left:2px solid rgba(255,223,53,.4); } + #overview > .iters a { border-left:2px solid rgba(255,134,53,.4); } + #overview > div > h4 { + margin:0 5px 10px 0; + padding:5px 10px; + letter-spacing:1px; + color:#fff; + border-left:2px solid #fff; + border-radius:2px; + background:rgba(255,255,255,0.1); } + #overview > .types h4 { color:#1cb3ec; border-color:#1cb3ec; } + #overview > .procs h4 { color:#ffdf35; border-color:#ffdf35; } + #overview > .iters h4 { color:#ff8635; border-color:#ff8635; } + #overview h5 { + color:rgba(28,179,236,.6); + margin:10px 0 5px 0; + padding:5px 5px; + letter-spacing:1px; } + +#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; } + + #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; line-height:150%; } + #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:rgba(0,0,0,.8); } + #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.docs #content > div { margin-top:40px; padding-top:40px; border-top:1px dashed rgba(0,0,0,.25); } + #body.docs #content > div:first-child { margin-top:0; padding-top:0; border:none; } + #body.docs #content > div > h3 { + color:#fff; + margin:0 0 10px 0; + padding:10px 20px; + letter-spacing:1px; + border-left:8px solid #fff; + border-radius:3px; + background:rgba(0,0,0,.7); + box-shadow:1px 3px 12px rgba(0,0,0,.4); } + #body.docs #content > #types-wrap > h3 { color:#1cb3ec; border-color:#1cb3ec; } + #body.docs #content > #procs-wrap > h3 { color:#ffdf35; border-color:#ffdf35; } + #body.docs #content > #iters-wrap > h3 { color:#ff8635; border-color:#ff8635; } + #body.docs #content > div > div > div { + overflow:auto; + margin:10px 0; + border-left:8px solid #fff; + border-radius:3px; + background:rgba(0,0,0,.1); } + #body.docs #content > #types-wrap > div > div { border-color:rgba(28,179,236,.5); } + #body.docs #content > #procs-wrap > div > div { border-color:rgba(255,223,53,.5); } + #body.docs #content > #iters-wrap > div > div { border-color:rgba(255,134,53,.5); } + #body.docs #content > #procs-wrap > div > div.overload-head { margin-bottom:0; } + #body.docs #content > #procs-wrap > div > div.overload-tail { margin-top:0; border-top:1px dashed rgba(255,223,53,.5); } + #body.docs #content > #procs-wrap > div > div.overload { margin-top:0; margin-bottom:0; border-top:1px dashed rgba(255,223,53,.5); } + #body.docs #content > #iters-wrap > div > div.overload-head { margin-bottom:0; } + #body.docs #content > #iters-wrap > div > div.overload-tail { margin-top:0; border-top:1px dashed rgba(255,134,53,.5); } + #body.docs #content > #iters-wrap > div > div.overload { margin-top:0; margin-bottom:0; border-top:1px dashed rgba(255,134,53,.5); } + #body.docs #content > div > div > p { margin:20px 10px 10px 10px; } + + #body.docs #content > div > div > div > div { float:left; } + #body.docs #content > div > div > div > div.head { width:60%; } + #body.docs #content > div > div > div > div.data { width:40%; } + + #body.docs #content > h1 > .symbol { + padding:0 8px; + border-radius:5px; + background:rgba(206,218,233,.4); } + + #body.docs #content > div > div > div > div.head > .sign { + margin:0 10px 5px 10px; + padding:10px 10px 0 10px; + font-weight:bold; + border-bottom:1px dashed rgba(0,0,0,.25); } + #body.docs #content > div > div > div > div.head > .desc { + padding:0 20px 10px 20px; + color:rgba(0,0,0,.75); } + #body.docs #content > div > #types > div > div.head > .sign > .symbol { + padding:0 5px; + border-radius:3px; + background:rgba(28,179,236,.4); } + #body.docs #content > div > #procs > div > div.head > .sign > .symbol { + padding:0 5px; + border-radius:3px; + background:rgba(255,223,53,.3); } + #body.docs #content > div > #iters > div > div.head > .sign > .symbol { + padding:0 5px; + border-radius:3px; + background:rgba(255,134,53,.3); } + + #body.docs #content > div > div > div > div.data > div { + margin:0 20px 5px 10px; + padding:10px 0 0 10px; + font-style:italic; + color:rgba(0,0,0,.6); + border-bottom:1px dashed rgba(0,0,0,.25); } + #body.docs #content > div > div > div > div.data > ul { margin:0; padding:0 10px; } + #body.docs #content > div > div > div > div.data > ul:last-child { margin-bottom:5px; padding-bottom:10px; } + #body.docs #content > div > div > div > div.data > ul .symbol { padding:0 5px; border-radius:3px; background:rgba(23,192,23,.25); } + #body.docs #content > div > div > div > div.data > ul.pragmas .symbol { background:rgba(106,50,145,.25); } + #body.docs #content > div > div > div > div.data > ul > li { margin:0; padding:0 10px; list-style:none; } + + #body.docs #content pre { + overflow:auto; + margin:10px 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); } + + #docs-sort { float:right; font-size:75%; } + #docs-sort > a { + cursor:default; + margin:0 0 0 10px; + padding:2px 10px; + border-radius:5px; + color:rgba(0,0,0,.25); + background:rgba(0,0,0,.1); + box-shadow:inset 0 1px 8px rgba(0,0,0,.4); } + #docs-sort > a:hover, + #docs-sort > a.active { color:#000; background:rgba(0,0,0,.2); } + + #talk-heads { overflow:auto; margin:0 8px 0 8px; } + #talk-heads > div { float:left; font-size:120%; font-weight:bold; } + #talk-heads > .topic { width:55%; } + #talk-heads > .detail { width:15%; } + #talk-heads > .author { width:15%; } + #talk-heads > .reply { 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 > .author > div { margin-right:0; } + + #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; + background:rgba(0,0,0,0.1); } + #talk-thread > div:nth-child(odd) { background:rgba(255,255,255,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; } + #talk-thread > div > div > div, + #talk-threads > div > div > div { margin:10px 20px; } + #talk-threads > div > .topic { width:55%; } + #talk-threads > div > .reply { width:15%; overflow:hidden; } + #talk-threads > div > .detail { width:15%; overflow:hidden; } + #talk-thread > div > .author, + #talk-threads > div > .author { + position:absolute; + right:0; + top:0; + bottom:0; + width:15%; + overflow:hidden; + background:rgba(0,0,0,0.8); } + #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 > div > a { font-weight:bold; } + #talk-threads > div > .detail > div { float:left; margin:0; } + #talk-threads > div > .detail > div > div { margin-left:20px; padding:10px 10px 10px 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; } + #talk-threads > div > .detail > div:last-child > div { background:url("/images/forum-posts.png") no-repeat left; } + + #talk-thread > div { margin:20px 0; min-height:150px; box-shadow:1px 3px 12px rgba(0,0,0,.4) } + #talk-thread > div > .author > div > .avatar { margin-top:20px; } + #talk-thread > div > .author > div > .avatar > img { box-shadow:0 0 12px #1cb3ec; } + #talk-thread > div > .author > div > .name { } + #talk-thread > div > .topic { width:85%; padding-bottom:10px; } + #talk-thread > div > .topic pre { + 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); } + + #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 > .user, + #talk-info > .user { width:20%; background:rgba(0,0,0,.2); } + #talk-info > .user > div > .reply { font-weight:bold; padding-left:22px; background:url("/images/forum-reply.png") no-repeat left; } + #talk-head > div > div, + #talk-info > div > div { padding:5px 20px; } + #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; } + + .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; } diff --git a/public/images/bg.jpg b/public/images/bg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4e33a79ce15aadf50fc685d7b0f6e827821d2925 GIT binary patch literal 94894 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 IKlbGR0F%nD#{d8T literal 0 HcmV?d00001 diff --git a/public/images/forum-posts.png b/public/images/forum-posts.png new file mode 100644 index 0000000000000000000000000000000000000000..a9ef428441a837797a614fd3a93b410c8a3341a0 GIT binary patch literal 206 zcmeAS@N?(olHy`uVBq!ia0vp^0w6XA8<1SE`<)7qVoUONcVYMsf(!O8p9~b?Ebxdd z2GVaqm~nOf$jg8%>k literal 0 HcmV?d00001 diff --git a/public/images/forum-views.png b/public/images/forum-views.png new file mode 100644 index 0000000000000000000000000000000000000000..93ef8ed05a24a3b0a4d621d0a7f996159a537932 GIT binary patch literal 424 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 a)U0iX&8^H=;tz)}0E%)dvenYCkpBZy4QF%! literal 0 HcmV?d00001 diff --git a/public/images/glow-line.png b/public/images/glow-line.png new file mode 100644 index 0000000000000000000000000000000000000000..6607bdee90e9b89858ca7cd0c243c7830c71a4e1 GIT binary patch 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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 To*z!d00000NkvXXu0mjfm4kai literal 0 HcmV?d00001 diff --git a/public/images/head-link.png b/public/images/head-link.png new file mode 100644 index 0000000000000000000000000000000000000000..d97cba5b81c36e9e6ac3d4faa6a1e652c75ad179 GIT binary patch literal 203 zcmeAS@N?(olHy`uVBq!ia0vp^_CU|gW!U_%O?XxI14-? zi-9_Tvb@(Boit`w00r4gJbhi+?{V;Ot6TrPJ6#PZ6yfRO7~*mK?G#I{0|q=S>CJQh z@4wFRM6EF)Xveg2FHX_L+&zZs3r#1?WXfA0{=r+JK~eMildr)6u770n>gU{;n9pjy u=j|zX$DcRep7`-3Qe*= zK;!}PAc~$MK%O7~0X>6WKi zO%f}ZVJx@C0OJ4yc=hL{v1Pb?`D)2zh`2_aA`B!R3EPUifdR&Em+83qWvWt{awqv0 zvyaZxrvSwGZsSGpy^a8UA&nNHbeH4Fv!nV=N#=wAB$fi0q$TmezkTL>K9sXP zJ@K%8Q@yX0c5;qZa&wIBAukkocGKLjiGsUujauW;eaZ9cs!xrX3Itr>?-@2*rSB;h zhf0fYBd4D%^p%?>#a87V3EUKta-er6oh$QtUUqa}U1u)3fs~D39z;e9}3asB#fQhN4_&I0)!>j6jhp)$1*JrhJ3X{oOUlfgkh|;9*!FG6@ z0{V2I*!;j&3s^RIFxq7Mkrd^kXcUvlTj%r?{}0{T>lxpMZ2vzvXRy0_9F2NW<6fEZ zJJ^i1NB*FuvH7u9_7Ap2Oi4N?d)25H_3rL*aCU|v85g93;jrB*y%XrjWG~I_2hcZ& zgqhn~HEcE{FtZTKf_EmJ2fTLdW>($Y4G+3gPRPYWQsZ!NFt4f)R{{VX7t;>@GlPLfI002ovPDHLkV1ijrcCi2e literal 0 HcmV?d00001 diff --git a/public/images/head.png b/public/images/head.png new file mode 100644 index 0000000000000000000000000000000000000000..009f86728e96fb18c580c73b5b8cfed1462ac8d1 GIT binary patch literal 171 zcmeAS@N?(olHy`uVBq!ia0vp^96+4H!3HFETNbVYQfx`y?k)`fL2$v|<&%LToCO|{ z#Xud`L734=V|E2lkiEpy*OmPqha8W*vHUTuI-rn+r;B5V$MLrp6!{nwcvud$etB4{ z(X>3_!rFryXU~x?W%lS&^JQvuP!Ql?Iq!I9YC?I!&m|5Ylr6TrV&=1ZAfW{`gTd3) K&t;ucLK6VpDKi8B literal 0 HcmV?d00001 diff --git a/public/images/logo.png b/public/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..85d3d2e5169217dbff6c9e0b5407b1bd3507a055 GIT binary patch literal 116562 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ȇ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 hRn&Fjcd?lKrNCUC$>{g4vxR^!oc2Y`V@ Date: Fri, 31 Oct 2014 15:45:02 +0000 Subject: [PATCH 044/560] Implemented new design for thread view. --- forms.tmpl | 68 ++++++++++++++++++---------------- main.tmpl | 21 +++++++++-- public/css/style.css | 5 ++- public/images/forum-reply.png | Bin 0 -> 490 bytes 4 files changed, 59 insertions(+), 35 deletions(-) create mode 100644 public/images/forum-reply.png diff --git a/forms.tmpl b/forms.tmpl index 2b31609..c51c41e 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -110,39 +110,45 @@ # const userEmail = 6 # result = "" # count = 0 -# for row in fastRows(db, query, threadId, $((c.pageNum-1) * PostsPerPage), $PostsPerPage): +# let posts = getAllRows(db, query, threadId, $((c.pageNum-1) * PostsPerPage), $PostsPerPage) +

+
+# for row in posts: # inc(count) - - - - - - - - - -
- ${xmlEncode(%postHeader)} - ${xmlEncode(%postCreation)} -
- #let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(%userName) - ${xmlEncode(%userName)} -
- ${genGravatar(%userEmail)} - #if c.userId == %postAuthor and c.currentPost.subject.len == 0: -
${UrlButton(c, "Edit post", c.genThreadUrl(%postId, "edit"))} - #elif c.isAdmin and c.currentPost.subject.len == 0: -
- ${UrlButton(c, "Edit post", c.genThreadUrl(%postId, "edit"))} - #end if -
- #try: - ${(%postContent).rstToHtml} - #except EParseError: - # c.errorMsg = getCurrentExceptionMsg() - #end -
+
+
+
+ #try: + ${(%postContent).rstToHtml} + #except EParseError: + # c.errorMsg = getCurrentExceptionMsg() + #end +
+
+
+
+ #let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(%userName) +
${genGravatar(%userEmail)}
+ marcianx +

${xmlEncode(%postCreation)}

+ #if c.userId == %postAuthor and c.currentPost.subject.len == 0: +
Edit post + #elif c.isAdmin and c.currentPost.subject.len == 0: +
Edit post + #end if +
+
+
# end for +
#end proc # # diff --git a/main.tmpl b/main.tmpl index 8a21565..4789387 100644 --- a/main.tmpl +++ b/main.tmpl @@ -79,10 +79,25 @@
+
+ ${genPagenumNav(c, stats)} +
+ #else: +
+
+
+ ${genPagenumNav(c, stats)} +
+
+
+
+ #let replyUri = c.req.makeUri(c.req.path & "?action=reply#reply") + Reply +
+
+
#end if -
- ${genPagenumNav(c, stats)} -
+ diff --git a/public/css/style.css b/public/css/style.css index 030362c..e3aa313 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -440,6 +440,7 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #talk-thread > div > .author > div > .avatar { margin-top:20px; } #talk-thread > div > .author > div > .avatar > img { box-shadow:0 0 12px #1cb3ec; } #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; } #talk-thread > div > .topic pre { overflow:auto; @@ -465,9 +466,11 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #talk-info > div { float:left; } #talk-head > .info, #talk-info > .info { width:80%; } + #talk-info > .info-post { width: 85%; } #talk-head > .user, #talk-info > .user { width:20%; background:rgba(0,0,0,.2); } - #talk-info > .user > div > .reply { font-weight:bold; padding-left:22px; background:url("/images/forum-reply.png") no-repeat left; } + #talk-info > .user-post { width: 15%; background:rgba(0,0,0,.2); } + #talk-info > .user-post > div > .reply { font-weight:bold; padding-left:22px; background:url("/images/forum-reply.png") no-repeat left; } #talk-head > div > div, #talk-info > div > div { padding:5px 20px; } #talk-head > .detail > div { float:left; margin:0; } diff --git a/public/images/forum-reply.png b/public/images/forum-reply.png new file mode 100644 index 0000000000000000000000000000000000000000..1c1cfb72f8cb353498e86d8560fb80af725e1b02 GIT binary patch literal 490 zcmVK~y-6oszLHLs1lmzw@+RYB1=FZY`|@krboRC?O&?gw52& zK;j=jguz+~gSCS}EH=7nsW7xJ5tDjNL?nbn-!U|;=C#$vFFC`#=brDJ-^oQ)amQ88 zUcRvkW}}=t3|zX-7ncN0e03OgxkmET9x^$55#a#p!ObM9S}wNi1(d@|u_|XGdMRq8 z@p)6RP}gfyK-#kBMLGJT^NR@aSj0T817z}rX;n$KIrfS3ZLEw9bU#%t3`J82cWo2{ zOm%pii~IXxXB99K=_!IUfpFUnL8154r@K`Pze_9xz&J-422f=WgRm$uAmj##%4DoR za9Z{4o}}cH?a_!iGZHn!3bR1I7T}0(fopNi?2Jaur1NGDA+la}0Rn^B?6bdWp;j5I z5ODnZJ{Amq)a7#aD#8wc7>qXrv{EK-Zvtx|@wUHgt*ofIf$|8L{{qZG3D`$?@U(p0 gM2^Rsz$x|p05Ht1-T&X94*&oF07*qoM6N<$f`5I?%K!iX literal 0 HcmV?d00001 From 7b6e629bfc25a2bb204917e813c4a165b3951387 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 4 Nov 2014 13:52:48 +0000 Subject: [PATCH 045/560] Small fixes for styling and reply button presence. --- forms.tmpl | 4 ++-- forum.nim | 4 ++++ main.tmpl | 2 +- public/css/style.css | 1 + 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index c51c41e..8bf1fe5 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -112,13 +112,13 @@ # count = 0 # let posts = getAllRows(db, query, threadId, $((c.pageNum-1) * PostsPerPage), $PostsPerPage)
-
# for row in posts: diff --git a/forum.nim b/forum.nim index 2247bf0..2e3148a 100644 --- a/forum.nim +++ b/forum.nim @@ -427,6 +427,10 @@ proc login(c: var TForumData, name, pass: string): bool = else: return c.setError("password", "Login failed!") +proc hasReplyBtn(c: var TForumData): bool = + result = c.req.pathInfo != "/donewthread" and c.req.pathInfo != "/doreply" + return c.threadId >= 0 and result + proc genActionMenu(c: var TForumData): string = result = "" var btns: seq[TStyledButton] = @[] diff --git a/main.tmpl b/main.tmpl index 4789387..d5417cb 100644 --- a/main.tmpl +++ b/main.tmpl @@ -82,7 +82,7 @@
${genPagenumNav(c, stats)}
- #else: + #elif hasReplyBtn(c):
diff --git a/public/css/style.css b/public/css/style.css index e3aa313..e7481d5 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -469,6 +469,7 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #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 > div > .reply { font-weight:bold; padding-left:22px; background:url("/images/forum-reply.png") no-repeat left; } #talk-head > div > div, From dd61f5bdbf2ddb7268588b99cd4aa6fa571e5f26 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 6 Nov 2014 16:35:25 +0000 Subject: [PATCH 046/560] Implemented side bar. --- forms.tmpl | 8 +- main.tmpl | 49 +++--- public/css/style.css | 346 +++++++++++++++---------------------------- 3 files changed, 157 insertions(+), 246 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index 8bf1fe5..2e5d9aa 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -158,8 +158,12 @@
-
- ${topText} +
+
+
+ $topText +
+
${FieldValid(c, "subject", "Subject:")} diff --git a/main.tmpl b/main.tmpl index d5417cb..2a89a6f 100644 --- a/main.tmpl +++ b/main.tmpl @@ -55,29 +55,9 @@
- #if c.loggedIn: - #let profileUrl = c.req.makeUri("profile/", false) & - # xmlEncode(c.username) - Welcome ${c.username}! -  |  - - #end if ${c.genListOnline(stats)}
-
-
- #if c.loggedIn: - new thread -  |  - logout - #else: - login -  |  - register - #end if -
-
${genPagenumNav(c, stats)} @@ -99,6 +79,35 @@ #end if +
diff --git a/public/css/style.css b/public/css/style.css index e7481d5..8cf8418 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1,5 +1,4 @@ -* { cursor:default; } a, a * { cursor:pointer; } html { margin:0; overflow-x:auto; } @@ -98,134 +97,7 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } width:3px; height:844px; background:url("/images/glow-line-vert.png") no-repeat; } - - #slideshow { position:absolute; top:10px; left:10px; width:700px; } - #slideshow > div { visibility:hidden; opacity:0; position:absolute; transition:visibility 0s linear 1s, opacity 1s ease-in-out; } - #slideshow > div.active { visibility:visible; opacity:1; transition-delay:0s; } - #slideshow > div.init { transition-delay:0s; } - #slideshow-nav { z-index:3; position:absolute; top:110px;; right:-12px; } - #slideshow-nav > div { margin:5px 0; width:23px; height:23px; background:url("/images/slideshow-nav.png") no-repeat; } - #slideshow-nav > div:hover { background-image:url("/images/slideshow-nav_active.png"); opacity:0.5; } - #slideshow-nav > div.active { background-image:url("/images/slideshow-nav_active.png"); opacity:1; } - - #slide0 { margin:30px 0 0 10px; } - #slide0 > div { float:left; width:320px; font:10pt monospace; } - #slide0 > div:first-child { margin:0 40px 0 0; } - #slide0 > div > h2 { margin:0 0 5px 0; color:rgba(162,198,223,.78); } - #slide0 > div > pre { - margin:0; - padding:15px 10px; - line-height:14pt; - background:rgba(0,0,0,.4); - border-left:8px solid rgba(0,0,0,.3); - box-shadow:1px 2px 16px rgba(28,180,236,.4); } - - #slide1 { margin-top:50px; } - #slide1 > p { - padding:40px 20px 0 20px; - font-style:italic; - color:rgba(162,198,223,.78); - letter-spacing:1px; - line-height:25pt; - background:url("/images/quotes.png") top left no-repeat; } - #slide1 > div { - float:right; - margin-right:40px; - font-style:italic; - font-weight:bold; - color:rgba(93,155,199,.44); } - - #sidebar { - z-index:2; - position:absolute; - top:5px; right:0; - width:275px; - height:726px; - padding:210px 0 0 0; - background:url("/images/sidebar.png") no-repeat; } - #sidebar > h3 { margin:0 30px 0 30px; color:rgba(255,255,255,.5); } - #sidebar > h3.blue { color:rgba(28,180,236,.5); } - #sidebar-links, - #sidebar-news { - margin:10px 30px 50px 30px; - padding:10px 0; - background:rgba(0,0,0,.6); } - #sidebar-links { box-shadow:1px 2px 12px rgba(255,255,255,.4); } - #sidebar-news { box-shadow:1px 2px 12px rgba(28,180,236,.6); } - #sidebar-links > a { - display:block; - margin-left:15px; - padding:12px 20px 12px 45px; - font-weight:bold; - text-decoration:none; - letter-spacing:1px; - color:rgba(255,255,255,.4); - transition: - color 0.1s ease-in-out, - text-shadow 0.2s ease-in-out; } - #sidebar-news > a { transition: color 0.3s ease-in-out; } - #sidebar-news > a > h4 { transition: color 0.1s ease-in-out, text-shadow 0.2s ease-in-out; } - #sidebar-links > a:hover { color:#fff; text-shadow:0 0 6px #fff; } - #sidebar-news > a { display:block; padding:15px; color:rgba(255,255,255,.4); text-decoration:none; } - #sidebar-news > a > h4 { margin:0 0 5px 0; color:rgba(28,180,236,.5); } - #sidebar-news > a:hover > h4 { margin:0 0 5px 0; color:rgba(28,180,236,.8); text-shadow:0 0 6px rgba(28,180,236,.6); } - #sidebar-news > a:hover { color:rgba(255,255,255,1); } - #sidebar-news > a.blue { color:rgba(28,180,236,.5); font-weight:bold; } - #sidebar-news > a.blue:hover { color:#fff; } - - #links-forum { background:url("/images/more-links_forum.png") no-repeat left center; } - #links-github { background:url("/images/more-links_github.png") no-repeat left center; } - #links-editors { background:url("/images/more-links_editors.png") no-repeat left center; } - #links-nimbuild { background:url("/images/more-links_nimbuild.png") no-repeat left center; } - - #overview-bg { - position:fixed; - top:0; - bottom:0; - left:0; - width:280px; - background:rgba(0,0,0,0.25); } - #overview { - z-index:3; - position:fixed; - overflow:auto; - top:115px; - bottom:20px; - left:20px; - width:245px; } - #overview::-webkit-scrollbar { width:5px; } - #overview::-webkit-scrollbar-track { border-radius:2px; background:rgba(255,255,255,.03); } - #overview::-webkit-scrollbar-thumb { border-radius:2px; background:rgba(28,179,236,.5); } - #overview > div { overflow:auto; margin-bottom:40px; } - #overview a { - display:block; - padding:0 10px; - margin:2px 5px 2px 0; - color:rgba(255,255,255,.6); - background:rgba(255,255,255,0.03); - border-radius:2px; - letter-spacing:1px; - text-decoration:none; } - #overview a:hover { color:#fff; background:rgba(255,255,255,0.05); } - #overview > .types a { border-left:2px solid rgba(28,179,236,.4); } - #overview > .procs a { border-left:2px solid rgba(255,223,53,.4); } - #overview > .iters a { border-left:2px solid rgba(255,134,53,.4); } - #overview > div > h4 { - margin:0 5px 10px 0; - padding:5px 10px; - letter-spacing:1px; - color:#fff; - border-left:2px solid #fff; - border-radius:2px; - background:rgba(255,255,255,0.1); } - #overview > .types h4 { color:#1cb3ec; border-color:#1cb3ec; } - #overview > .procs h4 { color:#ffdf35; border-color:#ffdf35; } - #overview > .iters h4 { color:#ff8635; border-color:#ff8635; } - #overview h5 { - color:rgba(28,179,236,.6); - margin:10px 0 5px 0; - padding:5px 5px; - letter-spacing:1px; } + #body { z-index:1; position:relative; background:rgba(220,231,248,.6); } #body.docs { margin:0 40px 20px 320px; } @@ -290,101 +162,6 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #content a:hover { color:#fff; } #content ul { padding-left:20px; } #content li { margin-bottom:10px; text-align:justify; } - - #body.docs #content > div { margin-top:40px; padding-top:40px; border-top:1px dashed rgba(0,0,0,.25); } - #body.docs #content > div:first-child { margin-top:0; padding-top:0; border:none; } - #body.docs #content > div > h3 { - color:#fff; - margin:0 0 10px 0; - padding:10px 20px; - letter-spacing:1px; - border-left:8px solid #fff; - border-radius:3px; - background:rgba(0,0,0,.7); - box-shadow:1px 3px 12px rgba(0,0,0,.4); } - #body.docs #content > #types-wrap > h3 { color:#1cb3ec; border-color:#1cb3ec; } - #body.docs #content > #procs-wrap > h3 { color:#ffdf35; border-color:#ffdf35; } - #body.docs #content > #iters-wrap > h3 { color:#ff8635; border-color:#ff8635; } - #body.docs #content > div > div > div { - overflow:auto; - margin:10px 0; - border-left:8px solid #fff; - border-radius:3px; - background:rgba(0,0,0,.1); } - #body.docs #content > #types-wrap > div > div { border-color:rgba(28,179,236,.5); } - #body.docs #content > #procs-wrap > div > div { border-color:rgba(255,223,53,.5); } - #body.docs #content > #iters-wrap > div > div { border-color:rgba(255,134,53,.5); } - #body.docs #content > #procs-wrap > div > div.overload-head { margin-bottom:0; } - #body.docs #content > #procs-wrap > div > div.overload-tail { margin-top:0; border-top:1px dashed rgba(255,223,53,.5); } - #body.docs #content > #procs-wrap > div > div.overload { margin-top:0; margin-bottom:0; border-top:1px dashed rgba(255,223,53,.5); } - #body.docs #content > #iters-wrap > div > div.overload-head { margin-bottom:0; } - #body.docs #content > #iters-wrap > div > div.overload-tail { margin-top:0; border-top:1px dashed rgba(255,134,53,.5); } - #body.docs #content > #iters-wrap > div > div.overload { margin-top:0; margin-bottom:0; border-top:1px dashed rgba(255,134,53,.5); } - #body.docs #content > div > div > p { margin:20px 10px 10px 10px; } - - #body.docs #content > div > div > div > div { float:left; } - #body.docs #content > div > div > div > div.head { width:60%; } - #body.docs #content > div > div > div > div.data { width:40%; } - - #body.docs #content > h1 > .symbol { - padding:0 8px; - border-radius:5px; - background:rgba(206,218,233,.4); } - - #body.docs #content > div > div > div > div.head > .sign { - margin:0 10px 5px 10px; - padding:10px 10px 0 10px; - font-weight:bold; - border-bottom:1px dashed rgba(0,0,0,.25); } - #body.docs #content > div > div > div > div.head > .desc { - padding:0 20px 10px 20px; - color:rgba(0,0,0,.75); } - #body.docs #content > div > #types > div > div.head > .sign > .symbol { - padding:0 5px; - border-radius:3px; - background:rgba(28,179,236,.4); } - #body.docs #content > div > #procs > div > div.head > .sign > .symbol { - padding:0 5px; - border-radius:3px; - background:rgba(255,223,53,.3); } - #body.docs #content > div > #iters > div > div.head > .sign > .symbol { - padding:0 5px; - border-radius:3px; - background:rgba(255,134,53,.3); } - - #body.docs #content > div > div > div > div.data > div { - margin:0 20px 5px 10px; - padding:10px 0 0 10px; - font-style:italic; - color:rgba(0,0,0,.6); - border-bottom:1px dashed rgba(0,0,0,.25); } - #body.docs #content > div > div > div > div.data > ul { margin:0; padding:0 10px; } - #body.docs #content > div > div > div > div.data > ul:last-child { margin-bottom:5px; padding-bottom:10px; } - #body.docs #content > div > div > div > div.data > ul .symbol { padding:0 5px; border-radius:3px; background:rgba(23,192,23,.25); } - #body.docs #content > div > div > div > div.data > ul.pragmas .symbol { background:rgba(106,50,145,.25); } - #body.docs #content > div > div > div > div.data > ul > li { margin:0; padding:0 10px; list-style:none; } - - #body.docs #content pre { - overflow:auto; - margin:10px 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); } - - #docs-sort { float:right; font-size:75%; } - #docs-sort > a { - cursor:default; - margin:0 0 0 10px; - padding:2px 10px; - border-radius:5px; - color:rgba(0,0,0,.25); - background:rgba(0,0,0,.1); - box-shadow:inset 0 1px 8px rgba(0,0,0,.4); } - #docs-sort > a:hover, - #docs-sort > a.active { color:#000; background:rgba(0,0,0,.2); } #talk-heads { overflow:auto; margin:0 8px 0 8px; } #talk-heads > div { float:left; font-size:120%; font-weight:bold; } @@ -466,6 +243,7 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #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); } @@ -531,3 +309,123 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } width:202px; height:319px; background:url("/images/mascot.png") no-repeat; } + +article#content +{ + width: 75%; +} + +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: 20%; + + position: absolute; + right: 3%; + top: 5%; + height: 81%; + + 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 +{ + 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; +} + +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; + margin-left: 5pt; + width: 52.5%; +} + +div#sidebar .user .logout +{ + clear: left; + width: 52pt; + text-align: center; + margin-left: 0pt; +} + +div#sidebar .content .search +{ + text-align: center; + margin: auto; + display: block; + width: 95%; +} + +form +{ + + border-right: 8px solid rgba(0, 0, 0, 0.2); + + background-color: rgba(255, 255, 255, 0.1); + +} From 70b409e6e39e714d0304b99778efe16e296d5a63 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 8 Nov 2014 20:41:00 +0000 Subject: [PATCH 047/560] Design changes for thread view. --- forms.tmpl | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index 2e5d9aa..96b6c59 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -118,21 +118,11 @@ ${posts[0][postHeader]}
-
???
?
# for row in posts: # inc(count)
-
-
- #try: - ${(%postContent).rstToHtml} - #except EParseError: - # c.errorMsg = getCurrentExceptionMsg() - #end -
-
#let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(%userName) @@ -146,6 +136,15 @@ #end if
+
+
+ #try: + ${(%postContent).rstToHtml} + #except EParseError: + # c.errorMsg = getCurrentExceptionMsg() + #end +
+
# end for
From c1cf06c74790c801462730f61cde96d5148411ea Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 8 Nov 2014 23:42:23 +0000 Subject: [PATCH 048/560] Sidebar style adjustments. --- public/css/style.css | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index 8cf8418..35c9a4b 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -259,6 +259,7 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #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 { margin-left: 5pt; } .standout { padding:5px 30px; @@ -312,7 +313,8 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } article#content { - width: 75%; + width: 80%; + display: inline-block; } div#sidebar @@ -324,12 +326,11 @@ div#sidebar border-bottom: 8px solid rgba(0, 0, 0, 0.8); border-radius: 3px; - width: 20%; + width: 15%; + margin-top: 40px; - position: absolute; - right: 3%; - top: 5%; - height: 81%; + display: inline-block; + float: right; color: #FFF; } @@ -358,6 +359,7 @@ div#sidebar .content .button float: right; border-bottom: 2px solid rgba(0,0,0,0.24); font-size: 11pt; + margin-top: 5pt; } div#sidebar .content .button:hover From 77bcb14327d95b80c1979a6eecfc8d2ca795ab95 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 9 Nov 2014 00:49:14 +0000 Subject: [PATCH 049/560] Error handling for login in sidebar. --- forum.nim | 7 ++++++- main.tmpl | 14 +++++++++++--- public/css/style.css | 15 ++++++++++++++- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/forum.nim b/forum.nim index 2e3148a..bb6a29c 100644 --- a/forum.nim +++ b/forum.nim @@ -772,7 +772,12 @@ routes: finishLogin() cacheHolder.invalidateAll() else: - resp c.genMain(genFormLogin(c)) + 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() diff --git a/main.tmpl b/main.tmpl index 2a89a6f..44a7dfb 100644 --- a/main.tmpl +++ b/main.tmpl @@ -101,10 +101,18 @@ #else:
Login
- Username: - Password: + + Username: + Password: + + + #if c.errorMsg != "": + $c.errorMsg + #end if Register - Login + Login
#end if
diff --git a/public/css/style.css b/public/css/style.css index 35c9a4b..a285984 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -423,7 +423,20 @@ div#sidebar .content .search width: 95%; } -form +div#sidebar .content 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; +} + + +article#content form { border-right: 8px solid rgba(0, 0, 0, 0.2); From 114d322b50eaaaa291e45541ec4c402f0f45ba48 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 9 Nov 2014 18:28:21 +0000 Subject: [PATCH 050/560] Redesigned front page. Made font smalled for the thread list. Gravatars are now shown for each user that posted in each thread listed. Time since last reply is now calculated and shown beside each thread. --- forms.tmpl | 51 +++++++++++++++++++++----------------------- forum.nim | 49 ++++++++++++++++++++++++++++++++++++------ main.tmpl | 3 ++- public/css/style.css | 40 +++++++++++++++++++++++----------- 4 files changed, 95 insertions(+), 48 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index 96b6c59..b4f531e 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -15,9 +15,9 @@ # count = 0
Topic
-
Last
+
Users
Details
-
Author
+
Activity
# for row in rows(db, query, $((c.pageNum-1) * ThreadsPerPage), $ThreadsPerPage): @@ -25,47 +25,44 @@
- ${UrlButton(c, xmlEncode(%name), c.genThreadUrl(threadid = %threadid))} + ${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) +
+
+ #for i in 0 .. min(6, users.len-1): + + #end for +
+
#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) - #let latestReplyDate = getValue(db, sql("SELECT strftime('%s', " & - # "(select creation from post where id = (select max(id) from post where thread = ?)))"), %threadId) - #let timeStr = formatTimestamp(latestReplyDate.parseInt()) + #let replyProfileUrl = c.req.makeUri("profile/", false) & # xmlEncode(latestReplyAuthor) - # let posts = getValue(db, sql"select count(*) from post where thread = ?", %threadId)
-
${xmlEncode(%views)}
-
$posts
+
${xmlEncode(%views)}
+
$posts
- - #let authorName = getValue(db, sql("select name from person where id = " & - # "(select author from post where id = " & - # "(select min(id) from post where thread = ?))"), %threadId) - #let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(authorName) -
- + + #let latestReplyDate = getValue(db, sql("SELECT strftime('%s', " & + # "(select creation from post where id = (select max(id) from post where thread = ?)))"), %threadId) + #let timeStr = formatTimestamp(latestReplyDate.parseInt()) +
+
$timeStr
# end for - -
#end proc diff --git a/forum.nim b/forum.nim index bb6a29c..dc32cfb 100644 --- a/forum.nim +++ b/forum.nim @@ -143,15 +143,50 @@ proc genButtons(c: var TForumData, btns: seq[TStyledButton]): string = result.add(("""$2""") % [ btns[i].link, btns[i].text, class, anchor]) +proc toInterval(diff: int64): TimeInterval = + var remaining = diff + let years = remaining div 31536000 + remaining -= years * 31536000 + let months = remaining div 2592000 + remaining -= months * 2592000 + let days = remaining div 86400 + remaining -= days * 86400 + let hours = remaining div 3600 + remaining -= hours * 3600 + let minutes = remaining div 60 + remaining -= minutes * 60 + #assert false + result = initInterval(0, remaining.int, minutes.int, hours.int, days.int, + months.int, years.int) + proc formatTimestamp(t: int): string = - let t2 = getGMTime(TTime(t)) - return t2.format("ddd',' d MMM yyyy HH':'mm 'UTC'") + let t2 = TTime(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") + elif diff.seconds > 0: + return $diff.seconds & + (if diff.seconds > 1: " seconds ago" else: " second ago") + else: + return "less than a second ago" + +proc getGravatarUrl(email: string, size = 80): string = + let emailMD5 = email.toLower.toMD5 + return ("http://www.gravatar.com/avatar/" & $emailMD5 & "?s=" & $size & + "&d=identicon") proc genGravatar(email: string, size: int = 80): string = - let emailMD5 = email.toLower.toMD5 - result = "" % - ("http://www.gravatar.com/avatar/" & $emailMD5 & "?s=" & $size & - "&d=identicon") + result = "" % getGravatarUrl(email, size) proc randomSalt(): string = result = "" @@ -535,8 +570,8 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string = else: lastTag = htmlgen.a(href=lastUrl, ">>") nextTag = htmlgen.a(href=nextUrl, "••>") - result.add(lastTag) result.add(nextTag) + result.add(lastTag) proc gatherTotalPostsByID(c: var TForumData, thrid: int): int = ## Gets the total post count of a thread. diff --git a/main.tmpl b/main.tmpl index 44a7dfb..c44dea3 100644 --- a/main.tmpl +++ b/main.tmpl @@ -110,7 +110,8 @@ #if c.errorMsg != "": $c.errorMsg #end if - Register + Register Login
diff --git a/public/css/style.css b/public/css/style.css index a285984..1bf81f0 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -26,7 +26,7 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } .page-layout { margin:0 auto; width:1000px; } .docs-layout { margin:0 40px; } -.talk-layout { margin:0 40px; } +.talk-layout { margin:0 40px; min-height: 700px; } .wide-layout { margin:0 auto; } #head { height:100px; background:url("/images/head.png") repeat-x bottom; } @@ -167,11 +167,11 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #talk-heads > div { float:left; font-size:120%; font-weight:bold; } #talk-heads > .topic { width:55%; } #talk-heads > .detail { width:15%; } - #talk-heads > .author { width:15%; } - #talk-heads > .reply { width:15%; } + #talk-heads > .activity { width:15%; } + #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 > .author > div { margin-right:0; } + #talk-heads > .activity > div { margin-right:0; } #talk-thread > div, #talk-threads > div { @@ -186,21 +186,35 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #talk-thread > div:nth-child(odd) { background:rgba(255,255,255,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; } + #talk-threads > div > div + { + float:left; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + font-size: 10pt; + } #talk-thread > div > div > div, - #talk-threads > div > div > div { margin:10px 20px; } + #talk-threads > div > div > div { margin: 5px 20px; } #talk-threads > div > .topic { width:55%; } - #talk-threads > div > .reply { width:15%; overflow:hidden; } + #talk-threads > div > .users { width:15%; overflow:hidden; } + #talk-threads > div > .users > div > img + { + margin-bottom: -4pt; + cursor: help; + } #talk-threads > div > .detail { width:15%; overflow:hidden; } - #talk-thread > div > .author, - #talk-threads > div > .author { + #talk-thread > div > .activity, + #talk-threads > div > .activity { position:absolute; right:0; top:0; bottom:0; width:15%; overflow:hidden; - background:rgba(0,0,0,0.8); } + background:rgba(0,0,0,0.8); + color: white; + } #talk-thread > div > .author a, #talk-threads > div > .author a { color:#1cb3ec !important; } #talk-thread > div > .author a:hover, @@ -208,10 +222,10 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #talk-threads > div > .topic .pages { float:right; } #talk-threads > div > .topic > div > a { font-weight:bold; } #talk-threads > div > .detail > div { float:left; margin:0; } - #talk-threads > div > .detail > div > div { margin-left:20px; padding:10px 10px 10px 22px; } + #talk-threads > div > .detail > div > div { margin-left:20px; 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; } - #talk-threads > div > .detail > div:last-child > div { background:url("/images/forum-posts.png") no-repeat left; } + #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:150px; box-shadow:1px 3px 12px rgba(0,0,0,.4) } #talk-thread > div > .author > div > .avatar { margin-top:20px; } From ab84e3249a76d0e561648abd64cc5f25038cd776 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 9 Nov 2014 22:41:37 +0000 Subject: [PATCH 051/560] Fix double scrollbar. --- public/css/style.css | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index 1bf81f0..cd4c480 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -26,7 +26,7 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } .page-layout { margin:0 auto; width:1000px; } .docs-layout { margin:0 40px; } -.talk-layout { margin:0 40px; min-height: 700px; } +.talk-layout { margin:0 40px; } .wide-layout { margin:0 auto; } #head { height:100px; background:url("/images/head.png") repeat-x bottom; } @@ -101,7 +101,7 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #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; } +#body.forum { margin:0 40px 20px 40px; min-height: 700px; } #body-border { position:absolute; @@ -204,12 +204,8 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } cursor: help; } #talk-threads > div > .detail { width:15%; overflow:hidden; } - #talk-thread > div > .activity, - #talk-threads > div > .activity { - position:absolute; - right:0; - top:0; - bottom:0; + #talk-thread > div > .author, + #talk-threads > div > .author { width:15%; overflow:hidden; background:rgba(0,0,0,0.8); From d1b6d0ad40399f313fff1820512a98a719d033fb Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 9 Nov 2014 23:35:14 +0000 Subject: [PATCH 052/560] Design fixes for posts list. --- forms.tmpl | 4 ++-- public/css/style.css | 31 ++++++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index b4f531e..7a0f9a8 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -124,8 +124,7 @@
#let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(%userName)
${genGravatar(%userEmail)}
- marcianx -

${xmlEncode(%postCreation)}

+ ${xmlEncode(%userName)} #if c.userId == %postAuthor and c.currentPost.subject.len == 0:
Edit post #elif c.isAdmin and c.currentPost.subject.len == 0: @@ -140,6 +139,7 @@ #except EParseError: # c.errorMsg = getCurrentExceptionMsg() #end + ${xmlEncode(%postCreation)}
diff --git a/public/css/style.css b/public/css/style.css index cd4c480..753cd9f 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -153,7 +153,7 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } height:3px; background:url("/images/glow-line2.png") no-repeat right; } - #content { padding:40px 0; line-height:150%; } + #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; } @@ -182,7 +182,12 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } border:8px solid rgba(0,0,0,.8); border-top:none; border-bottom:none; - background:rgba(0,0,0,0.1); } + background:rgba(0,0,0,0.1); + } + #talk-threads > div + { + line-height: 150%; + } #talk-thread > div:nth-child(odd) { background:rgba(255,255,255,0.1); } #talk-threads > div:nth-child(odd) { background:rgba(0,0,0,0.2); } #talk-thread > div > div, @@ -196,6 +201,19 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } } #talk-thread > div > div > div, #talk-threads > div > div > div { margin: 5px 20px; } + #talk-thread > div > .topic + { + margin-top: 25pt; + white-space: normal; + } + #talk-thread > div > .topic > div > span.date + { + position: absolute; + top: 5px; + right: 10pt; + border-bottom: 1px dashed; + color: #3D3D3D; + } #talk-threads > div > .topic { width:55%; } #talk-threads > div > .users { width:15%; overflow:hidden; } #talk-threads > div > .users > div > img @@ -205,12 +223,16 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } } #talk-threads > div > .detail { width:15%; overflow:hidden; } #talk-thread > div > .author, - #talk-threads > div > .author { + #talk-threads > div > .activity { width:15%; overflow:hidden; background:rgba(0,0,0,0.8); color: white; } + #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, @@ -225,10 +247,9 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #talk-thread > div { margin:20px 0; min-height:150px; box-shadow:1px 3px 12px rgba(0,0,0,.4) } #talk-thread > div > .author > div > .avatar { margin-top:20px; } - #talk-thread > div > .author > div > .avatar > img { box-shadow:0 0 12px #1cb3ec; } #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; } + #talk-thread > div > .topic { width:85%; padding-bottom:10px; margin-left: 15%; } #talk-thread > div > .topic pre { overflow:auto; margin:0; From 707393a509d1c4cc0fb4aab37b37a717411a7066 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 9 Nov 2014 23:37:04 +0000 Subject: [PATCH 053/560] Added hr style. --- public/css/style.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/public/css/style.css b/public/css/style.css index 753cd9f..92c8c5f 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -475,3 +475,9 @@ article#content form background-color: rgba(255, 255, 255, 0.1); } + + +hr +{ + border: 1px solid #3D3D3D; +} From 5d90a9e651d054a14c31c3935c62f405966ae871 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 10 Nov 2014 00:20:05 +0000 Subject: [PATCH 054/560] Add spacing between page num links. --- public/css/style.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/css/style.css b/public/css/style.css index 92c8c5f..91dcf93 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -290,7 +290,8 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #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 { margin-left: 5pt; } + #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; From 4d459819e1c91ad228a073ecc72f932169c60dd9 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 10 Nov 2014 17:16:18 +0000 Subject: [PATCH 055/560] Fixes sidebar style on smaller screens. --- public/css/style.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/css/style.css b/public/css/style.css index 91dcf93..e72ddf2 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -435,7 +435,6 @@ div#sidebar .user .button { float: left; margin-top: 5pt; - margin-left: 5pt; width: 52.5%; } @@ -447,6 +446,11 @@ div#sidebar .user .logout margin-left: 0pt; } +div#sidebar .user .avatar > img +{ + margin-right: 5pt; +} + div#sidebar .content .search { text-align: center; From 9e7b889e3d75fae8b1fc10c72e76d0bba0241e51 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 10 Nov 2014 17:31:15 +0000 Subject: [PATCH 056/560] Syntax highlighting colors implemented. --- public/css/style.css | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index e72ddf2..1f27869 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -9,15 +9,16 @@ body { font:13pt "arial"; background:#152534 url("/images/bg.jpg") no-repeat fixed center top; } -pre { color:#5997AF;} +pre { color: #ffffff;} pre, pre * { cursor:text; } -pre .cmt { color:#6D6D6D; font-style:italic; } -pre .kwd { color:#43A8CF; font-weight:bold; } -pre .typ { color:#128B7D; font-weight:bold; } +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 .prg { color:#854D6A; font-weight:bold; font-style:italic; } -pre .val { color:#8AB647; font-style:italic; } +pre .StringLit { color:#854D6A; font-weight:bold; font-style:italic; } +pre .DecNumber, pre .FloatNumber { color:#8AB647; font-style:italic; } pre .tab { border-left:1px dotted rgba(67,168,207,0.4); } pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } From fb24365d9aa39431e56789eee464509c239c68ff Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 10 Nov 2014 19:32:39 +0000 Subject: [PATCH 057/560] Better form styles. Implemented new design for post preview. --- forms.tmpl | 43 ++++++++++++++++++++++--------------------- forum.nim | 1 + public/css/style.css | 15 +++++++++++---- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index 7a0f9a8..4419486 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -68,30 +68,30 @@ #end proc # # -#proc genPostPreview(c: var TForumData, +#proc genPostPreview(c: var TForumData, # title, content, author, date: string): string = # result = "" - - - - - - - - -
- ${xmlEncode(title)} - ${xmlEncode(date)} -
- ${xmlEncode(author)} - - #try: - ${content.rstToHtml} - #except EParseError: - # c.errorMsg = getCurrentExceptionMsg() - #end -
+
+
+
+
+ #let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(author) + ${xmlEncode(author)} +
+
+
+
+ #try: + ${content.rstToHtml} + #except EParseError: + # c.errorMsg = getCurrentExceptionMsg() + #end + ${xmlEncode(date)} +
+
+
+
#end proc # # @@ -157,6 +157,7 @@
+ forum index > $topText
diff --git a/forum.nim b/forum.nim index dc32cfb..42da8f2 100644 --- a/forum.nim +++ b/forum.nim @@ -464,6 +464,7 @@ proc login(c: var TForumData, name, 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" return c.threadId >= 0 and result proc genActionMenu(c: var TForumData): string = diff --git a/public/css/style.css b/public/css/style.css index 1f27869..2f48282 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -246,7 +246,7 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #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:150px; box-shadow:1px 3px 12px rgba(0,0,0,.4) } + #talk-thread > div { margin:20px 0; min-height:150px; box-shadow:1px 3px 12px rgba(0,0,0,.4); 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; } @@ -475,13 +475,20 @@ div#sidebar .content span.error 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:focus, article#content form > textarea:focus +{ + border: 1px solid #1cb3ec; +} hr { From 2ac0f934a73b585d6a4fc0a06572afc80d95e4e6 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 10 Nov 2014 20:41:29 +0000 Subject: [PATCH 058/560] Margin bottom for
 code.

---
 public/css/style.css | 18 ++++++++++--------
 1 file changed, 10 insertions(+), 8 deletions(-)

diff --git a/public/css/style.css b/public/css/style.css
index 2f48282..dceed6e 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -252,14 +252,16 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; }
     #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 {
-      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); }
+        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;
+    }
     
     #talk-head,
     #talk-info {

From 4f7396d059aa6a143b1166abf695fcfbdc800bdd Mon Sep 17 00:00:00 2001
From: Miguel 
Date: Tue, 11 Nov 2014 08:22:11 +0300
Subject: [PATCH 059/560] Forum search added and some modifications done.

---
 fts.sql                | 74 ++++++++++++++++++++++++++++++++++++++++++
 static/search-help.rst | 35 ++++++++++++++++++++
 2 files changed, 109 insertions(+)
 create mode 100644 fts.sql
 create mode 100644 static/search-help.rst

diff --git a/fts.sql b/fts.sql
new file mode 100644
index 0000000..ab49d31
--- /dev/null
+++ b/fts.sql
@@ -0,0 +1,74 @@
+-- selects just threads,
+-- those where title doesn't coinside with some of its posts' titles
+-- by now selects only the threads title (no post snippet)
+SELECT
+        thread_id,
+        snippet(thread_fts, '', '', '...') AS thread,
+        0 AS post_id,
+        '' AS header,
+        '' AS content,
+        person.name AS author,
+        cdate,
+        author_id,
+        person.email AS email,
+        0 AS what
+    FROM (
+        SELECT
+                thread_fts.id AS thread_id,
+                post.id AS post_id,
+                post.creation AS cdate,
+                MIN(post.creation) AS cdate,
+                post.author AS author_id
+            FROM thread_fts
+            JOIN post ON post.thread=thread_id
+            WHERE thread_fts MATCH ?
+            GROUP BY thread_id, post_id
+            HAVING thread_id NOT IN (
+                SELECT thread
+                    FROM post_fts JOIN post USING(id)
+                    WHERE post_fts MATCH ?
+            )
+            LIMIT ? OFFSET (? - 1) * ?
+    )
+        JOIN thread_fts ON thread_fts.id=thread_id
+        JOIN person ON person.id=author_id
+    WHERE thread_fts MATCH ?
+UNION
+-- the main query, selects posts
+SELECT
+        thread.id AS thread_id,
+        thread.name AS thread,
+        post.id AS post_id,
+        CASE what WHEN 1
+            THEN snippet(post_fts, '', '', '...', what)
+            ELSE post_fts.header END AS header,
+        CASE what WHEN 2
+            THEN snippet(post_fts, '', '', '...', what, -45)
+            ELSE SUBSTR(post_fts.content, 1, 200) END AS content,
+        person.name AS author,
+        cdate,
+        post.author AS author_id,
+        person.email AS email,
+        what
+    FROM post_fts JOIN (
+    -- inner query, selects ids of matching posts, orders and limits them,
+    -- so snippets only for limited count of posts are created (in outer query)
+        SELECT id, post.creation AS cdate, thread, 1 AS what, post.author AS author
+            FROM post_fts JOIN post USING(id)
+            WHERE post_fts.header MATCH ?
+            GROUP BY post.header
+            HAVING SUBSTR(post.header,1,3)<>'Re:'
+        UNION
+        SELECT id, post.creation AS cdate, thread, 2 AS what, post.author AS author
+            FROM post_fts JOIN post USING(id)
+            WHERE post_fts.content MATCH ?
+        ORDER BY what, cdate DESC
+        LIMIT ? OFFSET (? - 1) * ?
+    ) AS post USING(id)
+        JOIN thread ON thread.id=thread
+        JOIN person ON person.id=author
+    WHERE post_fts MATCH ?
+ORDER BY what ASC, cdate DESC
+LIMIT 300 -- hardcoded limit just in case
+;
+
diff --git a/static/search-help.rst b/static/search-help.rst
new file mode 100644
index 0000000..8e7fc80
--- /dev/null
+++ b/static/search-help.rst
@@ -0,0 +1,35 @@
+Full-text search for Nim forum
+==============================
+
+Syntax (using *SQLite* dll compiled without *Enhanced Query Syntax* support):
+-----------------------------------------------------------------------------
+
+- Only alphanumeric characters are searched.
+- Only full words and words beginnings (e.g. ``Nim*`` for both ``Nimrod`` and ``Nim``) are searched
+- All words are joined with implicit **AND** operator; there's no explicit one
+- There's explicit **OR** operator (upper-case) and it has higher priority
+- Words can be prepended with **-** to be excluded from search
+- No parentheses support
+- Quotes for phrases search, e.g. ``"programming language"``
+- Distances between words/phrases can be specified putting ``NEAR`` or ``NEAR/some_number`` between them
+
+Syntax - differences in *Enhanced Query Syntax* (should be enabled in *SQLite* dll):
+------------------------------------------------------------------------------------
+
+- **AND** and **NOT** logical operators available
+- Precedence of operators is, from highest to lowest: **NOT**, **AND**, **OR**
+- Parentheses for grouping are supported
+
+Where search is performed:
+--------------------------
+
+- **Threads' titles** - these results are outputed first
+- **Posts' titles** - middle precedence
+- **Posts' contents** - the latest
+
+How results are shown:
+----------------------
+
+- All results are ordered by date (posts' edits don't affect)
+- Matched tokens in text are marked (bold or dotted underline)
+- Threads title is the link to the thread and posts title is the link to the post

From 3f08e94e2184ebc863eebbdac0ac784dfde94dac Mon Sep 17 00:00:00 2001
From: Miguel 
Date: Tue, 11 Nov 2014 08:24:46 +0300
Subject: [PATCH 060/560] Forum search added and some modifications done.

---
 forms.tmpl           |  95 ++++++++++++++++++++++++++++++++
 forum.nim            | 126 ++++++++++++++++++++++++++++++++++++-------
 main.tmpl            |  17 +++++-
 public/css/style.css |  81 ++++++++++++++++++++++++++++
 4 files changed, 299 insertions(+), 20 deletions(-)

diff --git a/forms.tmpl b/forms.tmpl
index 7a0f9a8..f757731 100644
--- a/forms.tmpl
+++ b/forms.tmpl
@@ -119,6 +119,7 @@
 
# for row in posts: # inc(count) +
@@ -240,3 +241,97 @@ ${stats.totalThreads} threads  |  ${stats.totalPosts} posts  |  newest member: ${stats.newestMember.nick} #end proc +# +# +# +# +#proc genSearchResults(c: var TForumData, +# results: iterator: db_sqlite.TRow {.closure, tags: [FReadDB].}, +# count: var int): string = +# const threadId = 0 +# const threadName = 1 +# const postId = 2 +# const postHeader = 3 +# const postContent = 4 +# const userName = 5 +# const postCreation = 6 +# const postAuthor = 7 +# const userEmail = 8 +# const what = 9 +# result = "" +# count = 0 +# var whCount: array[bool, int] +
+
+
+ Search results for: ${xmlEncode(c.search.replace(""","\""))}. +
+
+
+
+# for row in results(): +# inc(count) +# let isThread = %what == "0" +# inc(whCount[isThread]) +# let postUrl = c.genThreadUrl(%postId,"",%threadId,"0") +# 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 + #elif c.isAdmin and c.currentPost.subject.len == 0: +
Edit post + #end if +
+
+
+
+ #if %threadName != %postHeader and "Re: " & %threadName != %postHeader: + #headersDiffer = true +

+ + Thread: + ${%threadName} + +

+ #end if + #if not headersDiffer or %postHeader != "": +
+ + Post: + ${%postHeader} + +
+ #end if + #if not isThread: + #try: + ${(%postContent)} + #except EParseError: + # c.errorMsg = getCurrentExceptionMsg() + #end + #end if + ${xmlEncode(%postCreation)} +
+
+
+# end for +
+# if c.pageNum > 1: +
+ + +
+# end if +# if whCount[true] == ThreadsPerPage or whCount[false] == ThreadsPerPage: +
+ + +
+# end if +#end proc +# diff --git a/forum.nim b/forum.nim index dc32cfb..c85fc83 100644 --- a/forum.nim +++ b/forum.nim @@ -8,22 +8,23 @@ import os, strutils, times, md5, strtabs, cgi, math, db_sqlite, matchers, - rst, rstgen, captchas, scgi, jester, asyncdispatch, asyncnet, cache + rst, rstgen, captchas, scgi, jester, asyncdispatch, asyncnet, cache, sequtils from htmlgen import tr, th, td, span const unselectedThread = -1 transientThread = 0 - ThreadsPerPage = 15 - PostsPerPage = 10 + ThreadsPerPage = 2 + PostsPerPage = 3 + MaxPagesFromCurrent = 2 noPageNums = ["/login", "/register", "/dologin", "/doregister", "/profile"] noHomeBtn = ["/", "/login", "/register", "/dologin", "/doregister", "/profile"] type TCrud = enum crCreate, crRead, crUpdate, crDelete - TSession = object of TObject + TSession = object of RootObj threadid: int postid: int userName, userPass, email: string @@ -42,6 +43,8 @@ type isThreadsList: bool pageNum: int totalPosts: int + search: string + noPagenumumNav: bool TStyledButton = tuple[text: string, link: string] @@ -61,7 +64,8 @@ type var db: TDbConn - docConfig: PStringTable + docConfig: StringTableRef + isFTSAvailable: bool proc init(c: var TForumData) = c.userPass = "" @@ -75,6 +79,8 @@ proc init(c: var TForumData) = c.loginErrorMsg = "" c.invalidField = "" c.currentPost = (subject: "", content: "") + + c.search = "" proc loggedIn(c: TForumData): bool = result = c.userName.len > 0 @@ -114,6 +120,8 @@ proc genThreadUrl(c: TForumData, postId = "", action = "", threadid = "", pageNu result.add("?action=" & action) if postId != "": result.add("&postid=" & postid) + elif postId != "": + result.add("/" & postId & "#" & postId) result = c.req.makeUri(result, absolute = false) proc FormSession(c: var TForumData, nextAction: string): string = @@ -160,7 +168,7 @@ proc toInterval(diff: int64): TimeInterval = months.int, years.int) proc formatTimestamp(t: int): string = - let t2 = TTime(t) + let t2 = Time(t) let now = getTime() let diff = (now - t2).toInterval() if diff.years > 0: @@ -186,7 +194,8 @@ proc getGravatarUrl(email: string, size = 80): string = "&d=identicon") proc genGravatar(email: string, size: int = 80): string = - result = "" % getGravatarUrl(email, size) + result = "" % + [$size, $size, getGravatarUrl(email, size)] proc randomSalt(): string = result = "" @@ -212,7 +221,7 @@ proc makeSalt(): string = ## Creates a salt using a cryptographically secure random number generator. try: result = devRandomSalt() - except EIO: + except IOError: result = randomSalt() proc makePassword(password, salt: string): string = @@ -387,6 +396,8 @@ template setPreviewData(c: expr) {.immediate, dirty.} = template writeToDb(c, cr, setPostId: expr) = 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"), + retID.int, subject, content) if setPostId: c.postId = retID.int @@ -399,17 +410,21 @@ proc edit(c: var TForumData, postId: int): bool = checkOwnership(c, $postId) if not tryExec(db, crud(crDelete, "post"), $postId): return setError(c, "", "database error") + discard tryExec(db, crud(crDelete, "post_fts"), $postId) # delete corresponding thread: if execAffectedRows(db, sql"delete from thread where id not in (select thread from post)") > 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)") result = true else: checkOwnership(c, $postId) retrPost(c) exec(db, crud(crUpdate, "post", "header", "content"), subject, content, $postId) + exec(db, crud(crUpdate, "post_fts", "header", "content"), + subject, content, $postId) result = true proc reply(c: var TForumData): bool = @@ -434,7 +449,11 @@ proc newThread(c: var TForumData): bool = else: 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"), + c.threadID, c.req.params["subject"]) 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')") result = true proc login(c: var TForumData, name, pass: string): bool = @@ -500,7 +519,7 @@ proc getStats(c: var TForumData, simple: bool): TForumStats = sql"select id, name, admin, 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() - TTime(secs) + let lastOnlineSeconds = getTime() - Time(secs) if lastOnlineSeconds < (60 * 5): # 5 minutes result.activeUsers.add((row[1], row[0].parseInt, row[2].parseBool)) if row[4].parseInt > newestMemberCreation: @@ -543,12 +562,17 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string = else: firstTag = htmlgen.a(href=firstUrl, "<<") prevTag = htmlgen.a(href=prevUrl, "<••") + prevTag.add(htmlgen.link(rel="previous", href=prevUrl)) result.add(firstTag) result.add(prevTag) # Numbers var pages = "" # Tags - for i in 1..totalPages: + # cutting numbers to the left and to the right tp MaxPagesFromCurrent + let firstToShow = max(1, c.pageNum - MaxPagesFromCurrent) + let lastToShow = min(totalPages, c.pageNum + MaxPagesFromCurrent) + if firstToShow > 1: pages.add(span("...")) + for i in firstToShow .. lastToShow: if i == c.pageNum: pages.add(span($(i))) else: @@ -559,6 +583,16 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string = pageUrl = c.req.makeUri(firstUrl & "/" & $(i)) pages.add(htmlgen.a(href = pageUrl, $(i))) + if lastToShow < totalPages: pages.add(span("...")) + # a number input for quick jump to a page + pages.add(htmlgen.form( + action = c.req.makeUri(if c.isThreadsList: "/page" else: firstUrl), + class = "pagenumJump", + when false: htmlgen.input(`type` = "number", name = "page", value = $c.pageNum, max = $totalPages) + else: "" % + [$c.pageNum, $totalPages], + htmlgen.input(`type` = "submit", value = "►", title = "Go to the page", class = "jump") )) + # max attribute for inputs not supported in htmlgen result.add(pages) # Right @@ -570,6 +604,7 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string = else: lastTag = htmlgen.a(href=lastUrl, ">>") nextTag = htmlgen.a(href=nextUrl, "••>") + nextTag.add(htmlgen.link(rel="next",href=nextUrl)) result.add(nextTag) result.add(lastTag) @@ -639,7 +674,7 @@ proc gatherUserInfo(c: var TForumData, nick: string, ui: var TUserInfo): bool = proc genProfile(c: var TForumData, ui: TUserInfo): string = result = "" result.add(htmlgen.`div`(id = "avatar", genGravatar(ui.email, 250))) - let t2 = if ui.lastOnline != -1: getGMTime(TTime(ui.lastOnline)) + let t2 = if ui.lastOnline != -1: getGMTime(Time(ui.lastOnline)) else: getGMTime(getTime()) result.add(htmlgen.`div`(id = "info", @@ -708,14 +743,19 @@ routes: createTFD() resp genPostsRSS(c), "application/atom+xml" - get "/t/@threadid/?@page?/?": + get "/t/@threadid/?@page?/?@postid?/?": createTFD() parseInt(@"threadid", c.threadId, -1..1000_000) - if @"page".len > 0: - parseInt(@"page", c.pageNum, 0..1000_000) - cond (c.pageNum > 0) if (@"postid").len > 0: parseInt(@"postid", c.postId, -1..1000_000) + if @"page".len > 0: + parseInt(@"page", c.pageNum, 0..1000_000) + # for direct links to posts (with no thread id passed); used in search results + if c.pageNum == 0 and c.postId > 0: + const sqlGetPostsUnder = sql"select count(*) from post where thread = ? and id < ?" + let postsUnder = db.getValue(sqlGetPostsUnder, c.threadId, c.postId).parseInt + c.pageNum = (postsUnder div PostsPerPage) + 1 + cond (c.pageNum > 0) var count = 0 var pSubject = getThreadTitle(c.threadid, c.pageNum) cond validThreadId(c) @@ -739,6 +779,7 @@ routes: let content = ||row[1] body = genFormPost(c, "doedit", "Edit", header, content, true) title = "Editing post" + else: discard resp c.genMain(body, title & " - Nimrod Forum") else: incrementViews(c) @@ -746,7 +787,7 @@ routes: cond count != 0 resp genMain(c, posts, pSubject & " - Nimrod Forum") - get "/page/@page/?": + get "/page/?@page?/?": createTFD() c.isThreadsList = true cond (@"page" != "") @@ -799,7 +840,8 @@ routes: body.add genPostPreview(c, @"subject", @"content", c.userName, $getGMTime(getTime())) body.add genFormPost(c, action, topText, reuseText, reuseText, isEdit) - resp genMain(c, body(), "Nimrod Forum - Error") + resp genMain(c, body(), "Nimrod Forum - " & + (if c.isPreview: "Preview" else: "Error")) post "/dologin": createTFD() @@ -848,7 +890,7 @@ routes: createTFD() readIDs() if edit(c, c.postId): - redirect(c.genThreadUrl()) + redirect(c.genThreadUrl(pageNum = "0", postId = $c.postId)) else: body = "" handleError("doedit", "Edit", true) @@ -863,11 +905,59 @@ routes: createTFD() resp genMain(c, rstToHtml(licenseRst), "Content license - Nimrod Forum") + post "/search/?@page?": + if not isFTSAvailable: + redirect(uri("/")) + createTFD() + c.isThreadsList = true + c.noPagenumumNav = true + var count = 0 + var q = @"q" + for i in 0 .. q.len-1: + if q[i].int < 32: q[i] = ' ' + elif q[i] == '\'': q[i] = '"' + 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].} = + const queryFT = "fts.sql".slurp.sql + for rowFT in fastRows(db, queryFT, + [q,q,$ThreadsPerPage,$c.pageNum,$ThreadsPerPage,q, + q,q,$ThreadsPerPage,$c.pageNum,$ThreadsPerPage,q]): + yield rowFT + resp genMain(c, genSearchResults(c, searchResults, count), + 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 = + createTFD() + #c.isThreadsList = true + var page = "" + if existsFile(path): + page = readFile(path) + else: + let basePath = + if path[path.high] == '/': path & "index" + elif path.endsWith(".html"): path[-5 .. -1] + else: path + if existsFile(basePath & ".html"): + page = readFile(basePath & ".html") + elif existsFile(basePath & ".rst"): + page = readFile(basePath & ".rst").rstToHtml + writeFile(basePath & ".html", page) + resp genMain(c, page) + get "/search-help": + textPage "static/search-help" + + + when isMainModule: docConfig = rstgen.defaultConfig() math.randomize() db = open(connection="nimforum.db", user="postgres", password="", database="nimforum") + isFTSAvailable = db.getValue(sql"select 1 from post_fts limit 1")=="1" var http = true if paramCount() > 0: if paramStr(1) == "scgi": diff --git a/main.tmpl b/main.tmpl index c44dea3..cd96182 100644 --- a/main.tmpl +++ b/main.tmpl @@ -59,9 +59,11 @@
+ #if not c.noPagenumumNav:
${genPagenumNav(c, stats)}
+ #end if #elif hasReplyBtn(c):
@@ -82,8 +84,19 @@ # end for diff --git a/forum.nim b/forum.nim index 42da8f2..f5d9954 100644 --- a/forum.nim +++ b/forum.nim @@ -143,42 +143,9 @@ proc genButtons(c: var TForumData, btns: seq[TStyledButton]): string = result.add(("""$2""") % [ btns[i].link, btns[i].text, class, anchor]) -proc toInterval(diff: int64): TimeInterval = - var remaining = diff - let years = remaining div 31536000 - remaining -= years * 31536000 - let months = remaining div 2592000 - remaining -= months * 2592000 - let days = remaining div 86400 - remaining -= days * 86400 - let hours = remaining div 3600 - remaining -= hours * 3600 - let minutes = remaining div 60 - remaining -= minutes * 60 - #assert false - result = initInterval(0, remaining.int, minutes.int, hours.int, days.int, - months.int, years.int) - proc formatTimestamp(t: int): string = let t2 = TTime(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") - elif diff.seconds > 0: - return $diff.seconds & - (if diff.seconds > 1: " seconds ago" else: " second ago") - else: - return "less than a second ago" + return t2.getGMTime().format("yyyy-MM-dd'T'HH':'mm':'ss'Z'") proc getGravatarUrl(email: string, size = 80): string = let emailMD5 = email.toLower.toMD5 diff --git a/public/css/style.css b/public/css/style.css index dceed6e..484e2e4 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -496,3 +496,8 @@ hr { border: 1px solid #3D3D3D; } + +.activity .isoDate +{ + display: none; +} diff --git a/public/js/forum.js b/public/js/forum.js index 7af672b..2f354d3 100644 --- a/public/js/forum.js +++ b/public/js/forum.js @@ -2,4 +2,75 @@ window.onload = function() { positionGlowArrow(); + processMainDates(); + window.setTimeout(performProcessingMainDates, 1000); }; + +function performProcessingMainDates() +{ + if (processMainDates()) + { + window.setTimeout(performProcessingMainDates, 1000); + } + else + { + window.setTimeout(performProcessingMainDates, 60000); + } +} + +function timeSince(date) { + var result = ["", false]; + var seconds = Math.floor((new Date() - date) / 1000); + + var interval = Math.floor(seconds / 31536000); + + if (interval >= 1) { + result[0] = interval + (interval == 1 ? " year ago" : " years ago"); + return result; + } + interval = Math.floor(seconds / 2592000); + if (interval >= 1) { + result[0] = interval + (interval == 1 ? " month ago" : " months ago"); + return result; + } + interval = Math.floor(seconds / 86400); + if (interval >= 1) { + result[0] = interval + (interval == 1 ? " day ago" : " days ago"); + return result; + } + interval = Math.floor(seconds / 3600); + if (interval >= 1) { + result[0] = interval + (interval == 1 ? " hour ago" : " hours ago"); + return result; + } + interval = Math.floor(seconds / 60); + if (interval >= 1) { + result[0] = interval + (interval == 1 ? " minute ago" : " minutes ago"); + return result; + } + if (seconds >= 1) { + result[0] = Math.floor(seconds) + + (seconds == 1 ? " second ago" : " seconds ago"); + result[1] = true; + return result; + } + return ["less than a second ago", true]; +} + +function processMainDates() { + var result = false; + var threads = document.getElementById("talk-threads").children; + for (var i = 0; i < threads.length; i++) + { + var activity = threads[i].getElementsByClassName("activity")[0]; + var activityDiv = activity.children[0]; + var isoDate = activityDiv.children[0].innerHTML; + var parsed = Date.parse(isoDate); + var timeS = timeSince(parsed); + + activityDiv.innerHTML = activityDiv.children[0].outerHTML + + timeS[0]; + if (timeS[1]) { result = timeS[1] } + } + return result; +} From 374f6f96b4bcbd752955bcfd717e25c503e91d0e Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 11 Nov 2014 20:18:29 +0000 Subject: [PATCH 062/560] Improved register and profile pages. --- forms.tmpl | 9 ++++++++- forum.nim | 12 ++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/forms.tmpl b/forms.tmpl index 3b60cca..bccfc34 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -188,8 +188,15 @@ # #proc genFormRegister(c: var TForumData): string = # result = "" +
+
+
+ forum index > + Register +
+
+
- Register
diff --git a/forum.nim b/forum.nim index f5d9954..61f0d9a 100644 --- a/forum.nim +++ b/forum.nim @@ -606,6 +606,18 @@ proc gatherUserInfo(c: var TForumData, nick: string, ui: var TUserInfo): bool = proc genProfile(c: var TForumData, ui: TUserInfo): string = result = "" + + result.add(htmlgen.`div`(id = "talk-head", + htmlgen.`div`(class="info-post", + htmlgen.`div`( + htmlgen.a(href = c.req.makeUri("/"), + span(style = "font-weight: bold;", "forum index") + ), + " > " & ui.nick & "'s profile" + ) + ) + ) + ) result.add(htmlgen.`div`(id = "avatar", genGravatar(ui.email, 250))) let t2 = if ui.lastOnline != -1: getGMTime(TTime(ui.lastOnline)) else: getGMTime(getTime()) From a28e3539cec003cd747c9783136d6fa424b42a08 Mon Sep 17 00:00:00 2001 From: Miguel Date: Tue, 11 Nov 2014 23:55:53 +0300 Subject: [PATCH 063/560] defaults restored for ThreadsPerPage, PostsPerPage --- forum.nim | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/forum.nim b/forum.nim index c85fc83..7c9c48b 100644 --- a/forum.nim +++ b/forum.nim @@ -15,9 +15,9 @@ const unselectedThread = -1 transientThread = 0 - ThreadsPerPage = 2 - PostsPerPage = 3 - MaxPagesFromCurrent = 2 + ThreadsPerPage = 15 + PostsPerPage = 10 + MaxPagesFromCurrent = 8 noPageNums = ["/login", "/register", "/dologin", "/doregister", "/profile"] noHomeBtn = ["/", "/login", "/register", "/dologin", "/doregister", "/profile"] From f0036a6ab2b83c49d479c10ad58032422cb3d5bd Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 13 Nov 2014 15:45:23 +0000 Subject: [PATCH 064/560] Added createdb which creates the correct FTS tables. --- createdb.nim | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/createdb.nim b/createdb.nim index 860cda9..3b601ec 100644 --- a/createdb.nim +++ b/createdb.nim @@ -86,8 +86,35 @@ create table if not exists antibot( );""", []): echo "antibot table already exists" +# -------------------- 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) - - From 4dd8b0d8c76b949045c5739c81078738fe389a73 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 13 Nov 2014 16:32:06 +0000 Subject: [PATCH 065/560] Removed extraneous postId code. --- forms.tmpl | 2 +- forum.nim | 16 +++------------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index 6f48892..c9305d2 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -284,7 +284,7 @@ # inc(count) # let isThread = %what == "0" # inc(whCount[isThread]) -# let postUrl = c.genThreadUrl(%postId,"",%threadId,"0") +# let postUrl = c.genThreadUrl(%postId,"",%threadId,"") # let threadUrl = c.genThreadUrl("","",%threadId) # var headersDiffer = false
diff --git a/forum.nim b/forum.nim index 1af1812..a47e1a2 100644 --- a/forum.nim +++ b/forum.nim @@ -118,10 +118,8 @@ proc genThreadUrl(c: TForumData, postId = "", action = "", threadid = "", pageNu result.add("/" & pageNum) if action != "": result.add("?action=" & action) - if postId != "": - result.add("&postid=" & postid) elif postId != "": - result.add("/" & postId & "#" & postId) + result.add("#" & postId) result = c.req.makeUri(result, absolute = false) proc FormSession(c: var TForumData, nextAction: string): string = @@ -723,18 +721,11 @@ routes: createTFD() resp genPostsRSS(c), "application/atom+xml" - get "/t/@threadid/?@page?/?@postid?/?": + get "/t/@threadid/?@page?/?": createTFD() parseInt(@"threadid", c.threadId, -1..1000_000) - if (@"postid").len > 0: - parseInt(@"postid", c.postId, -1..1000_000) if @"page".len > 0: parseInt(@"page", c.pageNum, 0..1000_000) - # for direct links to posts (with no thread id passed); used in search results - if c.pageNum == 0 and c.postId > 0: - const sqlGetPostsUnder = sql"select count(*) from post where thread = ? and id < ?" - let postsUnder = db.getValue(sqlGetPostsUnder, c.threadId, c.postId).parseInt - c.pageNum = (postsUnder div PostsPerPage) + 1 cond (c.pageNum > 0) var count = 0 var pSubject = getThreadTitle(c.threadid, c.pageNum) @@ -886,8 +877,7 @@ routes: resp genMain(c, rstToHtml(licenseRst), "Content license - Nimrod Forum") post "/search/?@page?": - if not isFTSAvailable: - redirect(uri("/")) + cond isFTSAvailable createTFD() c.isThreadsList = true c.noPagenumumNav = true From 58dc6bfcec5dacb1e918b1b13894408ffaa0a2c5 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 14 Nov 2014 00:40:07 +0000 Subject: [PATCH 066/560] Remove caching. --- forum.nim | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/forum.nim b/forum.nim index a47e1a2..3e50f98 100644 --- a/forum.nim +++ b/forum.nim @@ -699,17 +699,14 @@ template createTFD(): stmt = if request.cookies.len > 0: checkLoggedIn(c) -var cacheHolder = newCacheHolder() - routes: get "/": createTFD() c.isThreadsList = true var count = 0 let threadList = genThreadsList(c, count) - let data = cacheHolder.get("/", - genMain(c, threadList, - additionalHeaders = genRSSHeaders(c), showRssLinks = true)) + let data = genMain(c, threadList, + additionalHeaders = genRSSHeaders(c), showRssLinks = true) resp data get "/threadActivity.xml": @@ -788,7 +785,6 @@ routes: get "/logout/?": createTFD() logout(c) - cacheHolder.invalidateAll() redirect(uri("/")) get "/register/?": @@ -818,7 +814,6 @@ routes: createTFD() if login(c, @"name", @"password"): finishLogin() - cacheHolder.invalidateAll() else: c.isThreadsList = true var count = 0 @@ -832,14 +827,12 @@ routes: if c.register(@"name", @"new_password", @"antibot", @"email"): discard c.login(@"name", @"new_password") finishLogin() - cacheHolder.invalidateAll() else: resp c.genMain(genFormRegister(c)) post "/donewthread": createTFD() if newThread(c): - cacheHolder.invalidate("/") redirect(uri("/")) else: body = "" From 6e3c7466648a5f79cc1b69ffb7b3bc0f3b1d38d4 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 14 Nov 2014 00:52:36 +0000 Subject: [PATCH 067/560] Fixes editing posts. --- forum.nim | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/forum.nim b/forum.nim index 3e50f98..e5e779f 100644 --- a/forum.nim +++ b/forum.nim @@ -118,6 +118,8 @@ proc genThreadUrl(c: TForumData, postId = "", action = "", threadid = "", pageNu result.add("/" & pageNum) if action != "": result.add("?action=" & action) + if postId != "": + result.add("&postid=" & postid) elif postId != "": result.add("#" & postId) result = c.req.makeUri(result, absolute = false) @@ -718,11 +720,13 @@ routes: createTFD() resp genPostsRSS(c), "application/atom+xml" - get "/t/@threadid/?@page?/?": + get "/t/@threadid/?@page?/?@postid?/?": createTFD() parseInt(@"threadid", c.threadId, -1..1000_000) 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) @@ -854,7 +858,7 @@ routes: createTFD() readIDs() if edit(c, c.postId): - redirect(c.genThreadUrl(pageNum = "0", postId = $c.postId)) + redirect(c.genThreadUrl(postId = $c.postId)) else: body = "" handleError("doedit", "Edit", true) From eb769485e6d3ca9d2b33c97db84e0f7b641138cc Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 14 Nov 2014 16:11:46 +0000 Subject: [PATCH 068/560] Fixes nav numbers. Removes jump page num. --- forum.nim | 11 +---------- public/css/style.css | 34 +++++++++------------------------- 2 files changed, 10 insertions(+), 35 deletions(-) diff --git a/forum.nim b/forum.nim index e5e779f..e8154cb 100644 --- a/forum.nim +++ b/forum.nim @@ -552,15 +552,6 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string = pages.add(htmlgen.a(href = pageUrl, $(i))) if lastToShow < totalPages: pages.add(span("...")) - # a number input for quick jump to a page - pages.add(htmlgen.form( - action = c.req.makeUri(if c.isThreadsList: "/page" else: firstUrl), - class = "pagenumJump", - when false: htmlgen.input(`type` = "number", name = "page", value = $c.pageNum, max = $totalPages) - else: "" % - [$c.pageNum, $totalPages], - htmlgen.input(`type` = "submit", value = "►", title = "Go to the page", class = "jump") )) - # max attribute for inputs not supported in htmlgen result.add(pages) # Right @@ -617,7 +608,7 @@ proc genPagenumLocalNav(c: var TForumData, thrid: int): string = else: inc(i) - result = htmlgen.`div`(class = "localnums", result) + result = htmlgen.span(class = "pages", result) proc gatherUserInfo(c: var TForumData, nick: string, ui: var TUserInfo): bool = ui.nick = nick diff --git a/public/css/style.css b/public/css/style.css index 49a34f7..ebc470d 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -195,7 +195,6 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #talk-threads > div > div { float:left; - white-space: nowrap; text-overflow: ellipsis; overflow: hidden; font-size: 10pt; @@ -239,7 +238,15 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #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 > div > a { font-weight:bold; } + #talk-threads > div > .topic .pages > a + { + margin-right: 5pt; + } + #talk-threads > div > .topic > div > a + { + font-weight:bold; + white-space: nowrap; + } #talk-threads > div > .detail > div { float:left; margin:0; } #talk-threads > div > .detail > div > div { margin-left:20px; padding: 5px 5px 5px 22px; } #talk-threads > div > .detail > div { width:50%; } @@ -559,26 +566,3 @@ form.searchNav { clear: both; height: 1px; } - -/* page numbers form */ - -.pagenumJump { - display: inline; - border-width: 0px !important; -} - -.pagenumJump input { - margin: 0 .2em; - width: 3.2em; - background: #d0d6e3; - padding: 1px 2px; - border-width: 0px; -} - -.pagenumJump .jump { - border-width: 0px; - margin: 0; - background: transparent; - width: auto; - cursor: pointer; -} From e52d65c3a77aee993efadab4f023383f6d8f16fd Mon Sep 17 00:00:00 2001 From: Miguel Date: Sat, 15 Nov 2014 17:05:36 +0300 Subject: [PATCH 069/560] highlighting current post via CSS :hover pseudo-element --- forms.tmpl | 2 +- public/css/style.css | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index c9305d2..15839dc 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -123,7 +123,7 @@ # for row in posts: # inc(count) -
+
#let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(%userName) diff --git a/public/css/style.css b/public/css/style.css index ebc470d..c5420f3 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -512,8 +512,8 @@ hr /* highlighting current post */ -.post.current { - background-color: #fefff3; +div:target { + background: rgba(255, 255, 240, 0.25) !important; } /* full-text search */ From 688a5503acf9b9a0536df45aaefab125ad9c6ecc Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 15 Nov 2014 14:33:46 +0000 Subject: [PATCH 070/560] Improved style of threads: better links. --- public/css/style.css | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index ebc470d..903d22f 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -158,7 +158,7 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #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:rgba(0,0,0,.8); } + #content p { text-align:justify; color: #1D1D1D; margin: 5pt 0pt; } #content a { color:#CEDAE9; text-decoration:none; } #content a:hover { color:#fff; } #content ul { padding-left:20px; } @@ -174,6 +174,9 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #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; @@ -183,11 +186,11 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } border:8px solid rgba(0,0,0,.8); border-top:none; border-bottom:none; - background:rgba(0,0,0,0.1); } #talk-threads > div { line-height: 150%; + background:rgba(0,0,0,0.1); } #talk-thread > div:nth-child(odd) { background:rgba(255,255,255,0.1); } #talk-threads > div:nth-child(odd) { background:rgba(0,0,0,0.2); } @@ -203,7 +206,7 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #talk-threads > div > div > div { margin: 5px 20px; } #talk-thread > div > .topic { - margin-top: 25pt; + margin-top: 15pt; white-space: normal; } #talk-thread > div > .topic > div > span.date @@ -253,7 +256,7 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #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:150px; box-shadow:1px 3px 12px rgba(0,0,0,.4); padding-bottom: 10pt; } + #talk-thread > div { margin:20px 0; min-height:150px; 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; } @@ -269,7 +272,15 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } border-left:8px solid rgba(0,0,0,.3); margin-bottom: 10pt; } - + #talk-thread > div > .topic a, #talk-thread > div > .topic a:visited + { + color: #3680C9; + text-decoration: none; + } + #talk-thread > div > .topic a:hover + { + text-decoration: underline; + } #talk-head, #talk-info { overflow:auto; @@ -292,7 +303,8 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #talk-info > .user-post { width: 15%; background:rgba(0,0,0,.2); } #talk-info > .user-post > div > .reply { font-weight:bold; padding-left:22px; background:url("/images/forum-reply.png") no-repeat left; } #talk-head > div > div, - #talk-info > div > div { padding:5px 20px; } + #talk-info > div > 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; } From 0ec7387e0c52648557599fdad0745830b4e231a1 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 15 Nov 2014 15:00:22 +0000 Subject: [PATCH 071/560] Restored server side activity calculations. Last reply info added. --- forms.tmpl | 3 +- forum.nim | 33 ++++++++++++++++++-- public/css/style.css | 29 +++++++++++++----- public/js/forum.js | 71 -------------------------------------------- 4 files changed, 54 insertions(+), 82 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index c9305d2..d3a7a5c 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -60,8 +60,7 @@ #let timeStr = formatTimestamp(latestReplyDate.parseInt())
- $timeStr - $timeStr + $latestReplyAuthor replied $timeStr
diff --git a/forum.nim b/forum.nim index e8154cb..c4d00c1 100644 --- a/forum.nim +++ b/forum.nim @@ -151,9 +151,38 @@ proc genButtons(c: var TForumData, btns: seq[TStyledButton]): string = result.add(("""$2""") % [ btns[i].link, btns[i].text, class, anchor]) +proc toInterval(diff: int64): TimeInterval = + var remaining = diff + let years = remaining div 31536000 + remaining -= years * 31536000 + let months = remaining div 2592000 + remaining -= months * 2592000 + let days = remaining div 86400 + remaining -= days * 86400 + let hours = remaining div 3600 + remaining -= hours * 3600 + let minutes = remaining div 60 + remaining -= minutes * 60 + result = initInterval(0, remaining.int, minutes.int, hours.int, days.int, + months.int, years.int) + proc formatTimestamp(t: int): string = - let t2 = TTime(t) - return t2.getGMTime().format("yyyy-MM-dd'T'HH':'mm':'ss'Z'") + let t2 = Time(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" proc getGravatarUrl(email: string, size = 80): string = let emailMD5 = email.toLower.toMD5 diff --git a/public/css/style.css b/public/css/style.css index 903d22f..24b8801 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -166,9 +166,9 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #talk-heads { overflow:auto; margin:0 8px 0 8px; } #talk-heads > div { float:left; font-size:120%; font-weight:bold; } - #talk-heads > .topic { width:55%; } + #talk-heads > .topic { width:45%; } #talk-heads > .detail { width:15%; } - #talk-heads > .activity { 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; } @@ -203,7 +203,7 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } font-size: 10pt; } #talk-thread > div > div > div, - #talk-threads > div > div > div { margin: 5px 20px; } + #talk-threads > div > div > div { margin: 5px 10px; } #talk-thread > div > .topic { margin-top: 15pt; @@ -217,20 +217,35 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } border-bottom: 1px dashed; color: #3D3D3D; } - #talk-threads > div > .topic { width:55%; } + #talk-threads > div > .topic { width:45%; } #talk-threads > div > .users { width:15%; overflow:hidden; } #talk-threads > div > .users > div > img { margin-bottom: -4pt; cursor: help; } - #talk-threads > div > .detail { width:15%; overflow:hidden; } + #talk-threads > div > .detail { width:16%; overflow:hidden; } #talk-thread > div > .author, #talk-threads > div > .activity { - width:15%; 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: 8pt; + } + #talk-threads > div > .activity a + { + color: #1CB3EC; + } + #talk-threads > div > .activity a:hover + { + color: #ffffff; } #talk-thread > div > .author { height: 100%; @@ -251,7 +266,7 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } white-space: nowrap; } #talk-threads > div > .detail > div { float:left; margin:0; } - #talk-threads > div > .detail > div > div { margin-left:20px; padding: 5px 5px 5px 22px; } + #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; } diff --git a/public/js/forum.js b/public/js/forum.js index 2f354d3..7af672b 100644 --- a/public/js/forum.js +++ b/public/js/forum.js @@ -2,75 +2,4 @@ window.onload = function() { positionGlowArrow(); - processMainDates(); - window.setTimeout(performProcessingMainDates, 1000); }; - -function performProcessingMainDates() -{ - if (processMainDates()) - { - window.setTimeout(performProcessingMainDates, 1000); - } - else - { - window.setTimeout(performProcessingMainDates, 60000); - } -} - -function timeSince(date) { - var result = ["", false]; - var seconds = Math.floor((new Date() - date) / 1000); - - var interval = Math.floor(seconds / 31536000); - - if (interval >= 1) { - result[0] = interval + (interval == 1 ? " year ago" : " years ago"); - return result; - } - interval = Math.floor(seconds / 2592000); - if (interval >= 1) { - result[0] = interval + (interval == 1 ? " month ago" : " months ago"); - return result; - } - interval = Math.floor(seconds / 86400); - if (interval >= 1) { - result[0] = interval + (interval == 1 ? " day ago" : " days ago"); - return result; - } - interval = Math.floor(seconds / 3600); - if (interval >= 1) { - result[0] = interval + (interval == 1 ? " hour ago" : " hours ago"); - return result; - } - interval = Math.floor(seconds / 60); - if (interval >= 1) { - result[0] = interval + (interval == 1 ? " minute ago" : " minutes ago"); - return result; - } - if (seconds >= 1) { - result[0] = Math.floor(seconds) + - (seconds == 1 ? " second ago" : " seconds ago"); - result[1] = true; - return result; - } - return ["less than a second ago", true]; -} - -function processMainDates() { - var result = false; - var threads = document.getElementById("talk-threads").children; - for (var i = 0; i < threads.length; i++) - { - var activity = threads[i].getElementsByClassName("activity")[0]; - var activityDiv = activity.children[0]; - var isoDate = activityDiv.children[0].innerHTML; - var parsed = Date.parse(isoDate); - var timeS = timeSince(parsed); - - activityDiv.innerHTML = activityDiv.children[0].outerHTML + - timeS[0]; - if (timeS[1]) { result = timeS[1] } - } - return result; -} From 99731e7bedc62a9776ac3e428c6001847c62b0fc Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 4 Dec 2014 18:44:38 +0000 Subject: [PATCH 072/560] Added smilieys. Fixed too many users overflow on front page. --- forum.nim | 1 + public/css/style.css | 7 ++++++- public/images/smilieys/Thumbs.db | Bin 0 -> 44544 bytes public/images/smilieys/icon_cool.png | Bin 0 -> 2546 bytes public/images/smilieys/icon_e_biggrin.png | Bin 0 -> 2424 bytes public/images/smilieys/icon_e_confused.png | Bin 0 -> 2455 bytes public/images/smilieys/icon_e_sad.png | Bin 0 -> 2920 bytes public/images/smilieys/icon_e_smile.png | Bin 0 -> 2382 bytes public/images/smilieys/icon_e_surprised.png | Bin 0 -> 2791 bytes public/images/smilieys/icon_e_wink.png | Bin 0 -> 2526 bytes public/images/smilieys/icon_exclaim.png | Bin 0 -> 897 bytes public/images/smilieys/icon_mad.png | Bin 0 -> 2746 bytes public/images/smilieys/icon_neutral.png | Bin 0 -> 2193 bytes public/images/smilieys/icon_razz.png | Bin 0 -> 2508 bytes 14 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 public/images/smilieys/Thumbs.db create mode 100644 public/images/smilieys/icon_cool.png create mode 100644 public/images/smilieys/icon_e_biggrin.png create mode 100644 public/images/smilieys/icon_e_confused.png create mode 100644 public/images/smilieys/icon_e_sad.png create mode 100644 public/images/smilieys/icon_e_smile.png create mode 100644 public/images/smilieys/icon_e_surprised.png create mode 100644 public/images/smilieys/icon_e_wink.png create mode 100644 public/images/smilieys/icon_exclaim.png create mode 100644 public/images/smilieys/icon_mad.png create mode 100644 public/images/smilieys/icon_neutral.png create mode 100644 public/images/smilieys/icon_razz.png diff --git a/forum.nim b/forum.nim index c4d00c1..deaf3a2 100644 --- a/forum.nim +++ b/forum.nim @@ -939,6 +939,7 @@ routes: when isMainModule: docConfig = rstgen.defaultConfig() + docConfig["doc.smiley_format"] = "/images/smilieys/$1.png" math.randomize() db = open(connection="nimforum.db", user="postgres", password="", database="nimforum") diff --git a/public/css/style.css b/public/css/style.css index 24b8801..7452f5b 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -218,7 +218,7 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } color: #3D3D3D; } #talk-threads > div > .topic { width:45%; } - #talk-threads > div > .users { width:15%; overflow:hidden; } + #talk-threads > div > .users { width:15%; overflow:hidden; height: 30px; } #talk-threads > div > .users > div > img { margin-bottom: -4pt; @@ -593,3 +593,8 @@ form.searchNav { clear: both; height: 1px; } + +img.smiley { + width: 16px; + height: 16px; +} diff --git a/public/images/smilieys/Thumbs.db b/public/images/smilieys/Thumbs.db new file mode 100644 index 0000000000000000000000000000000000000000..98e5edc1ead700fab1b86c0518eb9757348e95af GIT binary patch literal 44544 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 V_9{I_@IL?m002ovPDHLkV1k+To}K^z literal 0 HcmV?d00001 diff --git a/public/images/smilieys/icon_e_sad.png b/public/images/smilieys/icon_e_sad.png new file mode 100644 index 0000000000000000000000000000000000000000..1ef02913f3c6febd1f25d77c279953d334bc6dcc GIT binary patch literal 2920 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 literal 0 HcmV?d00001 diff --git a/public/images/smilieys/icon_e_surprised.png b/public/images/smilieys/icon_e_surprised.png new file mode 100644 index 0000000000000000000000000000000000000000..60a82d4bb883a0f57abd1d2e49eac999505bd854 GIT binary patch literal 2791 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 literal 0 HcmV?d00001 diff --git a/public/images/smilieys/icon_e_wink.png b/public/images/smilieys/icon_e_wink.png new file mode 100644 index 0000000000000000000000000000000000000000..e32171de6f7855083b29e5bc5610b5746f23e24a GIT binary patch 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 literal 0 HcmV?d00001 diff --git a/public/images/smilieys/icon_exclaim.png b/public/images/smilieys/icon_exclaim.png new file mode 100644 index 0000000000000000000000000000000000000000..9391f2bea947414c2d869a08d3f1d1e8a2ba391e GIT binary patch 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 literal 0 HcmV?d00001 diff --git a/public/images/smilieys/icon_mad.png b/public/images/smilieys/icon_mad.png new file mode 100644 index 0000000000000000000000000000000000000000..3029c96f15110bc4c101388e92ef46ac425a6757 GIT binary patch literal 2746 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 Wty2bOP1`E~0000 Date: Fri, 5 Dec 2014 18:50:09 +0000 Subject: [PATCH 073/560] Better smiley positioning. Larger font size. --- public/css/style.css | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index 7452f5b..db7934d 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -200,7 +200,7 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } float:left; text-overflow: ellipsis; overflow: hidden; - font-size: 10pt; + font-size: 13pt; } #talk-thread > div > div > div, #talk-threads > div > div > div { margin: 5px 10px; } @@ -595,6 +595,8 @@ form.searchNav { } img.smiley { - width: 16px; - height: 16px; + width: 20px; + height: 20px; + vertical-align: middle; + margin: 0; } From cf6e99223af7f1e93e7c245a941dba53edd12ae6 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 5 Dec 2014 18:56:01 +0000 Subject: [PATCH 074/560] Adjusted top margin for post content. --- 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 a7ee62f..83948a5 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -203,7 +203,7 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } font-size: 13pt; } #talk-thread > div > div > div, - #talk-threads > div > div > div { margin: 5px 10px; } + #talk-threads > div > div > div { margin: 15px 10px; } #talk-thread > div > .topic { margin-top: 15pt; From 5b30eddd16708286e7044d6dc833fa508ca1aafd Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 6 Dec 2014 23:31:55 +0000 Subject: [PATCH 075/560] Remove stupid Thumbs.db. --- public/images/smilieys/Thumbs.db | Bin 44544 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 public/images/smilieys/Thumbs.db diff --git a/public/images/smilieys/Thumbs.db b/public/images/smilieys/Thumbs.db deleted file mode 100644 index 98e5edc1ead700fab1b86c0518eb9757348e95af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44544 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 F@?R Date: Sat, 6 Dec 2014 23:32:29 +0000 Subject: [PATCH 076/560] gitignore *.db --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7c88dfb..3b9d320 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Wildcard patterns. *.swp nimcache/ +*.db # Specific paths /createdb From 4002d121b4863cc48ecd8dad898696e430562f94 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 7 Dec 2014 00:32:21 +0000 Subject: [PATCH 077/560] Fixes recently introduced style issues. --- 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 83948a5..c53854b 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -202,8 +202,8 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } overflow: hidden; font-size: 13pt; } - #talk-thread > div > div > div, - #talk-threads > div > div > div { margin: 15px 10px; } + #talk-threads > div > div > div { margin: 5px 10px; } + #talk-thread > div > div > div { margin: 15px 10px; } #talk-thread > div > .topic { margin-top: 15pt; From e11f5d93455bcd6f73d3ca35b140ab8fa49c803d Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 7 Dec 2014 00:48:24 +0000 Subject: [PATCH 078/560] Removes subject field when replying to thread. --- forms.tmpl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index 1675aeb..c649f22 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -166,8 +166,12 @@
- ${FieldValid(c, "subject", "Subject:")} - ${TextWidget(c, "subject", title, maxlength=100)} + #if action == "doreply": + + #else: + ${FieldValid(c, "subject", "Subject:")} + ${TextWidget(c, "subject", title, maxlength=100)} + #end if
${FieldValid(c, "content", "Content:")}
${TextAreaWidget(c, "content", content, width=100, height=20)}
From 048180dd8b22175ab618487406b31d77199b6411 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 7 Dec 2014 01:05:16 +0000 Subject: [PATCH 079/560] Larger subject field. --- forms.tmpl | 4 ++-- forum.nim | 7 +++---- public/css/style.css | 12 ++++++++++++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index c649f22..04397f4 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -171,10 +171,10 @@ #else: ${FieldValid(c, "subject", "Subject:")} ${TextWidget(c, "subject", title, maxlength=100)} +
#end if -
${FieldValid(c, "content", "Content:")}
- ${TextAreaWidget(c, "content", content, width=100, height=20)}
+ ${TextAreaWidget(c, "content", content)}
${FormSession(c, action)} # if isEdit: diff --git a/forum.nim b/forum.nim index deaf3a2..ef70254 100644 --- a/forum.nim +++ b/forum.nim @@ -99,12 +99,11 @@ proc TextWidget(c: TForumData, name, defaultText: string, return """""" % [ name, $maxlength, x, if size != -1: "size=\"" & $size & "\"" else: ""] -proc TextAreaWidget(c: TForumData, name, defaultText: string, - width = 80, height = 20): string = +proc TextAreaWidget(c: TForumData, name, defaultText: string): string = let x = if defaultText != reuseText: defaultText else: xmlEncode(c.req.params[name]) - return """""" % [ - name, $width, $height, x] + return """""" % [ + name, x] proc FieldValid(c: TForumData, name, text: string): string = if name == c.invalidField: diff --git a/public/css/style.css b/public/css/style.css index c53854b..b3f11c6 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -521,6 +521,18 @@ 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; From 32b9c27da94c7f52bd8a712331ba6136d637f35f Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 7 Dec 2014 01:26:54 +0000 Subject: [PATCH 080/560] Fixes Nim logo's incorrect url. --- main.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.tmpl b/main.tmpl index cd96182..5dccf05 100644 --- a/main.tmpl +++ b/main.tmpl @@ -21,7 +21,7 @@
${FieldValid(c, "name", "Username:")}${TextWidget(c, "antibot", "", maxlength=4)}
+ #if c.errorMsg != "": +
+ $c.errorMsg +
+ #end if
#end proc diff --git a/main.tmpl b/main.tmpl index 5dccf05..67aa9c4 100644 --- a/main.tmpl +++ b/main.tmpl @@ -120,7 +120,7 @@ - #if c.errorMsg != "": + #if c.errorMsg != "" and c.req.pathInfo.normalizeUri == "/dologin": $c.errorMsg #end if Date: Sun, 7 Dec 2014 02:09:33 +0000 Subject: [PATCH 082/560] Add atom feed icons. --- forms.tmpl | 18 ++++++++++++++++-- public/css/style.css | 6 ++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index e93fe49..1b4ff18 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -14,10 +14,24 @@ # result = "" # count = 0
-
Topic
+
Users
Details
-
Activity
+
+
+ Activity + + + +
+
# for row in rows(db, query, $((c.pageNum-1) * ThreadsPerPage), $ThreadsPerPage): diff --git a/public/css/style.css b/public/css/style.css index 9340d3d..2568983 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -618,3 +618,9 @@ img.smiley { vertical-align: middle; margin: 0; } + +img.rssfeed { + width: 16px; + float: right; + margin-top: 10px; +} From 38d386beb9e33852d461469d8b1ff2f0d28f6fb4 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 7 Dec 2014 02:13:56 +0000 Subject: [PATCH 083/560] Increased activity font size. --- 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 2568983..31ff63e 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -237,7 +237,7 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } } #talk-threads > div > .activity { width:24%; - font-size: 8pt; + font-size: 9pt; } #talk-threads > div > .activity a { From 3a975761605f563ff518348d07c8791e464afcd7 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 7 Dec 2014 02:17:16 +0000 Subject: [PATCH 084/560] Made syntax highlighting white slightly less white. --- 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 31ff63e..7ebfafd 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -9,7 +9,7 @@ body { font:13pt "arial"; background:#152534 url("/images/bg.jpg") no-repeat fixed center top; } -pre { color: #ffffff;} +pre { color: #F5F5F5;} pre, pre * { cursor:text; } pre .Comment { color:#6D6D6D; font-style:italic; } pre .Keyword { color:#43A8CF; font-weight:bold; } From 8af27ae64db1c00b96761d97fbdccf227f998223 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 7 Dec 2014 02:53:10 +0000 Subject: [PATCH 085/560] Improves search. --- forms.tmpl | 19 +++++-------------- fts.sql | 2 +- public/css/style.css | 15 ++++++++++++--- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/forms.tmpl b/forms.tmpl index 1b4ff18..b39153d 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -329,26 +329,17 @@
- #if %threadName != %postHeader and "Re: " & %threadName != %postHeader: - #headersDiffer = true -

- - Thread: - ${%threadName} - -

- #end if - #if not headersDiffer or %postHeader != "": -
+ #if %postHeader != "": +
+
#end if #if not isThread: #try: - ${(%postContent)} + ${(%postContent).rstToHtml} #except EParseError: # c.errorMsg = getCurrentExceptionMsg() #end diff --git a/fts.sql b/fts.sql index ab49d31..777eb99 100644 --- a/fts.sql +++ b/fts.sql @@ -43,7 +43,7 @@ SELECT THEN snippet(post_fts, '', '', '...', what) ELSE post_fts.header END AS header, CASE what WHEN 2 - THEN snippet(post_fts, '', '', '...', what, -45) + THEN snippet(post_fts, '**', '**', '...', what, -45) ELSE SUBSTR(post_fts.content, 1, 200) END AS content, person.name AS author, cdate, diff --git a/public/css/style.css b/public/css/style.css index 7ebfafd..9508e68 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -192,7 +192,6 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } line-height: 150%; background:rgba(0,0,0,0.1); } - #talk-thread > div:nth-child(odd) { background:rgba(255,255,255,0.1); } #talk-threads > div:nth-child(odd) { background:rgba(0,0,0,0.2); } #talk-thread > div > div, #talk-threads > div > div @@ -558,7 +557,7 @@ hr /* highlighting current post */ div:target { - background: rgba(255, 255, 240, 0.25) !important; + background: rgba(139, 218, 255, 0.25) !important; } /* full-text search */ @@ -569,7 +568,17 @@ div:target { } .titleHeader { margin-right: 1em; - color: #ddd; + 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 { From 4b6d1d3a3238ba1c269932f39f33b5da0882cbbe Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 7 Dec 2014 14:01:58 +0000 Subject: [PATCH 086/560] Increased margin on post content. --- public/css/style.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/css/style.css b/public/css/style.css index 9508e68..37da28e 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -208,6 +208,10 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } margin-top: 15pt; white-space: normal; } + #talk-thread > div > .topic > div + { + margin-left: 15px; + } #talk-thread > div > .topic > div > span.date { position: absolute; From 98127731a29fa5b6b168d8cb783a5e862182a6ce Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 7 Dec 2014 14:03:21 +0000 Subject: [PATCH 087/560] Remove italics from num lit syntax highlighting. --- 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 37da28e..0e0ab6b 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -18,7 +18,7 @@ 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; font-style:italic; } -pre .DecNumber, pre .FloatNumber { color:#8AB647; font-style:italic; } +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; } From 90d7a70556bc8f3f2edab48c111354e15bc72359 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 7 Dec 2014 14:04:50 +0000 Subject: [PATCH 088/560] Inline code in posts now has a white background. --- 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 0e0ab6b..bf47ca7 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -23,7 +23,7 @@ pre .tab { border-left:1px dotted rgba(67,168,207,0.4); } pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } .tall { height:100%; } -.pre { padding:0 5px; font:11pt monospace; background:rgba(255,255,255,.15); border-radius:3px; } +.pre { padding:0 5px; font:11pt monospace; background:rgba(255,255,255,.30); border-radius:3px; } .page-layout { margin:0 auto; width:1000px; } .docs-layout { margin:0 40px; } From 82843d5bc255b9c771a1191b80003b09489a2898 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 7 Dec 2014 14:21:03 +0000 Subject: [PATCH 089/560] Increased effective clickable size of Reply button. --- main.tmpl | 10 ++++++---- public/css/style.css | 13 +++++++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/main.tmpl b/main.tmpl index 67aa9c4..5a6cd5a 100644 --- a/main.tmpl +++ b/main.tmpl @@ -72,10 +72,12 @@
-
- #let replyUri = c.req.makeUri(c.req.path & "?action=reply#reply") - Reply -
+ #let replyUri = c.req.makeUri(c.req.path & "?action=reply#reply") + +
+ Reply +
+
#end if diff --git a/public/css/style.css b/public/css/style.css index bf47ca7..86f05e7 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -319,9 +319,18 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #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 > div > .reply { font-weight:bold; padding-left:22px; background:url("/images/forum-reply.png") no-repeat left; } + #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 { padding:5px 20px; color: #1a1a1a; } + #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; } From 4ba9de012ccacbdb04a9f186e60e03dd60d6331b Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 7 Dec 2014 14:26:59 +0000 Subject: [PATCH 090/560] Only show reply button if user is logged in. --- forum.nim | 4 ++++ main.tmpl | 14 ++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/forum.nim b/forum.nim index ef70254..3287c76 100644 --- a/forum.nim +++ b/forum.nim @@ -479,6 +479,10 @@ proc login(c: var TForumData, name, 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" + # 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 = diff --git a/main.tmpl b/main.tmpl index 5a6cd5a..7b505db 100644 --- a/main.tmpl +++ b/main.tmpl @@ -72,12 +72,14 @@
- #let replyUri = c.req.makeUri(c.req.path & "?action=reply#reply") - -
- Reply -
-
+ #if c.loggedIn(): + #let replyUri = c.req.makeUri(c.req.path & "?action=reply#reply") + +
+ Reply +
+
+ #end if
#end if From 9e5c05fe63a7e507272346b4b925cd331a9e1368 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 7 Dec 2014 14:40:36 +0000 Subject: [PATCH 091/560] Adds more space below "Edit Post". --- 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 86f05e7..412133e 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -274,7 +274,7 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } #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:150px; padding-bottom: 10pt; } + #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; } From 98acb26f2788f144fc51562c207d58b974ec64c2 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 7 Dec 2014 14:50:14 +0000 Subject: [PATCH 092/560] Better fonts. DejaVu Sans Mono and Helvetica. --- public/css/style.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index 412133e..6e5edd4 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -6,7 +6,7 @@ body { overflow-x:hidden; min-width:1030px; margin:0; - font:13pt "arial"; + font: 13pt Helvetica,Arial,sans-serif; background:#152534 url("/images/bg.jpg") no-repeat fixed center top; } pre { color: #F5F5F5;} @@ -23,7 +23,7 @@ pre .tab { border-left:1px dotted rgba(67,168,207,0.4); } pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } .tall { height:100%; } -.pre { padding:0 5px; font:11pt monospace; background:rgba(255,255,255,.30); border-radius:3px; } +.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; } @@ -289,6 +289,7 @@ pre .end { background:url("/images/tabEnd.png") no-repeat left bottom; } 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 { From fd08340b8fb2027e81a88c67589d3410dfb8a70a Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 7 Dec 2014 14:54:03 +0000 Subject: [PATCH 093/560] Syntax highlighting improvements. --- public/css/style.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/css/style.css b/public/css/style.css index 6e5edd4..d6c2147 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -17,10 +17,14 @@ 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; 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; } From bf80d4bf73132976ee2667596ff6d1906c398f93 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 7 Dec 2014 18:07:05 +0000 Subject: [PATCH 094/560] Enabled bcrypt on Linux. Closes #25. --- forum.nim | 9 ++++++++- nimforum.babel | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/forum.nim b/forum.nim index 3287c76..76613dd 100644 --- a/forum.nim +++ b/forum.nim @@ -9,6 +9,10 @@ import os, strutils, times, md5, strtabs, cgi, math, db_sqlite, matchers, rst, rstgen, captchas, scgi, jester, asyncdispatch, asyncnet, cache, sequtils + +when not defined(windows): + import bcrypt # TODO + from htmlgen import tr, th, td, span const @@ -221,7 +225,10 @@ proc makeSalt(): string = proc makePassword(password, salt: string): string = ## Creates an MD5 hash by combining password and salt. - result = getMD5(salt & getMD5(password)) + when defined(windows): + result = getMD5(salt & getMD5(password)) + else: + result = hash(getMD5(salt & getMD5(password))) # ----------------------------------------------------------------------------- template `||`(x: expr): expr = (if not isNil(x): x else: "") diff --git a/nimforum.babel b/nimforum.babel index 6c6f161..bcd7c68 100644 --- a/nimforum.babel +++ b/nimforum.babel @@ -8,4 +8,4 @@ license = "MIT" bin = "forum" [Deps] -Requires: "nimrod >= 0.9.2, cairo#head, jester#head" +Requires: "nimrod >= 0.9.2, cairo#head, jester#head, bcrypt#head" From 0ebc9e1d0be21b4a01b89e5323e52e3dc980f306 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 7 Dec 2014 18:35:37 +0000 Subject: [PATCH 095/560] Add salt param to hash(). --- forum.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forum.nim b/forum.nim index 76613dd..5f5ccdc 100644 --- a/forum.nim +++ b/forum.nim @@ -228,7 +228,7 @@ proc makePassword(password, salt: string): string = when defined(windows): result = getMD5(salt & getMD5(password)) else: - result = hash(getMD5(salt & getMD5(password))) + result = hash(getMD5(salt & getMD5(password)), genSalt(8)) # ----------------------------------------------------------------------------- template `||`(x: expr): expr = (if not isNil(x): x else: "") From 4f059984ad38eb4da19500d9a9d2b6c94adb459e Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 7 Dec 2014 19:09:36 +0000 Subject: [PATCH 096/560] Fixes bcrypt. --- forum.nim | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/forum.nim b/forum.nim index 5f5ccdc..95ab14c 100644 --- a/forum.nim +++ b/forum.nim @@ -223,12 +223,13 @@ proc makeSalt(): string = except IOError: result = randomSalt() -proc makePassword(password, salt: string): string = +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: - result = hash(getMD5(salt & getMD5(password)), genSalt(8)) + let salt = if comparingTo != "": comparingTo else: genSalt(8) + result = hash(getMD5(salt & getMD5(password)), comparingTo) # ----------------------------------------------------------------------------- template `||`(x: expr): expr = (if not isNil(x): x else: "") @@ -466,7 +467,7 @@ proc login(c: var TForumData, name, pass: string): bool = 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]): + if row[2] == makePassword(pass, row[4], row[2]): c.userid = row[0] c.username = row[1] c.userpass = row[2] From ad02b4e71041b32bee3c09ef497fa3b72ad3f622 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 7 Dec 2014 19:11:29 +0000 Subject: [PATCH 097/560] Fixes silly var name mistake. --- forum.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/forum.nim b/forum.nim index 95ab14c..5ac4ad6 100644 --- a/forum.nim +++ b/forum.nim @@ -228,8 +228,8 @@ proc makePassword(password, salt: string, comparingTo = ""): string = when defined(windows): result = getMD5(salt & getMD5(password)) else: - let salt = if comparingTo != "": comparingTo else: genSalt(8) - result = hash(getMD5(salt & getMD5(password)), comparingTo) + let bcryptSalt = if comparingTo != "": comparingTo else: genSalt(8) + result = hash(getMD5(salt & getMD5(password)), bcryptSalt) # ----------------------------------------------------------------------------- template `||`(x: expr): expr = (if not isNil(x): x else: "") From 3611b42aedb29233b5b76512287339125e9dcf7f Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 7 Dec 2014 20:05:49 +0000 Subject: [PATCH 098/560] Fixed link to atom feed. --- forms.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forms.tmpl b/forms.tmpl index b39153d..e21b439 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -27,7 +27,7 @@
Activity - +
From 638aaa44c0c6af401c966c2cc65eddc1ce3c7b63 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 7 Dec 2014 21:13:34 +0000 Subject: [PATCH 099/560] Fix top links. --- main.tmpl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main.tmpl b/main.tmpl index 7b505db..d01b363 100644 --- a/main.tmpl +++ b/main.tmpl @@ -24,11 +24,11 @@
From dfac36593dcf48ba2906f9f001eb48db02777715 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 7 Dec 2014 21:48:40 +0000 Subject: [PATCH 100/560] Fixed more links at the bottom of the page. --- main.tmpl | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/main.tmpl b/main.tmpl index d01b363..0227376 100644 --- a/main.tmpl +++ b/main.tmpl @@ -143,21 +143,20 @@ From 5dc95b27cbb0d2f96f8545bc1ae1bb80c1df9fb2 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 8 Dec 2014 22:15:32 +0000 Subject: [PATCH 101/560] Fixes #33 --- forms.tmpl | 2 +- forum.nim | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/forms.tmpl b/forms.tmpl index e21b439..e215afd 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -181,7 +181,7 @@
#if action == "doreply": - + ${HiddenField(c, "subject", title)} #else: ${FieldValid(c, "subject", "Subject:")} ${TextWidget(c, "subject", title, maxlength=100)} diff --git a/forum.nim b/forum.nim index 5ac4ad6..d330dbe 100644 --- a/forum.nim +++ b/forum.nim @@ -103,6 +103,11 @@ proc TextWidget(c: TForumData, name, defaultText: string, 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]) + return """""" % [name, x] + proc TextAreaWidget(c: TForumData, name, defaultText: string): string = let x = if defaultText != reuseText: defaultText else: xmlEncode(c.req.params[name]) From 1ebaba235546c06c3a915440658a8df55cf89d0b Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 8 Dec 2014 22:28:59 +0000 Subject: [PATCH 102/560] Fixes #34. --- forms.tmpl | 1 + 1 file changed, 1 insertion(+) diff --git a/forms.tmpl b/forms.tmpl index e215afd..d5be9e2 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -342,6 +342,7 @@ ${(%postContent).rstToHtml} #except EParseError: # c.errorMsg = getCurrentExceptionMsg() + ${xmlEncode(%postContent)} #end #end if ${xmlEncode(%postCreation)} From f678a3106084c6f8d57d36d7acfd0f4e52a15814 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 30 Dec 2014 15:19:28 +0000 Subject: [PATCH 103/560] Implements #35. --- main.tmpl | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/main.tmpl b/main.tmpl index 0227376..7d9a489 100644 --- a/main.tmpl +++ b/main.tmpl @@ -161,6 +161,17 @@ + + From 50cced23dfcaa4bdcfdaeec948ab51df4ecaab4f Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 30 Dec 2014 15:24:17 +0000 Subject: [PATCH 104/560] Ported fix to #16. --- forum.nim | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/forum.nim b/forum.nim index d330dbe..4508802 100644 --- a/forum.nim +++ b/forum.nim @@ -894,7 +894,8 @@ routes: createTFD() readIDs() if edit(c, c.postId): - redirect(c.genThreadUrl(postId = $c.postId)) + redirect(c.genThreadUrl(postId = $c.postId, + pageNum = $(c.getPagesInThread+1))) else: body = "" handleError("doedit", "Edit", true) From cb9ac61876ce3665308c9d96de40029c523cdc0f Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 30 Dec 2014 15:34:23 +0000 Subject: [PATCH 105/560] Ported fix to #17 --- forum.nim | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/forum.nim b/forum.nim index 4508802..fb28493 100644 --- a/forum.nim +++ b/forum.nim @@ -759,6 +759,11 @@ routes: 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: From a1c5c47c2ea6d3ea9666cbc02f702f12b7baa351 Mon Sep 17 00:00:00 2001 From: Flaviu Tamas Date: Sun, 18 Jan 2015 08:45:16 -0500 Subject: [PATCH 106/560] Update forms.tmpl --- forms.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forms.tmpl b/forms.tmpl index d5be9e2..032e6f1 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -203,7 +203,7 @@ - Syntax Cheatsheet + Syntax Cheatsheet #end proc From a472be41d8d710b66e09b024ecaaaf0c89006963 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 21 Jan 2015 00:12:24 +0000 Subject: [PATCH 107/560] Fix thread location not being updated when post is deleted. A thread table has its own modified attribute which needed to be reset. --- forum.nim | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/forum.nim b/forum.nim index fb28493..7f1bc69 100644 --- a/forum.nim +++ b/forum.nim @@ -425,6 +425,14 @@ proc edit(c: var TForumData, postId: int): bool = # 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 threads 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 else: checkOwnership(c, $postId) From 72f8126d01efe9a0ff99dec04a7788473644fea6 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 21 Jan 2015 00:29:50 +0000 Subject: [PATCH 108/560] Modify the thread title if first post's title is edited. --- forum.nim | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/forum.nim b/forum.nim index 7f1bc69..839e76e 100644 --- a/forum.nim +++ b/forum.nim @@ -426,7 +426,7 @@ proc edit(c: var TForumData, postId: int): bool = c.threadId = unselectedThread discard tryExec(db, sql"delete from thread_fts where id not in (select thread from post)") else: - # Update corresponding threads modified field. + # 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 & @@ -441,6 +441,11 @@ proc edit(c: var TForumData, postId: int): bool = subject, content, $postId) exec(db, crud(crUpdate, "post_fts", "header", "content"), subject, content, $postId) + # Check if post is the first post of the thread. + let rows = db.getAllRows(sql("select id, thread, creation from post " & + "where thread = ? order by creation asc"), $c.threadId) + if rows[0][0] == $postId: + exec(db, crud(crUpdate, "thread", "name"), subject, $c.threadId) result = true proc reply(c: var TForumData): bool = From 57ee631735da8bbef5a0adc1201fcc88e6e864cc Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 14 Feb 2015 14:13:36 +0000 Subject: [PATCH 109/560] Implement user bans and deactivations. Fix registration issues. --- createdb.nim | 5 ++ forum.nim | 127 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 128 insertions(+), 4 deletions(-) diff --git a/createdb.nim b/createdb.nim index 3b601ec..1566089 100644 --- a/createdb.nim +++ b/createdb.nim @@ -42,6 +42,11 @@ create table if not exists person( );""" % [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); """, []) diff --git a/forum.nim b/forum.nim index 839e76e..1ce63f9 100644 --- a/forum.nim +++ b/forum.nim @@ -24,6 +24,7 @@ const MaxPagesFromCurrent = 8 noPageNums = ["/login", "/register", "/dologin", "/doregister", "/profile"] noHomeBtn = ["/", "/login", "/register", "/dologin", "/doregister", "/profile"] + banReasonDeactivated = "DEACTIVATED" type TCrud = enum crCreate, crRead, crUpdate, crDelete @@ -65,6 +66,7 @@ type threads: int lastOnline: int email: string + ban: string var db: TDbConn @@ -223,11 +225,19 @@ proc devRandomSalt(): string = 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 .. " + 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 + formBody.add "" + content = htmlgen.form(action = c.req.makeUri("/dosetban"), + `method` = "POST", formBody) & content + resp genMain(c, content, "Set user status - Nim Forum") + + post "/dosetban": + createTFD() + 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": + 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") + const licenseRst = slurp("static/license.rst") get "/license": createTFD() From 8d800c753b20f3d74ab97091ab1d4c6216b7ddf1 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 14 Feb 2015 14:14:44 +0000 Subject: [PATCH 110/560] Nimrod -> Nim --- forum.nim | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/forum.nim b/forum.nim index 1ce63f9..4aa0338 100644 --- a/forum.nim +++ b/forum.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. @@ -867,12 +867,12 @@ routes: body = genFormPost(c, "doedit", "Edit", header, content, true) title = "Editing post" else: discard - resp c.genMain(body, title & " - Nimrod Forum") + 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 & " - Nimrod Forum") + resp genMain(c, posts, pSubject & " - Nim Forum") get "/page/?@page?/?": createTFD() @@ -884,7 +884,7 @@ routes: let list = genThreadsList(c, count) if count == 0: pass() - resp genMain(c, list, "Page " & $c.pageNum & " - Nimrod Forum", + resp genMain(c, list, "Page " & $c.pageNum & " - Nim Forum", genRSSHeaders(c), showRssLinks = true) get "/profile/@nick/?": @@ -893,13 +893,13 @@ routes: var userinfo: TUserInfo if gatherUserInfo(c, @"nick", userinfo): resp genMain(c, c.genProfile(userinfo), - @"nick" & "'s profile - Nimrod Forum") + @"nick" & "'s profile - Nim Forum") else: halt() get "/login/?": createTFD() - resp genMain(c, genFormLogin(c), "Log in - Nimrod Forum") + resp genMain(c, genFormLogin(c), "Log in - Nim Forum") get "/logout/?": createTFD() @@ -908,7 +908,7 @@ routes: get "/register/?": createTFD() - resp genMain(c, genFormRegister(c), "Register - Nimrod Forum") + resp genMain(c, genFormRegister(c), "Register - Nim Forum") template readIDs(): stmt = # Retrieve the threadid, postid and pagenum @@ -926,7 +926,7 @@ routes: body.add genPostPreview(c, @"subject", @"content", c.userName, $getGMTime(getTime())) body.add genFormPost(c, action, topText, reuseText, reuseText, isEdit) - resp genMain(c, body(), "Nimrod Forum - " & + resp genMain(c, body(), "Nim Forum - " & (if c.isPreview: "Preview" else: "Error")) post "/dologin": @@ -982,7 +982,7 @@ routes: get "/newthread/?": createTFD() resp genMain(c, genFormPost(c, "donewthread", "New thread", "", "", false), - "New Thread - Nimrod Forum") + "New Thread - Nim Forum") get "/setUserStatus/?": createTFD() @@ -1045,7 +1045,7 @@ routes: const licenseRst = slurp("static/license.rst") get "/license": createTFD() - resp genMain(c, rstToHtml(licenseRst), "Content license - Nimrod Forum") + resp genMain(c, rstToHtml(licenseRst), "Content license - Nim Forum") post "/search/?@page?": cond isFTSAvailable From e61f2462b742faf2e418ec611a6a2e5d0a823d9e Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 14 Feb 2015 14:19:44 +0000 Subject: [PATCH 111/560] Fixes navigation bar. Updates copyright year. --- main.tmpl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/main.tmpl b/main.tmpl index 7d9a489..235e4aa 100644 --- a/main.tmpl +++ b/main.tmpl @@ -25,9 +25,9 @@ @@ -144,7 +144,7 @@

Community

@@ -156,7 +156,7 @@
From a03e5a9bd330ca0c4ace6751572eac8f315a9f0d Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 14 Feb 2015 14:22:25 +0000 Subject: [PATCH 112/560] Fixes #36 --- forms.tmpl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/forms.tmpl b/forms.tmpl index 032e6f1..cd0133d 100644 --- a/forms.tmpl +++ b/forms.tmpl @@ -124,6 +124,8 @@ # result = "" # count = 0 # let posts = getAllRows(db, query, threadId, $((c.pageNum-1) * PostsPerPage), $PostsPerPage) +# if posts.len < 1: return "" +# end if
From a12349427df7dede2469feebb1172132a905cf23 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 14 Feb 2015 14:33:21 +0000 Subject: [PATCH 113/560] 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 114/560] 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 115/560] 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 116/560] 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 117/560] 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 118/560] 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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

_;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 119/560] 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 120/560] 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 121/560] 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 122/560] 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 123/560] 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 124/560] 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 @@

- - -
-
-
-
-
- +
@@ -83,7 +76,7 @@ #end if - + #else:
Login
@@ -136,7 +129,7 @@
- +
${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) & "?")}
-#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 151/560] 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 152/560] 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 153/560] 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 154/560] 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 155/560] 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 156/560] 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 157/560] 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 158/560] 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 159/560] 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 160/560] 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 161/560] 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 162/560] 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 163/560] 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 164/560] 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 165/560] 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 166/560] 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 167/560] 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 168/560] =?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 169/560] 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)}
${fieldValid(c, "antibot", "What is " & antibot(c) & "?")}${textWidget(c, "antibot", "", maxlength=4)}${fieldValid(c, "g-recaptcha-response", "Captcha:")}${captcha.render(includeNoScript=true)}
#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 170/560] 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:")}
${fieldValid(c, "antibot", "What is " & antibot(c) & "?")}${textWidget(c, "antibot", "", maxlength=4)}${fieldValid(c, "g-recaptcha-response", "Captcha:")}${captcha.render(includeNoScript=true)}
#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 171/560] 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 172/560] 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 173/560] 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 174/560] 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 175/560] 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 176/560] 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 177/560] 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 178/560] 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 179/560] 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 180/560] 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 181/560] 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 182/560] 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 183/560] 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 184/560] 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 185/560] 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 186/560] 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 187/560] 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 188/560] 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 189/560] 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 190/560] 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 191/560] 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 192/560] 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 193/560] 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 194/560] 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 195/560] 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 196/560] 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 197/560] 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 198/560] 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 199/560] 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 200/560] 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 201/560] 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 202/560] 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 203/560] 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 204/560] 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 208/560] 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 209/560] 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 210/560] 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 211/560] 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 212/560] 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 213/560] 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 218/560] 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 219/560] 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 220/560] 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 221/560] 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 222/560] 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 223/560] 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 224/560] 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 225/560] 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 226/560] 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 227/560] 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 228/560] 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 229/560] 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 230/560] 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 231/560] 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 232/560] 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 233/560] 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 234/560] 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 235/560] 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 236/560] 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 237/560] 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 238/560] 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 239/560] 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 240/560] 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 241/560] 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 242/560] 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 243/560] 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 244/560] 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 245/560] 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 246/560] 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 247/560] 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 248/560] 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 249/560] 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 250/560] 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 251/560] 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 252/560] 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 253/560] 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 254/560] 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 255/560] 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 256/560] 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 257/560] 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 258/560] 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 259/560] 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 260/560] 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 261/560] 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 262/560] 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 263/560] 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 264/560] 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 265/560] 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 266/560] 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 267/560] 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 268/560] 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 269/560] 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 270/560] 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 271/560] 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 272/560] 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 273/560] 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 274/560] 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 275/560] 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 276/560] 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 277/560] 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 278/560] 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 279/560] 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 280/560] 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 281/560] 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 282/560] 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 283/560] 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 284/560] 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 285/560] 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 286/560] 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 287/560] 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 288/560] 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 289/560] 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 290/560] 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 291/560] 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 292/560] 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 293/560] 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 294/560] 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 295/560] 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 296/560] 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 297/560] 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 298/560] 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 299/560] 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 300/560] 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 301/560] 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 302/560] 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 303/560] 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 zcmXt;c{o)6`~T0G#WMDNXY9KfJ6UEV5rv{CYE(icOUiEM$Xb-GqP#VVq9~&5%n(t8 zsFcKzwZzy5vwVH7-}U=v&dglr%-r|uT=#uHp3m30>F(+%fRaT403dM4>A*1nK<>ST z4f4SE4vZ3ge*l03?uR`cB-Bl$HBF^8&1BV$q}7dOHBF_|O(fNgCHG!6OeK^JBvddm z8pe9qeR>8~ikij}YK9VOMpEiVvf36pMtBLdo`jN)2FA+3*jCrfPEFTbNyi+ct}m@+ zrh~E3)i#k(H^!*x$tr4SY8uOHnn){Y>6qH-8{0{1n8+%q%W0WOsO^1kYJjoS)-jbv z>zEo^NT?brDQn5A8>wMzbaae$G5hokZ4`AabdBufwJqdzES1!a6m<8=E2iqNSv_Pe)l@Th~kvYa^wsZ=h$UtZlxRsiKytyc$+U%Un`P z%Lr?!sb#2dYNujgEv2lhu4SgLtfj52D~s0FGs5X0vAkbd0pMu*y0XQmPmQMJ+9?t&FmszLAZZx}l63 zRzX=CqitxcZ=#6S(KED_SJpGuG1OPn(bBV2RL5#*8EG2YsvF`I6*Y7;jkGcQG;~a` zD%y$)D*EaMk_sC7T83)6mI^8cYU&1h2A0wa>gpOsSi^m0MwV&{Dp<@uEknGvv7Lst zxrwfkhWt+fYs0bZ<4a z46L-&u`;R{Wp%89zNx&PwXA}QhPu9-ih;7cl8BUop{kw%##~xX)dXXvV~CTMQDhB}KaG<-g`mzucBpAVT-5$170H5!FvwmLjCDLnWo zvfdYqxNczJRBTXv8i6Wxej1uBgnql+>+@ZDe9oHlV0+*}#GU>=r<&Fu15Auj z`K`D|#%uF^ZXK+zWa6pQt&|tc_~l0@iTAtSXu3Tr*xdZ^?PtB)cw0h2SQz@>jQzsa z=9#v!xYzadTjvPT%Dow0mgAKC0EWg9OY6GL^Rv6a%CK@>s_QO=pX0~VM zbIFlar<{a)Iee2>GoJ32Uv9u!OR?Q;QLV{YsBfiL%nN@S3c}|#KfxOO)*@wY*GLS? z^L?6KSYViRVq|IzC8Nv#(=L2=JNkp$v)#yO#=qhE(4j-U(NBL>ta;7RQv8~33tUUh z=dOXGmi=?8uUBs@{rdLp{u^suAD^ig_au&fZPncJGZ6Ur=TGa`vWlAYF*WP2VVpL- zt>bY#Q70iu&71JJcaO{6#x#q9$CEi)xMLkewR4pDi-pt;GJfNDwbG1KjZz{|vh#D% zd$T#>T6@X`Y8Ew+`Y$s;0?d7ubY?NKc+DjH_2Y}4l9Eb>ak@P2*k5rj(A=B~-kkvH z@`=+DB#PxLTe>bN?uUewsWv=eO0ak#aPN~6nre&qo(8y;J^;WvH1#@9m;$UNHvoo+ ze*4nb9vTrYM!-8hs3W2+?CoJ$N%l-}W-=Rf4-3`6~sG2Y{R+WdSkqj`e*V&9%r3ijE z@sw^38F!!UK7J<=!*(*l&qBTurJ<HaBG&}2}8n|W&DX^yLOQIMauc`6MtTwUj~hXer{%vk-I4ny90mQ zZaeYVrs`}+v3%@wE=t(v>2!UJamkt6$>(XybpUDGX}(SCD5pJc^p`eEnI+ddZn)09 z?U1+;gug@7#(y5yv`en%0Yk1R6x1P?8UR8mOwC-CDpyS&JYz1&r)Ze5VRn-&G2*kh z_AB!-^Iz)hA)55q;9s@j0(IN!z1%x=@d#oGOw zQL<8K0j;vjNxO7)!oeptd0@XfHHp*)z+ng6!0`eW^Z}L<*){DZq%s{2Xb9h40oPJ4;{%}e`;hJa1EhwYmkk)y`&ailhk*<4sp7>S zv*Ih%@sXTs;AUYf1ndsMQPs6-+akUG?%vJLt0TGhrK>wnYCO;x5|plf$SM#$;mXp& zUwEmt|FhDe)4%h`Va6rn4PeATVjkzbIVj-m0zFs`OKG0BohC-w@GUyFEa@_YVDh(z zlWXn(4zY7XEwHhjKwjZS@StN!AL(CtloEq&_9d&nu?kMe@6<@}DEL(d&qds0&SMc4@yt}jI* zppFQ5L!F1d+3R;^y4O!w=U)4I)p00N<}gk_X`zIli`^V7F!v%lF(m-mtf#J@lnAOK z^-|ZfzrB_o+_s(IRVcfou3fP5twC)rTvKf(*|mFAR3pq=twE~S*6P%GnLf|Bkn5S- z{()`67d#$>jHf)2{vL_~U6m=k{1ywZgOZ;bu#3AJHZVV9V`ED_(v2R7%xdSHMSes~ z4)K0EdV&ki`d^{0o7>n}Sy|Z>J{%90xpe^>pw#P!rrtyePf(4ddFZE*T6Q@dQoq|S zU*z`^M)&%eA+(cz2(P^ze-5~LLZWwxq9;DyZ(oe*^;?SM>WPEA+ut9mBD=9uUryG9 z?&e%qVcQGcXjBTP)J{{)&ku9E>gDa^$Uaw^>~=hR0NNCwTDi;r`WI_VHJ&tabn`=5 zWN3sLQO9D;8MG)*fv;q&u&aRRfrJ>;A=-1K7_~-*)~FXWC-^>kPVQC!6flEFc=rMA zw<2q#iKWU|2#67Kr=gG*r9kNSz1tbVFMpSOv6S9*fx9gn?0}ALA0=(fxJpl`tgi76?preZB^CK(U+?Nfu`;`4hv$n^LmrzC<^ILX z0V>v_Mm>WN4p8 zvtWl|ciNqA5VdF()~LHDP7u{RxrdE9r@8-p$g-dy?f@&tM+i|5Ha#9C8M_|_Z||V- z-OwY#=J=^iuP?G&NTCkRg9*=MGUsJ)CyvA$BCqmfY7oq-06QD-RRhfA%EtIGpD zoEp%ij{CxVvvx6hxMRY_fvIp*_M5B8eC*LPd!a}C#2Da)(Kz9IWD`@uh285&-rhAO zpLa@6jGJY6SzbunpX{;AlZGz8&(uO&@4%}Y(?sx7S^#Cas zZdNMeEQ4(oy~*P~+PE+)MXUXP6+T$bZ%0$8V7W9^Fu`ZTDm6KQc<3)HvCd!4-Ex>LQzZ>Rxn%9_NR2 zyTSFupBivwN%zJZhIDZ&b?J>GaxhpVsvRr&Y)D$;_kNi^sTi(R3YK=OK25|Uy88y;cTf{>&)u1}Bf+U(Fi_!6&IyzUF0Jtx?u^P^W=d`Qr|sdl zT^EQ3eE^5?&L1iO-(Qoh6(7(^|5h{|R+xGJA^Y|{-~O~izI_wL^1sLKag`obiQ3>am+}%AN4NlPi=a+5Wl=EIJ?(^t|GLqJOjv`2(8&> z7a8kOT?<)w z$b^RB8pXJoY^nBUlLez6ytnM^OB??@aq(Sm|9>**a$TzVbHE%1^E@$_hGck3Ua4?9 zk_3>)9l#+-X$oe2=+Lt_g+Gbcj*gMqS1!%3@|=yf;pR*aQ0vG9h%bk*JtQSMa{NgC z*Ls3AvNN>x82sunM9t}3b${ZsPJ@aDK9R#anR)VC0&)(JQeFWI@D zAszqabrd>XUAc45;X@Y8bGx-)BX+71`&Hok*qO@RRT(nZNnT^=xb9{X)r*Wgih}3{ z!5ezcb~+8VS%e{ir1mF$g!!J3=_7vFn?+%#OEo7^oBt!gdWqz7GKbx0CRqEKnloJ) z*Y}6K&u5fk_no%=S`O6-stt}h#plE1mN?@;5NdVUShl@9xxZZ1hL7tc&sC8rL{{w1DWt^*jTDiw{dawz_a-a< z_~C!EZ)^h-?Ij{)H*>4=gEw#LXC8GUxaH~By?)QKC&blBq6n+S!(WNAH!ffo;_?=a zq#t8<`^sF>F}x!Op)A~Sog_=VOlFKok8mj5GI4iDm}X}5zhV@giW36cZU9^VA6gGx z*t-xYSwMidbC~tvi}<@r|?WVcp@Fd3iB&u=4)S zWGhAVla85yb?+gLwQ;*|DfXxj_J>1w%$8h%>WL4WGZ{eQA(p>#yLfhlb&^J;UOF>R zFaUbyATuL_2p zkX@KO!wTf1d!|cgqe^!rE$9l(ajU4z7g_xKP1ZxP!;p{)S1DUk3-2^ARh$cS9lr_K zyCp6c17|4>fTbat)x8B>yYy?bggC|adBTF!aTjUMQp>e|H77fWrgI01*`f3xcwOLVx+uGf(h6{=hA%qS6aLQ9} zPv_TcdK@4ov-A2N9v_!PYruSAr**Jy_vYHrKF7BcuPqG}gA2B=ViGm*0?4bKe?Jqx zq)e_0{TVjsOY#-8pdZ@@|8l1^PZ?73fL8dkS1SytVl~LRH%AhMQY&wuP?C55Oe0QIa|NhVV2fo;0+!B8^Sz}Oies@UeG`y*VP>D=baZ-nO zgndymAN=}_jQ&L)nyLqt)ach3bUS~Zv2_&IT#motBKS&UuP^q(;*YyQ51AL|M2e4L zwlest@3ZdsJk9xhL5I4iPWk7V^aQuyXPQOHGE~=984}HFrVEptviOx?oFize_M&;px^yS0H@4{L zD=5Y1-i%?;^?F8T$TR<$!AP6e--U(kw4s>^k&wdx+8Ts z^xnOuc-9NhCv4&x^_Uca%0UPvYx7$q(9thb&-UcEH`KOE=ki`^^X(w!3)9pQS{JaD zq_ghe89T^Jt}(1y5Dd70O$|IL(}()#cUMT5HaU>^!|%kWU@XT77T*vncL7^^jw^^F zzK}fM4Ea0+UzOZdzrWj6OQ}2Zbxdd(FdV^`$sQ%SdA$&H_03_!wu`YB!0 zUL!YE9jrB7(yL0}>?ryz>ataDf+E(|?wpe_=5o*D7Qh?@i~lVLb3~+5fA|sk!ObL? z{C)_AD4@M2#Be@c!GT5PG$4x|M7>H0(*2lqVA|0jpyMLHb0@A$hT3d@Ch`EVO6SF==tAY)oQIpLBHK9`4IJv{ zb2FOmiH~Ka8}$1g2#eF6+kbr>I&9~Ew(Rh|Y3}#bIoo5g-<$JxmT?_2VG;N$*maEr zU@L`x9+fSF_MoUO#Hj|0BN=-lcRk7*!VsEZV}x8FO$U!pLo7upR7cKVpYU`41eMKxuH?7}4cH&w1-lc#-J4~y z;cR%nFSv7+nwWB)dbAVD5(xRj{qrU(7`(*!TV=E(0qT%kK-3fK1sxvYPDJ^Y^4^=O z)caW*VQlj(R=HiF9=F~?$9ZM{y&mU{`Yl>B@#@H7;v2$$uM`^_9xT4Y@h)(^Z@Y(d z+S>js^*nrk>+6R{WNo{g*WNtmvo4PdbpJ+-A2fWIP%}t`t3_XD&dCZ-ML?X7e00Nx zpRh^-*V?Ku%wHyx{zM4=$mzW4;Y$94PQ88ypaM?H-k>zW>LnS%TKYb5M73=aA4z9& zEZ{CrZ{`0E&KU7`Us*WOB6~0^nRAOHxj1&sUbC>s!5lu^=)KmE?Bd@df%#^2r%ko` zOBX877aM9Xx`*Y*F5Qy;eCF*XX%X~9Dgt!n9&*Wkk3E#sW-01(Z0T{^uztT>No%m_ z%e8<{V?*LCT_*9rb9PR+VQ2I-;)^Z!ksIxRs0aEyF*HX_V)E71P>hrYuN*o{~2+6%>C>FR@hGoc9?KW zO_^~_jVEk?C+xb+1uVu!rubUwB^lagC4cn*tRVR5*N=hFORXhFeaKSx@w;UR?Ly$a z2QK$1QO)S~#V!Jpg!{O5+8i?_pBT?&?zmL{=0%V{)WR7F!iwnGg{NoI+%c@R{y?l* z#)sAa>@YCDDK8BcpC%-~{_unm@=IWQO*zYd`^vNekT?Ba$(Xv<I$zKRxq4^0KSh&94w;4hG|sigTOA?)R_QSd#m=*O!alU9`(wG; z??={7#uLZUcVG7D9VLpR`GdwBwJc}e7+mjeyTc0W_rmn6W?8aEg6Qqs0(&>&xeC=> zoVM(pW0N5ZYliurz6CfWyaKjF!KLHkw9UgH>dgNP%pG>cdEBE9QK~YR_}nudSNs_G zp}z^I%Pva26uLIUeRapaLF=Zaa6r@irSbs0O|X}uWoNJ3j+o0XL*vZo0@0uhn>NgK$99A5gTTm+96HI7e`_D)U=z2`Wc0pE|K z>J`@s7q)p%4yEB?f|S)qe6qki=xp-@4Sk<6M zzAf^?wO-zja%qsaR&nPJ_~_56h_S+g2Dd?%L_74t?}tN9GDbZqA30WxXrVaU?^VY%rb`yE$fi){6w1a9fb`l$RDD5D9WqkM4q1d)C|H{d9$=i<-WkQs_IUA2OYE zaFwx$@9tl&Jy_Mk<$4v1s5}gv|9MS1E^Kq)Rp_kZN!H&gokpckUMHZC9WQ$*D*+}B z4?&CE-P72q1ZclLX!{oFP2uPIpCEnt!|x0+`)IXxQXFpbAe&Pw9c!JQ-IawaY12;X>!Axlw45v78az>rSMNVwue^P9 zY*aexg_Kr^%HEzt;1Abt4b}9SbSrf#k#YFN;ZkFyo>a~;Da&KeO#}|N7N@$@xi6iy z?Uf+;Zzw;hF@C-Vt*fPr&IqgP=Ol8xx!#Q+o|+m=K0o8l1>X+u#UAgj{^C%SG$dA= zQUtrPXTj~Vvfelq+~-f(CU6ZaRInY|zA>%NT3b2EWaiMGqOvbwvu*<%x1BL2#(jKa zd-)*r;oH|nQ#pQwx7J?975VLz`z~NB^pZ~Exq8SQ|L*kzLV*{cgp<}XXw>8OxnnYD zOM?w()g#NyD)2pv63MI5NZ__vqX?$eOZ@mG=EmAHQQdfFyXMc~wiJt#C(g^znq_2)s~h;kh#-nj%nzqR9U#ZeAbN(VqMMiJ z&NeE!z3Z`b9V4z!gs1HkxA0aeYF}z0Qu5};rYfBb>>et2u-Sbn(l(ciCsI{$OItsxezsoOJ{rOK zCN1fA1Wkdh3O7^&m;VY0+3erhtskVX&T;ahx?hF+?kZBr8B!st<`<_;7dqY}LjO*m zIqzAD^+IQg7-=WViTzS>2=08bqI>Q}xJ+1{s{{I#04z~-4fYDNEF_Ew`Fq9O07CpT;GESr!8Z*65bjI21Z8yHc=whuKhGN*bEkJ`Bp4wRWNJKQ+0ZpPzMu- zO7aH=1n_zv-@14|4bAvK?$B4l>lK0V-%)5)U?m7h?*#jH9cK`}qqwPVcl=wcs4C8@ z1N32!cW&GMBAnEDP2lHl@l!I{UzI<%JG3F8=!maR?|+gJ(vr>IqC7!T;k!T6ejNX@ zsJU>Cy{#6ruB^S|%&tDHp_q^vwE)k*^-_u}EB1R916L*RcL`vl07rzdZh$Oua2C;- z4bXSWqL$)W3)ii~*&@`x5)iqK1S|^z^vMq3>O87=>eF_VdN;v+?xLf?hK3vgXA_lA z`Z1`+xwL7X5<#X4CZl15RAhNG~b~x>)Q17vzKCw?xfcfzN!epSSsVsyN z{fj*zp?#n0y#8B^!5Y7EtSA;y&2U`sZ$(l`oq*>vUmkS*vjKD9aYX1Xi9Dom_^LKd zP*(_F%niKu2RdUJ!C2`f5$ZQHX_)uh_OXikc8jge@0=%vDk9W_a(x-(Cs(iwY^k@k zrTfl&>7H8u{S8WFER<|#MaNGFyf1#}$yEOJ$H@4%ZA=6%+-X~zSkar^tbCMWK1+U< zXvhi}8=|uFy+bp>`GK!P6<^hd`$bzGM_;)O*Xq0eF1uI8)J5>G)n8-1p1wt&t-V&` zy+-hVC~jImXdA4&a`qtS%hz*B&!f(#zTG`sJs0^f;Y`%6hdzsAkw);~OMwr>sFo}5 zXtcuf;YrQ?udiZD;*IsX{Oav$!v4Nryt)?RL=Qa(w36OVaIO931+6E&px~sVoQ&sD zT&wP@MMw0@V(R;bETYU}TmW*(dg+F()zKFPJ zv&664)dy1afw1|QWuYI8ZgUjQ`cvRbr|u3R(Gy26rp%4i?PkV(9oezXg#EgTvVPt*&epr%d+SQF{%1Wv^*M>V9F<)=#VvvU3Ws7PPe6mqe~PLe zSViv#P9u9Rqo1RM{{|t`4L!h3kRGB0+yn4`yukEQ@QZ%pFfhB(Or8w|S<+YXa4Aw} z6{EJ?0HXy-d~;#Ai8&0AR7U;bh>-S?AR@1MOGD63QEUTmg@`l4E&_FM_LmQS|M&&30YNSuvDE|8Eg1GjLZeETkhDc%OoXx+yLt^C7v$m- z`r}{M@!;;n?EXAA)2pZ_9pf;?#Y}1ng7{wM~&%nUw0l4rX zc)U0J!U~q)A@yURpd1<*@ny<|9O3R~x*Ef}g-RGzV z8zk3%FrIELi1BBDz8f0F{`*GmDKG{=1HXZeHVtTGK!F-B*LMUKod?X0`T;aP7=u-xxnKQ3P8`0K!XNu0f2;lyzsdO2hchj_>(Tlp_IT>2 zKcbN>l@}%)nPL8xi_y0+brIbUanTM^NiNr&wi-Qx6=$pEo-Ky2q#c~dY&zA_zKdPz zB=uAldbDkK>ixH@Xf^(ER&wW=zT6l%`h+we{(xqv2}Nw>NUQw9GBh7RGr`#Ps?Tpr ze)!=h1d-?sZY?;WXyO8!6nB&&eXx5uF-U}`@+=K!+Dh5AsGEcC2J|CtU|_vUc+l|o0NJ_;aTiY z{A%xu+x};r2-Lu3rxZThw{2!9M9?nb&|E#&KEdU>mj=3I{~LZT1?efY|4KFlUl%^l zheDd!KEg-rf6qClz(nz9SYZUfwL}6vA2)s9aYx`k0Xc65pGE0W@Jy5ht*8pKsvV|C zO*;bv-pQ400m6q?9Xa=(6Vhtu5=V$Be#5qAt^P}#pFTLyiMg{-AMyb{f8yfQsUZ1V z1l#O8-_I^)ki}qTm3TZTEt-xzFU9H@iTV5+nyXH(;Gd~|d#%?oZ@-&0c(&02b{`P+ zQ}e@!z|uWvFzQip>TMLYLeB1$ttdqB6WB(ezt=jFfY11--w*=K??YCce0^?d29&o1 zwsp-l5PLgO>B9Ilr6I3ETz?NP#4dzu#je2i0q5Y6;I6aactyzDTGZ=xSwuHW!Rkem%;)nc;WLUz~(}}!~l;3T@xzokwh8)3MJIkIH!Ca~v z(1EJ}$}q^{D2&>LCTYl7k^1yad;Hztr0t(JaBNW0YZyHdE}XP5(xNSjwC;LhR`C8v z4nLLrQs5v9gV<}l=jfw;4Hy2>%$zKWBf4Gw%^vhz7XUj+MALm(*mu zA3ats%k@)*cp;1Ls}6U~w;1|Tsy{h*DOx$gcz*6otp~y6c9`b<0Nlqw`Hm4tq43<* zyyiFjojSJJ6w9pt{w7q122@LmQLGCSZ}j~1`gUrEARBcQ=4;GGM;{#TcqYA=)%vWR zUTC>tQYlS}m@ui6RF)*N1Ldf0a{2EDL$SdNg|x^dSd;{!IliiUD$jso|rP8?|jay!a(u ztv7rMrx_lVSc!WjzI)9pJ*=?4sw32jV|#rk$jW$ZIEaC@qx%IYMGI)?lU(^pY?mt}ma@et=2;#ON5(544=A{fe$9AdDiko^a{`Sbr*QKeAV`<_N(*Nh;NhK@&`oXPunickbzDW zpb$yh1%$Va!2>+#T7Q7WLv0}yktoW{V?b=?%yu5Jr`ShnquZ@>hFS6GN1!^rcY%9R zMf_F7)yi}zX+>^HDXZsq*&^e!Yd_Y5H~O7hb9{~4+0W{AkZ4^sXu3BgOeGdvA>e)BlDT@GO=R-iqxAd{o)d zp6va!OLf@j-BabVouo3bCmN9C*{sJbPlWA?=ZseOm-(Px$Q7OuQX%`3hF{ z>J@B{46R~Ml7z#%tX8;zbh84+k(?e^%R!27w@a{*%qAs_K6-NKHb%D2!ovJ#NPMZX z#Q__G@$7~0qY->*ojJGJ`7#gAce`ABFGcbu*>k?Ac(kT&iBM(MD?W+aqh8;W8L~0x z8B^jZmSei{j?=DO6udh+*x+z%V^lUUXJz*8LBNGwZ2OzCN|>Q%M@(JlXqCG-SLJwK_yZ#2djN z={}oJT<3vDBGh^WJrg)M>i25=$Igwt2TR(e-?Rg5Sp>h&djj;8oz0u}hRIFS%-|$D z46U)geXLRk&y(I&0o?1Cpk*vcpcmi4b@^Yz+^?S=7?Kf<_S{nL(>YFz9H=Gdz&|dZfOB-M{Q=Mmu=Kze;pm8N(!vobG z57f(E91&s*^aAYCQ)s_fSthN;>(Er$_eZli0zhL#;92G>`$CqjI7u69wHYteRm4<{ z^>k@=x!#D;k$=Q+9bJjb&By$k$ZlCeqQ4=s%@g^`_QtRW%;HAE;UB_`bO~AvjM3f* z>shGY4}@OdJ0s;UKU(fFNFJgKC0+ablDxlNJf6E?vZ}51d+R{q<_{gYh5Xg39~`~6 zvGZl~g8=?QU|=Id``g6GhN@uB+jk|UB*x`VUBzD4dBG*^Ppl@-X1-mT&t)p%Mq9iF zwa#nDtfL3lpD2M8?r(36t==FhQ88Bml_GHLGMo;A{6yaUjQv0tnJHIFX5^8aGKlQD z0KgahlxUN;US&{H#3VMm*3>XxMtyMu9;p&htlr@=9;)vjhVpUMYlXzhm17sjs^$;b zf(d7y=3jH4A?!UW{vRL2k0GJEklB}-Jc7)H@iR}nASDAH+9MQI8AwGIzeJ9?vpTkY zVZiXtMdu22h9elKTd*=Pxuri_FG_UQWYU*tP6^!JEs?GaZL0?(M_$_x;I>A(aaQJ= z&81@ZCYC}@qsg`h*44MItb-SOzQm6fX#r7BCrXtd=YAZlC)k3;DA?qp_qzj_Rsw(v z=BCeX!x=7cyaXvTQ?6Wzmk%5hh1ze1@>Ba+0~Hk~!QpS$-@JaTRD)sN72H|*HE1mR zOXroTN^sDLn)O4t8J>kjrGM|dpW12{%l(O5%%ytz6Xq2qHU1E8HLriD)@31Roji&> ze(W@AiO#SV6wPb|&WJ%7dv&6bAoRL3^j7kof5V`iXcI^U9_3J~1-N#KwxjG-AkDj3 z_Jdqv%cRDMZ!1sQ3%il;SeK|jd5dFxv-SB6a_kf^FWpE#{PrvY5jCuw1D1iMX6<_`POIKg!2F7?#L-6RO*weNkgx; z%Q&%RW(9t-(uHDge{j7E1p@!$nM~d(s)jL(0JbYIA&MX2tPW_`@l%aq(av0MRjbdO z#H*xVg-OG0(BDV7>^GLeqsz^tX;FE=-fpS#c-n~MrKH9Rl)LeMp_jn&S6%#S8Oohk zhe|G#pcNe5kfN|Fe!X1zQ$MX8*0kxN7^TO0abP*?(YD|q>57#P$Fik><^gc(Q)8bNX&QWEMx!mvXyt@+qLUY{j~G=a4pLxl~Ghl~%D7Gfu6SihF@ zvyGmi7B}9TO&P?UUI`Mz0xv5P_xWz7Fw_}7T}ocki*eMqSnXYnbqu=Ne^9EwPb}ea z!YxO_LtGKKtn#Z3iHM;KSyrD2>bf$`Y!1%`++n%4+g{wl$`C^gl0F6a^Pm-Np}sJH zBMLB-q{OMMNRWSry!%&^%`gG5WlF=RaSl6Qhu)K4g*)NZS6CB7YfGm1zsyM~Hjnp@ zo44wcF052BTQ0r)Fflth{I$$m?uMiH$!{vW<%RPl_JHOuF%`{SY_`pog{}1+9;dCy z(@j9oyd}GooGX^S1#h1M8LD6=!wbbgvA+Vz*I@1oNc6&5KypYw;`Z0K@=%5s6g8lL z*W-hG&k3D$r|g5ehH1H>``)dKK{K_Ad9A_Rk8kcXTOiYW|K!uN7*9ce?(pdS8OO_9 z+P7=Rvne%;RPSU=X0kgQ|iwNIo$ARF9R+M?-e8T?QpDPcn_y8Rge44|uK!|7s>DavE#0_fRN zvinDBOw9Ent*l&LoM=GzFPkJhkvr&EO7{^VhO-Nz98c`Nw6(eVSJgeJ*`Zhq!Ed+H zeYpILVKhQfI)G)&ZMZ2#-_6G9g35k2l(yW2 z%8kRe1_6b@HK8l6+a3)Xkb^vN4|4Bv(MMC@^hp(fJ;sMGJ4Il5PHT^S)2?3crJFn9 z9M~5ay?o!TExIORF|6qcK~bQmwF4vQ7iUZpgfE;pH&aY^S*=gKwHzeDwZF9_szUma zBG*Y{PTw0^$zqC1bYt+N2ed@p58ir~I|m+}xZ#Ew#?_zCe|{Xg|1fP#9kd62$)4Y* z&GK#Ivj?9N9^?S=XJF;q>@j;FB>tk*jYI-J)zQleUarUE_rJb3-cnc~@XSZ9PkLuE zCn(8~)6YYb0X^joTLRQ#f%mC~JpLS)1a|)JZ&wA(SGn|}It%N%z5U)R*z*dxite!* zf$_?*Uk8n}GZS{VEP?{Tix2;FIaDFn-ZKM|%+32E->f*;7|6=%8%-+vhonCJ@%u)R zK|8%K(X6&YvFXCmO1ad~^r(;3ivLx4UeT7Fovs@9TJ0Br!mbk=Vy2_sLFOR}pES~K z`!1G^7xX{|vM!d^ubqYAQ>kAn4K~Rym#wc!vfK6fY4m7UU=9)H%K4v%Bz3Jt-u&S? zL5&cn3GEM0LJEFOQOXAXJKFM|Uczgk8Xr35%)&r|n?=IZJb?;;WX}4+ReQ9qJ-N31 zyVRcBJ$WQ2TNe>_m`zlMgm`EfPdF*SxQ84+&~gL_6`>X)s)Ldmtx5$BO3)nN=~B}^ zqlATiV=95yT8Qc&JhuZYfutQFABD>}WK-9Z2tO%~iPdrTm5t}D>N{%T+p5L`cJ4dK z?q%)|TJFkExvy&Y^JaeMP@ig6|oW2=Ql7SAXKZ z5I;ISu;qsAkm=S{ql97=9el8ufYEttYfi(x zub6zg3pHGUx~{oS6@s&$FjD|7&9_GtS?3vjk}mTEZdRXdCP-YxsQOKPHzC#-v`>_Z zk(T92%Vxcw$|~L$-`RJLDHQSbzjcpGUlcAJ3;j17p|Kx5v+I>Xdh#JM+w$M>JJ(^p z&YBx{^&mz+Mu#>*IKIM63cL=)NR?`Hf%A{p#vrXkpbwso!$~3VdM*g}J&Wfax8HS% zf(&4tDQBFqAazDmk&k+~X(0_)DRvZoxTUUy_BY5JJtxFu zmh&;=POO;UTyf-F+t^ZgBprjL7g-5xs=RvmmC(qS6!)-vvjDISA5q;K=NgE?yyxG= zP}0r~g+iWDsryHw#}s@l5fk>7E5~v`BFe zPNtc9tyT%(1D@}EFb2J?FWT%7&)}vN7J4P14?oWa$ieNIt6WQSd2jSHCXAKk=$gBw zWU1vl_xj5V_B<+yA|iI+^j5-)2*Mp1AVxd+;{C`XWc;bYG`uM%uFd~K4-BhcEQC&| zJ>AY1TXcxed)>wShsQiwOt@r}(?{{TJwZZaUxKuB;Ep@<_L&X99V8O0faMl1;6Vy- zfC~(&um$cEo_s>|c>(0aBMY?I<%+sgSKwK0mm3oaEJeANavG8NEB-*{LcQd$+e)KL zR9m{0XPozsuB-#rz-|2UF{fRr_kazG5$XE#jq{spq6I@BtKmFK$ z_e`24BjFMq4s z$o17NrM|fW-#%~U63p`~7Q7yERqBbC(!)?MuZ&csb5^}(1?F9ntiqQS6;FTC?Oz|S z8=zL)KanrdUo#~A@TI}+_QI7EWO3b4(nR-@0YsX#;Kr%HfWXma*6Wk&uY*U!g0_>6 zqwDDpFN|NLF);@6m4}{QivN7MoqnV`?^{>u@|>^`a4szT-QPWMeA`ZKlVKG$!i2Bv zIqJblAS9f91siht)?`%M=6#^8XrEwNC@;-tj~)RxUYhn--Jg3o6##4)SYcL>#WE{s z4U)?YNRk=iDnVttY@vi@Ji*m^s*MiSP#3Rv{QimEmCT)Rk&uQfm8t^93y z)W4qA<;SUtQ?=IB%aJew_hB?>cs6OzVTO}%9LG38e z_5y|P0{oHl1~Dx)#)QgXJzeV28d%F{C4dvu!B4m7P@jU)mo1>u0g7-gw=f8i?vZ*w z3+*+y)ss>zac;C*ikMhYDi9L5=lv*w{6n|M442ZKo9O~p(&>H%>;2sBe!LWGl6aVU z3YqEi7MJW5w>wqlN-^6qD>P)#v*T9uc=7+7U_ExrmyvYR^XBt|`(DKh&@M*{@_Jug z@l;El(|_dNrz+=(NF1Mfe?VF;rHiLf{M-kXN8z>K13s9=CJMYS53Bu@Yv6!SoinR` zpX;3eYO%kk_(Z)EN`~)M&uqdR=To0~V83NVz(Mo6wO>{d0kh+}%{wb(D^6VMX{5)lOGH+8iF5D=+%g8CzV^!JzX(E8u$B|8aEY;ZXf; z82_Hx?E4zoLXu^)p~6h1R1`%~_9T@oB}BWoeFpbg1VlqHIg8Ci>>BtmAgQy2`! znB_gc_pdHj|BdmS=eh6CeV=AZ)VwvzXJrk1{d#Ry{)JDkTE>}X(dLhjHJ+LU&wX*l zenp2hs1WlplD4I9z%>wQ0k{3MMZP@e9{=6lbQvaJnh2{3Lhc~)7Gs4G_m7a! zFC?jRJRzdc)c04w_4niji!F<>9Wuvn(d3Bw8nDxpyAWLnl?aIl7^cmAfwn*khtY{{ zQ^Leb2?+^eflzJ(+Ch`q%(PE*`ydf01(7=?d3RK7Dar6N5(DqL_UEB(>0(Uq6&1!)nJfWzT0i$TBfZiacy!- z(>cA5s|BY>dogL^=c=&c=REhI68C{ek;P!a25R?S^fOZE#d&e@S7NTRULu=jsT9=@ z#e!z_;t}UiXpS2@UUoTV!6)W~vdy-s!o$6OX?MOYoc#CT%TA@A9p-=I_sJ`)F2s4I z?p3{%OVZST=t=QYzp~zJxaWR=2E6Va)G<@6+)fj;HkEw+PNNSuN(Nt3rqpnMFMC3X z=AspMGS4g9{2M-eK^bRVt?{!>`teX4TjlQS14l!{G9!M>r&W0d;;-S&Yr0Mlk8$28 zOp{Nbt^e8`=j46JOO1;;>pF5^`8Ye22qen$+k;g+2Ch##RJ<+hf(PFnJaC;u!5||FM+eY3t z);m`pSX{*(cZ3yUgpGD-?)-Xli3V-06+{)djE8g%NN@@&kthsd4>DPg=dIW; zXCBYTG$#bP;wz?poL=|4MgHksyah-N9&2{HT(2rmPMioj5&!e%+B|oT*HePheQf;T z&n~*#&+T)=px4LHp>b?sw@%c)$!;GEr|0k3uJp>S;kNz6P4x|ZBRgx0`m!A zlP?N^E^JN{VwW_W?gF=9*Uo#BW&6&$QLe>sjt~lt5w2#YboLzgR(%}0zxW^3hyZo~hqFO%XtbdK;t${@)L2&(T zZgd!K=)-0}v%_xTVbpshsCi-&GGt>#=tY$ye1K_RJCz`d&BeTG@~L?FTwuB=RGT?^ zxbj*-&dN2yYD(d~@PXok;~vyUOR3dM1w15ULEAuF~3hs{lqWmxyei%|DYg)Bg3F%^@#<%@sOj7S2IqFV9yplYz- zSmUxVG3$ptK|4VWI$xXoW_F2MzMSL)fpapp`u+$`pa}@p;KdKfY)ubqWUk!+nroHeLFdQD!qkH( z)2rP3Hwo1R$~xZ~N+#TI>>R@jsOu!|`_4`$Xy3_U+1HK~F>5|od}G*_7eGH6Mnay0 z?fnobc}<}vq<+!%-Xj#D`L!6~+k?h=&j4tWGRzHE0HhB-J|78hx>S4|NgIl2B-v%1QxfmSOZl9DQuAZ=A(b9OFj?v~d`RJe7W(^)t#?#{?ep zGkW=W(*r-F97ULrPRRl8-MAf_;h+3BXM&qd!EQ&9jembFd_=UqlxX%W{&dV|@O?#}g}kn-X*T1&w%uet;vb9&&b=QI zCIXpV5BB)5p#6PlkzuGRA)#gw*>2vDp~`FvIRVbG6&)efIgN|AZ>>-yiG%j3)`L`Q zYJZ7IgCud-Q+N4&q_QY7v+9t+H6MnCHY&q=@j`TK6zim)QTMzYy)r`6vp#e^5ws3^ zY=n!D?Q@p^V&e(YbsX>)V;Z{=VtlULI7{!FaQF6llmmp=jSCJJK(4P1GT2;w3EJ-K zxjZDrh%hlvDF^>>eZ-NTg>C@a>ht@M=l-)*NOziHY`6yqe;6ym(z z=An4{!_D1|(`CDMq^esi`)wy@^+XFyO z(UR=+l3eZ#VZ5S%qqnfNJOXMLPD@e)7x7An%@C?2m)jzYjIR|Hx<2H9Ww|56>+wV3 z^7-U$M;t2AN1Vvc#Mmt;Ly-F3C5(p~p&*I$xpsv5b{(w=r%ey?^43nufOoc@TRF|{ zs?-j@Blj|8>@u|Nl2p=R<*WR{q9WOk2>Fu8Sw7N?D1A-%HO=aT*9#9VFRyHqP1D50 zlAZ|d+NaiJk!j&WO_YmFElF(J2i&%f@R>j~tw4TSP5#+X3^pDN>j2p#o)+=GCAd2h+say06(+?Ot^Qu5ZIf4vdUcrN{ zC)TcE*vh-sSB4IV&Yj7lam-0G0_(s(sP$HL$dbJ*~NO)Oatb+10awI9+sJ z$?fILZ23c%n-dI8FIlM#Q)gAvI{=fD3dkRa{IFDOpQscreFRP_(BB$aQ zMU}%aA=VYop0(OYIal;N{*5iFRFHD$E;vf{zgqOQBdoGyNp(l7JD@MVk&Aj3t;^5I z2vIDXutx#8IVob&@n|fviTwlgmv=zJC?mpW5uDwOD4^z3ZB;p_0DpfJ)j)tAu8Mqu zRnu)>W)GJB{D47hRevyZF<0$wPwI3D?aV_3Jt*$Xs<07BrWyiU(3Vebz;_dP_-o@$4#~~ca|TF=cUkd2;SC3tqS#(S>E1Q z3DHE{NNA08KrnQEphXq2U@9WB)T6$FGeb(W86ej%LR%H3bCKkP!7Sw|A8{xl$%v9h02c_J_7Wem+uIJ5j z-hk$<&d^VB%gcn#j@)f)BN8kP^4^G*D@*?}?)_T593?cIU-{kZG`y8Bf9~NqdQQ?p zTfQ)LLWA<9MII-6?jBn=wC6^@`O?BGr;yN4U!BG;cnFd`t>Q}^>F;r<|4Z2lodP&* zHIt81b(BMOL|_6xr6`6V$&vW;SSN!o!i?v+sH*dNL{uWG8^zyD79Lv9 zR!2!rl#gk*ThIB}KzP~o>K z4z1mE>Pc=2Jd+R!q4~GNW|lGKk5JL95=yXngg357tlt}_7L$2zb?avn(;d8x0^^4S zc=7#3TDCE|>QM7{8#Uhfr~CTN(3@d5mzFlBfRSuk=Z5HAoUm>KYwG-4D6v2So*XuU zZ$B2p*|PfNh*i`p`v_W?GVdBm)&e19IbhfGoc+8-)WlIr75S(hd^qO7nhwx= z@`fcuoYvu&%acCG=g5Nt>(@t|XWBFlyEHVibzMQS2x~OeoEhO}c7J@r5&o$XUHeD3|k&*ywc<=(lL3{1^k`xAklKO=75&xQ| zB}~X3TG6b27aL&#_Oz1>~?p`&Sg_GW?9J4_Z{q zQ6m`59p+|!`&?wIwo-cp|LL8U0&>WqstBV8Jq(M*HV4~ly=}Z`tjI4s%#XM(GW85O zT6x#M{3$x(85W?$Hfg=zO$`u5L-Z`wlnPltBQ@R$Kcn}pnSp`dA8_%PgMau~$epUP-=31dmIc&Bch{p{Ko18Ug<^hn`v*mU1a zUgP}L8;TFbHc!PrGzpcsv5zH5gZ5Jod=5R#7Qf>9uJU47>4zV_9mcU=3_7wlwXRIj zpy59__D|#FsT>;4Uev4Bq%g!QF}K$=VU_D@9@)fbro?9#sE63YDNDNzR}WHV>k}D~e4-1O=IYDr0#SIQ5dEYKRcAN& zTSPsKRlkd4xInqh5tZO3r2)vBcb@?!6?9HDCU7s-gUsDOTcgJ4lQo)9OT~hdlq(J# zgkND7aP~$RFZB#>fq$eTI?t9zb1Mc;kwBqPp>T$E%!|hKWeBWXQ856{7v4^8;ay%7 zOSu)&hcjz_EX!OL&h`Zu^wu3y9N^^k=UJpodq1U6i0=mo>u zm(zB_|45CssK7!Q_zbt%J!@AhBMDNRY1_Fbf=MPRC=S0<*xSUk-_OZPj2Cs4xwf%W z+kd$&^%>c|7m^@g$l;eqBY4V3pJf}Tvwsv+T(e^xd$m}ir3n|$+(iu(-8ber@eA~b zh6`0nlL-^s-o>TT;H#H-*Ld6H;Fa^v$v0OCVbs+3<3b_7BW^Us-DSK7Z)kL*XWK}rH`9x{`S8&&s^}7Fs zO>wm?nAvn#F{)Wl>F@L@XmOpuo>YLvD~Xi*IGWY+ zK?^QMRZJyOkDyKK#Wo`>E|mPP@k*Usyic{ir=4;#nvn=pzfr@KML7pr5u+5e?I0<4 zJ~2_YDI7gGt1sK7{Bz|PL3Co+LO#IB#l>}1hxD@gx2(Y5%3Ae>xK{SDEz(r%-_GCt~w&7tSKAE4*N@F?SyyNUkEa*?DYa_OFBu+8g#E@lv@FJWCBW7At?$?RG1}56{Md+bWP~VM zIl$Qewk5zzQzJGhh6+59DSd8lY zK|1=1W&ciphw#6kzrDRKP|ex%Dk4jyG^Ck?!*!YseYkLxz)(0cp`ezmvfNQO?a!qlaNM_0=hJha&nY=`wvq6dU|2v<{}jp`2< z<}`z3B=`GT*zSgX5%mVxHU=!~gFLcV-$5@P+9jPS@1z(b-&ybDq!zVN1i*Ck*?1)z z7UKO(pOG~kp;R-@01%H{8Q_iXkFHRS(2ywgTY0 zxpLM7^NY(Vw%pQ353lho_V0olsPgb=8pAW?n5bYvn&{QcmDT-bJw-x^0w zImyYb9wv(cUyebk$%rd}z~(lE6HD$1u|Dd5;o65{G3BS*qY+r4 zzPTi`@G%GLM@}`(4{&|h9grLK?4}DAU)~|XTNbeU^tWgXIA|0J5^agA=Wh$yEnF!i z#SV*M13#f9mDftBO^B4Y8Pwku=xz~U{Ho-E&jjQfE$1Pq&eViuMs0bgobgjMku^sJ zNvrD=$%DMzNbDn?d4dv9=|lOwY@g_Umar8A9zG!>V6+AK*25>h|1<^`HX+)|g>xll zq6x5_!(kp38>GDm^}sz z7g6GvBgp0WLTN#sevil~qi+I-r>=3fZQy-0WlO%YgYq`Cu1csI-lk~5$9FExHwFA& z)L3COlNM*Jgb51@g-;16OMX^8dE__{rWnQNY~o4tbcNXYIomb3q*9r_mkOKR*vGP| zZ83uRA@!Ve(_gN*P$#AITT#UX*JV4X-QuGD;oT}frHA8R7YqbgHH3}q=hQ-BcmIxp zFhewg(o6^gvjBkeGrIM7lg6O`xc}|*YXGoK4kCdgKzy+(18wv=T;2F80AJcQTV|!? z{&8@FwzB&a%|f0`70vfn?kwKX{kB#g*;iSQ$MBp4_5b{Fo4Noir{`?t6Xp#QVviQ$ zOacZeul6lb!c_1>qk(HzX75;inY$k+#oe%*&s5SqDke_kJ`ghj#K9i1gh?1{GL;7A_*R+6~uTujSk-!ecpQAP|NpP4ENYxy`O~q(hO3JlKW1Wl3|>6R!t&0kFiA32L_V#HF}pJZVm4SPcRq;3wm%8D z;DK#dvQyaiE>BL~81l$RxgfWR059!f_-PfAlYYHt_||V9#Uo02qvPko6dQEvBfMt~ zGyRUplmA(I{!^xw%6TY;Ue|cUAC;3JjBBuquIuqs59SS|cGVG9xAJ%eBZh<6*CkDz zv6l&0)aICxGM^`;Lpgq!_s^G8HjQgbzTyM`0x!!eZ*gg~7e;RL=IC5hy`vji16B~E z`=P=hy#V8^d*AHoL4I=TTP;d!s~YPMfr>HcI`h|UWNnlN{zag3N`XIJ{d(&g5WugtHXWc&tY)?j73mU47lakK$)PZEd>MRH} zFrel`!pKc{lsDw~v|G4$G+^W!OB-OwTcBt)+3WuqJvpRez7jj`&7Ul$b=#7yM?6bc zNZAj`z;w!cInS1>bmvLS~Q-e&F( zh?z9f>fqkJPSK6?yni@$rYt^A-Q-Ca%V}&YL%~UupkM9k;jRO0TfuoZO&H-tPLpQ9 zeo86Wj*>)ThKry%>Khv9d{#Ssf`8iQ=WpC+kd|J zNJt&60$QHXH3Qc4@*Z-JHpvN1&{}Vy-9}RE1VU`o3-%i}SETD*N8Rpqm{uPWA=8g- zv|w?~dr}-V&=I_towzB%gXbsa#fiCTiT^58DAorK*nH`m!ph{w@!kn;OK)>sSk=h+@e@h1 zwq;2vfjf{oQ~7`nFx?DK?LMKLJZRm?%8O>d>}^p~--uIQt+<9>9!wwuJN|yt0M z-x{~=Z<=Y05x{TTAy1F+uH27=W_fN>#4@o72`dcaKWWjOA{203DKXx9q?j$wlrkT*HFXa4iYT(Jq)z&?uDA^Q=DQh*OB0qMmkm!^5TC=szk zh);4_^{C4i*x4NE`fEvbKT<0`T%WCO&le%ay;XZYPEB=B}DQGDjW2P3nkWi@cc+~^S~?+7MfSAKo|r{|ce2rl-jo$gbCg+cS8fWytx zkQ)NFOB|P`R&RtoBt#t3h0{R8F|bS9LXY_B6oevXX3OO9c8jSCa>iajmgPsG7h|?$ zd$z{VyKQ-D$D;?C_sB>06PlN71CCI{c^cm(K8>_#AvjESaFeY|EyX8QJf38z=*79Q zqOtL6wFz}T=Slha@~Po z!MNjof%<7b_O1WCRd5Blbf5aZdi$lXr>eUc}APQta0BV zfynOElK#+vJDnEVQBr}5WAx39MKk2zp{ipd}a@zB+L9k_Pnq<;^Wkw0RGu8p#Bz0=O?_IlWqg4 zSrS}WUSE|51v!AZ;Pp|SavnO&y~bIiU)5)Qtjrq4T);Y&>@z+LNe3-g&M?^Mjpyk#YnpiyQ#6ykCubZPe*;$>e0tUo>3- z`Cz1}R>&fdC$ssQn306KVDnvpz*-FB=^nTTQ|_)T%B(b2>`)V;AN#%Jzh1sj@NB5Aa0!X*oiOo1^Cwa}^i{%*haaV|Gi;TAdMLyXU3gBfI=p{k>jJ zkQ+UQ{bY!u@2o({olIYGs^3iYF>Wiy=qJwpZS6(YzGK^%0XDqW<^Y?Ooj=yy8)8ZB z!-`D=MMUqsr|7i8b<^kXTiysP z_m6%wPm0f7&^8T|Jy5m*iGG+gT+D?dWV^+UYdwuu+K2sjS+d;)J=Ua9HP-wPzu#F4 zE#hu9l%z#Wz$|qVDxby2r{Q*ex-bcFm+eTuY3M#Qx0SVGw<>H;*H!j`=4Vz#A zeV(|Bi28~k9fCL=N6-utWdn=`TTA0Doal^L9rgR@2-7E;I$_2u)${n|%_AA9GoAW8VPnGdM4y68hwYofz zicYdf9%ATGeOo@UunS z3$y&>xhSJ&QL9xG0x#6uHq*Uj?4+XnQwoVownzTE?iKh)0Ywr{l?J4FbmD_)f#m@x z7S6(K7E`~6VlYpDL~S9DFfqjDd`89umZAc0fea~_=D(j9D7!4-yC5)!LWDI?pk+Q9eJ?^zO1i{do6&51*~mHzR;`+GI6YmgUh9l@>$_wR&1MJ^k0 z>ihqz4KhpF^J10tUC*@*Z4=nAL*52os@d`=q*IMEtZyCNhe3*o8*z%;oBUloNIumdnkC7-q4&ze>&`VOg*OzLa5#Rcj@5j$`f6!Y+x*YP1ba?P( z0Ox~j*nrXnn$|rF32POLSIWQw4@G(sR`2%llz4tGFgy8z>Lf`U=-wW__fh_REks6e z7y6HP>=Pp<3D6Coi{|hX!96It^HV6<5$*{PrvAIn3g(~2c-$sPj-zWPAjQc5gU>m7 z+)X@#Pn7f6i^z?}@rLe`Q03L6kZ-e1#|zx2+^e3C2awmLj&P+48XTYLaOIcYs*N(7 zPqkUkSN({LiFmv`^_+LlP3+ErvYkdDGO>cixX5peGRy9llW#vxZLAUI*4cYDNXW z=kVH)hYFt2l_T34Tu;w_r7TIN&R3u|%22lRir%QE2RQ$lx1{?tgRT*bvEP@&2Vxzwet3igwfgHMpb9m^8WJM zK{T5->yX6I((bC#*M@FtBHc^;xZ_;B?LAww&l0PvQ`rcW?BQS{dbLtxkBQBPgE;VW zlDzNp9fkc)V*kj|QU2=Wx~c#~7MAr-);3p+@mhQrgI$@GB#J6jEZ?2Kx~0z3Z8z{! zQpBLa*nxCAlxi+>^oo%>XW7T#GGuY%eF?*|-a3ErwvEobE|dQOC0b0lVpU8CJYD6qzHfQ*&BIrq z#KS+5l^IDbTPsR}GuZ-{Jx{#qU^P7}pd|!;CCtCRTf)0vc~F!vSoxfqiWK+jT2s

_;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 zcmV?Ro5^i-CXZ$gvbLIyj$mYlr*<>3+zwRc0fCvHuL>QQHWLGe30}R^<5tt+g5IiV| zVd*TL1;Qo{44cX-YeZH9$YPl6>CQsZdrjTjd9Rn(Fa6%_?ht+Fd?$K%eP7jg>sHmR zTUP}Ds&K-#x8H9OZy@3jNr*JWF~m(oC87qAkN65Wb%yNKpZngA5=mx!tLGieV(X(>1n=MnP}&ovnWP?)C>Zz1l4L1`%zB4!|d z@k9wg0R|#2`3iXI-`!#5*fy|rMhB?N)iEn--O~?p|Jezajcf&nmv^QA@AD4v9b?HP zG%5kLSuq{Vxw@171kRZk1(UdDFj>=#{yiGsm6?FN4|~!vcx;*44$Qm#5_$m<)hGm1 zXX-|l>N$(mM+h?0pH6A#B{0FowLO^$_+d-57nuY}g~nB%-_?Rst$BweiK< z)lOrEw*W+-L==n{k{38<-YY?`cVa=4H4zkad>nh}h*nUN&iQ-aftV9=0>~OO88rKf zwyB|Ts~XI??vECyaj^K!mV$u1u*X((4Vlv;Ry4g|1We(-!9~E_!7bo^O0?8(Qb<$^ z2?6%=QO_9DHP?vZ1f{8N=I?zL4RZ#y5CkKCiJ+I)^$fY^zq9$z5cgUnRi6LWyD`n- z>PNk$enXP2Uk6P|- z3DV#12*ymmenB4;0mcmNdQasarSyek@!eoH&V;9B+N`5lAonQr? zWXvDZ0!}A(_skiHV;ruC0NbAaEz9&83sF38O)pqIwr#!h@eXnCqsbF5ZP~4ZM$7^Q ze3R5277Tq+hz_2Uvto2>xUo_257mgyvI#I5w0DT|hZc9CN>BDN_i4Ha*g5|dsLIwn z5ygG2;n1=!@ZNw(uY7^G&I5}(^Y4<~=Q9BbFlA_G^JbrbEb;?Rq2a9=q)cy5L*J8D z+!vKuS}@Fxf+^YvAyN>`!zJPI-joIjDEzF)bH&>^Gf%+h3pxt}ts2t?Zg1+- zal{`_dRdU0SgOar7a)EkNdU(CO`cw#;Oe^Ga3QG&{cp=2=?~ivj|0cGP2eoe184bF za9rO4_9GL6SL)ChaD2BBoFx~*S#bj#H@AWP*xLbr_Yjk$yPx`Br1D=)2e8(F=&_r@erhhW^7iAC0L(`5xDFfG&;E;bTqELn5dn|0bt|NlC%~oQ zU&t?~qVm-s`A`z(Fev|yf8_-dtYAO0kd=hhA_6MXH3bb3P$EHq({u*{KDEVQ+c!Xw zkj{$hfyXshfNlS4ECk&5BA_};9a*$pT6s#q4lx1t)AL0#-Fxs5Zr{BpHiQcw$e;bM z-xK}*VZkH#{`MVkI-TBzkhg}_3c3me;Py79A?4{NNG~S9S$s|qHQ9yhFDdBDJ^DgzZJkTdONoKk|Ebx6V(KAp zN{&lVVKlk?z?Jo^1k6-bq-plaCBQQga$MOUn14e;67b8waVs^TP>##V^xev;>VV^( zyL8z#?(J+=0zOosbGspz0P`-LhoDLH<3^Jypr9l&P(d*}3}^uuEy!cBSm|1CXxyte zZq*a3XC)w8h2gD4E&(+;S`Sm5BbaL6!NVnmjOwEWyTjpH0_Woc6F{=c`p47ziyUE3 zHPo41&XP<9BLOE>=-q4N5>RRI*#7=ERuFX4R=uR43CZi+&I+@}fI@vSTzEe3O6}UxdixTgN`5ey0cP?1(G&wgu$c7(}XBK zX)RROxHm{7xsfZFBi|tW&T6&7+K)HG(AbF(Gki2GTa^r^$}0D`Mc)Lq+f+mVjRYR$ zFZ+=RBo2a{$Nu#UMO$F9b!r5+AA?!@!mdzNmdf2zLI5d@ zh>oezxsh2G32D5=ro1N!Md!rhR^6k^Bm^b2Gb9r{uY>@C$2+zDWB8H3(()B4jgh@V z$ZCCu-=gU*Ewf>jD1<1cA@l>8N(pc|L)}lAfMhoTSzLD(0!nC| zP#4s>Ud_%Ywv?j1%HpLvxM~&>d^5v9Ag49@nH}lr?NqI+!C@Kp{G`li-%z(ls8NJ? z+WTA}joZ&;1v}LK`u9Yz?9su#MV;v3rEe#hXQKn@_ISK2>`QB-=wPHWyZ;|?Utnh; zAy7oHqVj7dw3Src7t66RGK@>3*0b-}>u|IpLt|hefE=oj8J7C!K|H^gt2_@wTvWIS zNRm7qc5K!?t8{nB1N#d6e(({JGoxRGVgg8mL;j|aH(XVC9*p{Jkib)-bu0vsWU9{3 z+T~kkSwNaGLQKGdfE^-^o2{S5h)~1k5IK=UTsz?P-m4g~?zm>MN9H~0?= z0ma+hyG3Hgtjf>qA1N!x2!IViwaQ|}=q?Kh^KzjUH2}mCQQn1P&^4A)ttXDW! z{d;t7RcxVA1IH7(F%v-gk%LhFs+?PAvfAa1X?QGF0UnZj zBBGs&c%>yb< zvPQ;xMz)5$eMm1&;UVdm5|55jxg93OxN}_hH|fME0lQHPw)JIAa$*|w+MFYI=&!Jn zq(>s`^1~ws=3E`U_;4n%2V}%`q_u$)t6rswGlV>n+b{5!;WgUDNn$u`O04t>9xjJ! t7YP<}P)W|}$Z-`pv8lg8-!a_s{U5wR-X^&2Wu*WB002ovPDHLkV1f{KGqL~x 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 304/560] 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 305/560] 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 306/560] 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 307/560] 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 308/560] 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 309/560] 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 310/560] 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 311/560] 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 312/560] 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 313/560] 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 314/560] 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 315/560] 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 316/560] 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 317/560] 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 318/560] 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 319/560] 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 320/560] 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 321/560] 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 322/560] 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 323/560] 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 324/560] 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 325/560] 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 326/560] 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 327/560] 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 328/560] 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 329/560] 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 330/560] 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 331/560] 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 332/560] 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 333/560] 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 334/560] 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 335/560] 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 336/560] 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 337/560] 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 338/560] 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 339/560] 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 340/560] 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 341/560] 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"] = "