Merge pull request #91 from nim-lang/first-post-needs-moderation

First post needs moderation
This commit is contained in:
Andreas Rumpf 2017-01-02 23:20:50 +01:00 • committed by GitHub
commit 27a1305b2d
8 changed files with 249 additions and 249 deletions

View file

@ -16,13 +16,13 @@ proc newCacheHolder*(): CacheHolder =
result.caches = initTable[string, CacheInfo]() result.caches = initTable[string, CacheInfo]()
proc invalidate*(cache: CacheHolder, name: string) = proc invalidate*(cache: CacheHolder, name: string) =
cache.caches.mget(name.normalizePath()).valid = false cache.caches[name.normalizePath()].valid = false
proc invalidateAll*(cache: CacheHolder) = proc invalidateAll*(cache: CacheHolder) =
for key, val in mpairs(cache.caches): for key, val in mpairs(cache.caches):
val.valid = false 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 ## 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. ## cache is valid then doesn't recalculate it but returns the cached version.
mixin normalizePath mixin normalizePath

View file

@ -11,7 +11,7 @@ import cairo, os, strutils, jester
proc getCaptchaFilename*(i: int): string {.inline.} = proc getCaptchaFilename*(i: int): string {.inline.} =
result = "public/captchas/capture_" & $i & ".png" 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) result = req.makeUri("/captchas/capture_" & $i & ".png", absolute = false)
proc createCaptcha*(file, text: string) = proc createCaptcha*(file, text: string) =
@ -23,7 +23,7 @@ proc createCaptcha*(file, text: string) =
setSourceRgb(cr, 1.0, 0.5, 0.0) setSourceRgb(cr, 1.0, 0.5, 0.0)
moveTo(cr, 0.0, 10.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) setSourceRgb(cr, 0.0, 0.0, 1.0)
moveTo(cr, 0.0, 10.0) moveTo(cr, 0.0, 10.0)
@ -35,5 +35,3 @@ proc createCaptcha*(file, text: string) =
when isMainModule: when isMainModule:
createCaptcha("test.png", "1+33") createCaptcha("test.png", "1+33")

View file

@ -36,8 +36,7 @@ create table if not exists person(
email $# not null, email $# not null,
creation timestamp not null default (DATETIME('now')), creation timestamp not null default (DATETIME('now')),
salt varbin(128) not null, 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')) lastOnline timestamp not null default (DATETIME('now'))
);""" % [TUserName, TPassword, TEmail]), []) );""" % [TUserName, TPassword, TEmail]), [])
# echo "person table already exists" # echo "person table already exists"

View file

@ -1,11 +1,13 @@
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") database="nimforum")
db.exec(sql"""ALTER TABLE person add column db.exec(sql("update person set status = ?"), $User)
lastOnline timestamp 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)
close(db) close(db)

View file

@ -1,12 +1,19 @@
#? stdtmpl | standard #? stdtmpl | standard
# #
#template `%`(idx: expr): expr {.immediate.} = #template `%`(idx: untyped): untyped =
# row[idx] # row[idx]
#end template #end template
# #
# #
#proc genThreadsList(c: var TForumData, count: var int): string = #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 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 ?, ?"""
# 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 not in ('Moderated', 'Spammer') or id = ?))
# order by modified desc limit ?, ?"""
# const threadId = 0 # const threadId = 0
# const name = 1 # const name = 1
# const views = 2 # const views = 2
@ -34,7 +41,8 @@
</div> </div>
</div> </div>
<div id="talk-threads"> <div id="talk-threads">
# for row in rows(db, query, $((c.pageNum-1) * ThreadsPerPage), $ThreadsPerPage): # for row in rows(db, if c.rank >= Moderator: queryModAdmin else: query,
# c.userId, $((c.pageNum-1) * ThreadsPerPage), $ThreadsPerPage):
# inc(count) # inc(count)
<div> <div>
<div class="topic"> <div class="topic">
@ -112,8 +120,12 @@
# #
# #
#proc genPostsList(c: var TForumData, threadId: string, count: var int): string = #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 = ? order by p.id limit ?, ?""" # 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 # const postId = 0
# const userName = 1 # const userName = 1
# const postHeader = 2 # const postHeader = 2
@ -123,7 +135,7 @@
# const userEmail = 6 # const userEmail = 6
# result = "" # result = ""
# count = 0 # count = 0
# let posts = getAllRows(db, query, threadId, $((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 "" # if posts.len < 1: return ""
# end if # end if
<div id="talk-head"> <div id="talk-head">
@ -146,7 +158,7 @@
<a class="name" href="$profileUrl">${xmlEncode(%userName)}</a> <a class="name" href="$profileUrl">${xmlEncode(%userName)}</a>
#if c.userId == %postAuthor and c.currentPost.subject.len == 0: #if c.userId == %postAuthor and c.currentPost.subject.len == 0:
<hr/><a href="${c.genThreadUrl(%postId, "edit")}">Edit post</a> <hr/><a href="${c.genThreadUrl(%postId, "edit")}">Edit post</a>
#elif c.isAdmin and c.currentPost.subject.len == 0: #elif c.rank >= Moderator and c.currentPost.subject.len == 0:
<hr/><a style="color: red;" href="${c.genThreadUrl(%postId, "edit")}">Edit post</a> <hr/><a style="color: red;" href="${c.genThreadUrl(%postId, "edit")}">Edit post</a>
#end if #end if
</div> </div>
@ -184,15 +196,15 @@
</div> </div>
<form action="${c.req.makeUri(action, false) & "#preview"}" method="POST"> <form action="${c.req.makeUri(action, false) & "#preview"}" method="POST">
#if action == "doreply": #if action == "doreply":
${HiddenField(c, "subject", title)} ${hiddenField(c, "subject", title)}
#else: #else:
${FieldValid(c, "subject", "Subject:")} ${fieldValid(c, "subject", "Subject:")}
${TextWidget(c, "subject", title, maxlength=100)} ${textWidget(c, "subject", title, maxlength=100)}
<br /> <br />
#end if #end if
${FieldValid(c, "content", "Content:")}<br /> ${fieldValid(c, "content", "Content:")}<br />
${TextAreaWidget(c, "content", content)}<br /> ${textAreaWidget(c, "content", content)}<br />
${FormSession(c, action)} ${formSession(c, action)}
# if isEdit: # if isEdit:
<input type="checkbox" name="delete" value="Delete">Delete Post<br /> <input type="checkbox" name="delete" value="Delete">Delete Post<br />
@ -226,20 +238,20 @@
<form action="${c.req.makeUri("/doregister", false)}" method="POST"> <form action="${c.req.makeUri("/doregister", false)}" method="POST">
<table border="0"> <table border="0">
<tr> <tr>
<td>${FieldValid(c, "name", "Username:")}</td> <td>${fieldValid(c, "name", "Username:")}</td>
<td>${TextWidget(c, "name", reuseText, maxlength=20)}</td> <td>${textWidget(c, "name", reuseText, maxlength=20)}</td>
</tr> </tr>
<tr> <tr>
<td>${FieldValid(c, "new_password", "Password:")}</td> <td>${fieldValid(c, "new_password", "Password:")}</td>
<td><input type="password" name="new_password" /></td> <td><input type="password" name="new_password" /></td>
</tr> </tr>
<tr> <tr>
<td>${FieldValid(c, "email", "E-Mail:")}</td> <td>${fieldValid(c, "email", "E-Mail:")}</td>
<td>${TextWidget(c, "email", reuseText, maxlength=300)}</td> <td>${textWidget(c, "email", reuseText, maxlength=300)}</td>
</tr> </tr>
<tr> <tr>
<td>${FieldValid(c, "antibot", "What is " & antibot(c) & "?")}</td> <td>${fieldValid(c, "antibot", "What is " & antibot(c) & "?")}</td>
<td>${TextWidget(c, "antibot", "", maxlength=4)}</td> <td>${textWidget(c, "antibot", "", maxlength=4)}</td>
</tr> </tr>
</table> </table>
#if c.errorMsg != "": #if c.errorMsg != "":
@ -251,6 +263,29 @@
</form> </form>
#end proc #end proc
# #
#proc genFormSetRank(c: var TForumData; ui: TUserInfo): string =
# result = ""
<form action="${c.req.makeUri("/dosetrank/" & ui.nick, false)}" method="POST">
<table border="0">
<tr>
<th>Reason</th>
<td>${textWidget(c, "reason", ui.ban, maxlength=100)}</td>
</tr>
<tr>
<th>Rank</th>
<td><select name = "rank">
# for i in low(Rank)..high(Rank):
<option ${if i == ui.rank: "selected" else: ""}>
$i
</option>
# end for
</td></select>
</tr>
</table>
<input type="submit" value="Change">
</form>
#end proc
#
#proc genFormLogin(c: var TForumData): string = #proc genFormLogin(c: var TForumData): string =
# result = "" # result = ""
# if not c.loggedIn: # if not c.loggedIn:
@ -288,7 +323,7 @@
# #
# #
#proc genSearchResults(c: var TForumData, #proc genSearchResults(c: var TForumData,
# results: iterator: db_sqlite.TRow {.closure, tags: [FReadDB].}, # results: iterator: db_sqlite.Row {.closure, tags: [ReadDbEffect].},
# count: var int): string = # count: var int): string =
# const threadId = 0 # const threadId = 0
# const threadName = 1 # const threadName = 1
@ -326,7 +361,7 @@
<div style="padding: 8px 0"><a href="$profileUrl">${xmlEncode(%userName)}</a></div> <div style="padding: 8px 0"><a href="$profileUrl">${xmlEncode(%userName)}</a></div>
#if c.userId == %postAuthor and c.currentPost.subject.len == 0: #if c.userId == %postAuthor and c.currentPost.subject.len == 0:
<hr/><a href="${c.genThreadUrl(%postId, "edit", %threadId)}">Edit post</a> <hr/><a href="${c.genThreadUrl(%postId, "edit", %threadId)}">Edit post</a>
#elif c.isAdmin and c.currentPost.subject.len == 0: #elif c.rank >= Moderator and c.currentPost.subject.len == 0:
<hr/><a style="color: red;" href="${c.genThreadUrl(%postId, "edit", %threadId)}">Edit post</a> <hr/><a style="color: red;" href="${c.genThreadUrl(%postId, "edit", %threadId)}">Edit post</a>
#end if #end if
</div> </div>
@ -383,12 +418,12 @@
<form action="${c.req.makeUri("/doresetpassword", false)}" method="POST"> <form action="${c.req.makeUri("/doresetpassword", false)}" method="POST">
<table border="0"> <table border="0">
<tr> <tr>
<td>${FieldValid(c, "nick", "Your nickname:")}</td> <td>${fieldValid(c, "nick", "Your nickname:")}</td>
<td><input type="text" name="nick" maxlength="20" /></td> <td><input type="text" name="nick" maxlength="20" /></td>
</tr> </tr>
<tr> <tr>
<td>${FieldValid(c, "antibot", "What is " & antibot(c) & "?")}</td> <td>${fieldValid(c, "antibot", "What is " & antibot(c) & "?")}</td>
<td>${TextWidget(c, "antibot", "", maxlength=4)}</td> <td>${textWidget(c, "antibot", "", maxlength=4)}</td>
</tr> </tr>
</table> </table>
#if c.errorMsg != "": #if c.errorMsg != "":

358
forum.nim
View file

@ -7,9 +7,9 @@
# #
import 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, captchas, scgi, jester, asyncdispatch, asyncnet, cache, sequtils,
parseutils, utils, random, rst parseutils, utils, random, rst, ranks
when not defined(windows): when not defined(windows):
import bcrypt # TODO import bcrypt # TODO
@ -25,8 +25,6 @@ const
MaxPagesFromCurrent = 8 MaxPagesFromCurrent = 8
noPageNums = ["/login", "/register", "/dologin", "/doregister", "/profile"] noPageNums = ["/login", "/register", "/dologin", "/doregister", "/profile"]
noHomeBtn = ["/", "/login", "/register", "/dologin", "/doregister", "/profile"] noHomeBtn = ["/", "/login", "/register", "/dologin", "/doregister", "/profile"]
banReasonDeactivated = "DEACTIVATED"
banReasonEmailUnconfirmed = "EMAILCONFIRMATION"
type type
TCrud = enum crCreate, crRead, crUpdate, crDelete TCrud = enum crCreate, crRead, crUpdate, crDelete
@ -35,12 +33,12 @@ type
threadid: int threadid: int
postid: int postid: int
userName, userPass, email: string userName, userPass, email: string
isAdmin: bool rank: Rank
TPost = tuple[subject, content: string] TPost = tuple[subject, content: string]
TForumData = object of TSession TForumData = object of TSession
req: PRequest req: Request
userid: string userid: string
actionContent: string actionContent: string
errorMsg, loginErrorMsg: string errorMsg, loginErrorMsg: string
@ -60,8 +58,8 @@ type
totalUsers: int totalUsers: int
totalPosts: int totalPosts: int
totalThreads: int totalThreads: int
newestMember: tuple[nick: string, id: int, isAdmin: bool] newestMember: tuple[nick: string, id: int]
activeUsers: seq[tuple[nick: string, id: int, isAdmin: bool]] activeUsers: seq[tuple[nick: string, id: int]]
TUserInfo = object TUserInfo = object
nick: string nick: string
@ -70,11 +68,12 @@ type
lastOnline: int lastOnline: int
email: string email: string
ban: string ban: string
rank: Rank
ForumError = object of Exception ForumError = object of Exception
var var
db: TDbConn db: DbConn
isFTSAvailable: bool isFTSAvailable: bool
config: Config config: Config
@ -103,27 +102,27 @@ proc loggedIn(c: TForumData): bool =
const const
reuseText = "\1" reuseText = "\1"
proc TextWidget(c: TForumData, name, defaultText: string, proc textWidget(c: TForumData, name, defaultText: string,
maxlength = 30, size = -1): string = maxlength = 30, size = -1): string =
let x = if defaultText != reuseText: defaultText let x = if defaultText != reuseText: defaultText
else: xmlEncode(c.req.params.getOrDefault(name)) else: xmlEncode(c.req.params.getOrDefault(name))
return """<input type="text" name="$1" maxlength="$2" value="$3" $4/>""" % [ return """<input type="text" name="$1" maxlength="$2" value="$3" $4/>""" % [
name, $maxlength, x, if size != -1: "size=\"" & $size & "\"" else: ""] 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( let x = xmlencode(
if defaultText != reuseText: defaultText if defaultText != reuseText: defaultText
else: c.req.params.getOrDefault(name) else: c.req.params.getOrDefault(name)
) )
return """<input type="hidden" name="$1" value="$2"/>""" % [name, x] return """<input type="hidden" name="$1" value="$2"/>""" % [name, x]
proc TextAreaWidget(c: TForumData, name, defaultText: string): string = proc textAreaWidget(c: TForumData, name, defaultText: string): string =
let x = if defaultText != reuseText: defaultText let x = if defaultText != reuseText: defaultText
else: xmlEncode(c.req.params.getOrDefault(name)) else: xmlEncode(c.req.params.getOrDefault(name))
return """<textarea name="$1">$2</textarea>""" % [ return """<textarea name="$1">$2</textarea>""" % [
name, x] name, x]
proc FieldValid(c: TForumData, name, text: string): string = proc fieldValid(c: TForumData, name, text: string): string =
if name == c.invalidField: if name == c.invalidField:
result = """<span style="color:red">$1</span>""" % text result = """<span style="color:red">$1</span>""" % text
else: else:
@ -141,12 +140,12 @@ proc genThreadUrl(c: TForumData, postId = "", action = "", threadid = "", pageNu
result.add("#" & postId) result.add("#" & postId)
result = c.req.makeUri(result, absolute = false) result = c.req.makeUri(result, absolute = false)
proc FormSession(c: var TForumData, nextAction: string): string = proc formSession(c: var TForumData, nextAction: string): string =
return """<input type="hidden" name="threadid" value="$1" /> return """<input type="hidden" name="threadid" value="$1" />
<input type="hidden" name="postid" value="$2" />""" % [ <input type="hidden" name="postid" value="$2" />""" % [
$c.threadId, $c.postid] $c.threadId, $c.postid]
proc UrlButton(c: var TForumData, text, url: string): string = proc urlButton(c: var TForumData, text, url: string): string =
return ("""<a class="url_button" href="$1">$2</a>""") % [ return ("""<a class="url_button" href="$1">$2</a>""") % [
url, text] url, text]
@ -202,7 +201,7 @@ proc formatTimestamp(t: int): string =
return "just now" return "just now"
proc getGravatarUrl(email: string, size = 80): string = 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 & return ("http://www.gravatar.com/avatar/" & $emailMD5 & "?s=" & $size &
"&d=identicon") "&d=identicon")
@ -268,7 +267,7 @@ proc makeIdentHash(user, password, epoch, secret: string,
result = hash(user & password & epoch & secret, bcryptSalt) 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 = 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 = ?",
@ -343,10 +342,10 @@ proc register(c: var TForumData, name, pass, antibot,
# add account to person table # add account to person table
exec(db, exec(db,
sql("INSERT INTO person(name, password, email, salt, status, lastOnline, " & sql("INSERT INTO person(name, password, email, salt, status, lastOnline) " &
"ban) VALUES (?, ?, ?, ?, 'user', DATETIME('now'), ?)"), name, "VALUES (?, ?, ?, ?, ?, DATETIME('now'))"), name,
password, email, salt, password, email, salt,
when defined(dev): "" else: banReasonEmailUnconfirmed) when defined(dev): $Moderated else: $EmailUnconfirmed)
return true return true
@ -384,15 +383,16 @@ proc logout(c: var TForumData) =
c.userpass = "" c.userpass = ""
exec(db, query, c.req.ip, c.req.cookies["sid"]) exec(db, query, c.req.ip, c.req.cookies["sid"])
proc getBanErrorMsg(banValue: string): string = proc getBanErrorMsg(banValue: string; rank: Rank): string =
case banValue if banValue.len > 0:
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 return "You have been banned: " & banValue
case rank
of Spammer: return "You are a spammer."
of Troll: return "You have been banned."
of EmailUnconfirmed:
return "You need to confirm your email first."
of Moderated, User, Moderator, Admin:
return ""
proc checkLoggedIn(c: var TForumData) = proc checkLoggedIn(c: var TForumData) =
if not c.req.cookies.hasKey("sid"): return if not c.req.cookies.hasKey("sid"): return
@ -407,14 +407,13 @@ proc checkLoggedIn(c: var TForumData) =
c.req.ip, pass) c.req.ip, pass)
let row = getRow(db, 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.username = ||row[0]
c.email = ||row[1] c.email = ||row[1]
c.isAdmin = parseBool(||row[2]) c.rank = parseEnum[Rank](||row[2])
# Check ban status. let ban = getBanErrorMsg(||row[3], c.rank)
let banErrorMsg = getBanErrorMsg(||row[3]) if ban.len > 0:
if banErrorMsg.len > 0: discard c.setError("name", ban)
discard c.setError("name", banErrorMsg)
logout(c) logout(c)
return return
@ -442,7 +441,7 @@ proc validateRst(c: var TForumData, content: string): bool =
except EParseError: except EParseError:
result = setError(c, "", getCurrentExceptionMsg()) 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 case c
of crCreate: of crCreate:
var fields = "insert into " & table & "(" var fields = "insert into " & table & "("
@ -470,14 +469,14 @@ proc crud(c: TCrud, table: string, data: varargs[string]): TSqlQuery =
of crDelete: of crDelete:
result = sql("delete from " & table & " where id = ?") result = sql("delete from " & table & " where id = ?")
template retrSubject(c: expr) = template retrSubject(c: untyped) =
if not c.req.params.hasKey("subject"): if not c.req.params.hasKey("subject"):
raise newException(ForumError, "Subject empty") raise newException(ForumError, "Subject empty")
let subject {.inject.} = c.req.params["subject"] let subject {.inject.} = c.req.params["subject"]
if subject.strip.len < 3: if subject.strip.len < 3:
return setError(c, "subject", "Subject not long enough") return setError(c, "subject", "Subject not long enough")
template retrContent(c: expr) = template retrContent(c: untyped) =
if not c.req.params.hasKey("content"): if not c.req.params.hasKey("content"):
raise newException(ForumError, "Content empty") raise newException(ForumError, "Content empty")
let content {.inject.} = c.req.params["content"] let content {.inject.} = c.req.params["content"]
@ -486,25 +485,25 @@ template retrContent(c: expr) =
if not validateRst(c, content): return false if not validateRst(c, content): return false
template retrPost(c: expr) = template retrPost(c: untyped) =
retrSubject(c) retrSubject(c)
retrContent(c) retrContent(c)
template checkLogin(c: expr) = template checkLogin(c: untyped) =
if not loggedIn(c): return setError(c, "", "User is not logged in") 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: if c.rank < Moderator:
let x = getValue(db, sql"select author from post where id = ?", let x = getValue(db, sql"select author from post where id = ?",
postId) postId)
if x != c.userId: if x != c.userId:
return setError(c, "", "You are not the owner of this post") 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.subject = subject
c.currentPost.content = content c.currentPost.content = content
template writeToDb(c, cr, setPostId: expr) = template writeToDb(c, cr, setPostId: untyped) =
# insert a comment in the DB # insert a comment in the DB
let retID = insertID(db, crud(cr, "post", "author", "ip", "header", "content", "thread"), let retID = insertID(db, crud(cr, "post", "author", "ip", "header", "content", "thread"),
c.userId, c.req.ip, subject, content, $c.threadId, "") c.userId, c.req.ip, subject, content, $c.threadId, "")
@ -591,7 +590,8 @@ proc spamCheck(c: var TForumData, subject, content: string): bool =
for word in ["appliance", "kitchen", "cheap", "sale", "relocating", for word in ["appliance", "kitchen", "cheap", "sale", "relocating",
"packers", "lenders", "fifa", "coins"]: "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 return true
proc rateLimitCheck(c: var TForumData): bool = proc rateLimitCheck(c: var TForumData): bool =
@ -616,6 +616,13 @@ proc rateLimitCheck(c: var TForumData): bool =
proc makeThreadURL(c: var TForumData): string = proc makeThreadURL(c: var TForumData): string =
c.req.makeUri("/t/" & $c.threadId) 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 = proc reply(c: var TForumData): bool =
# reply to an existing thread # reply to an existing thread
checkLogin(c) checkLogin(c)
@ -623,17 +630,15 @@ proc reply(c: var TForumData): bool =
if c.isPreview: if c.isPreview:
setPreviewData(c) setPreviewData(c)
else: else:
if spamCheck(c, subject, content): postChecks()
echo("[WARNING] Found spam: ", subject)
return true
if rateLimitCheck(c):
return setError(c, "subject", "You're posting too fast.")
writeToDb(c, crCreate, true) writeToDb(c, crCreate, true)
exec(db, sql"update thread set modified = DATETIME('now') where id = ?", exec(db, sql"update thread set modified = DATETIME('now') where id = ?",
$c.threadId) $c.threadId)
asyncCheck sendMailToMailingList(c.config, c.username, c.email, if c.rank >= User:
subject, content, threadId=c.threadId, postId=c.postID, is_reply=true, threadUrl=c.makeThreadURL()) asyncCheck sendMailToMailingList(c.config, c.username, c.email,
subject, content, threadId=c.threadId, postId=c.postID, is_reply=true,
threadUrl=c.makeThreadURL())
result = true result = true
proc newThread(c: var TForumData): bool = proc newThread(c: var TForumData): bool =
@ -645,11 +650,7 @@ proc newThread(c: var TForumData): bool =
setPreviewData(c) setPreviewData(c)
c.threadID = transientThread c.threadID = transientThread
else: else:
if spamCheck(c, subject, content): postChecks()
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 c.threadID = tryInsertID(db, query, c.req.params["subject"]).int
if c.threadID < 0: return setError(c, "subject", "Subject already exists") if c.threadID < 0: return setError(c, "subject", "Subject already exists")
discard tryExec(db, crud(crCreate, "thread_fts", "id", "name"), discard tryExec(db, crud(crCreate, "thread_fts", "id", "name"),
@ -657,26 +658,29 @@ proc newThread(c: var TForumData): bool =
writeToDb(c, crCreate, false) writeToDb(c, crCreate, false)
discard tryExec(db, sql"insert into post_fts(post_fts) values('optimize')") discard tryExec(db, sql"insert into post_fts(post_fts) values('optimize')")
discard tryExec(db, sql"insert into post_fts(thread_fts) values('optimize')") discard tryExec(db, sql"insert into post_fts(thread_fts) values('optimize')")
asyncCheck sendMailToMailingList(c.config, c.username, c.email, if c.rank >= User:
subject, content, threadId=c.threadID, postId=c.postID, is_reply=false, threadUrl=c.makeThreadURL()) asyncCheck sendMailToMailingList(c.config, c.username, c.email,
subject, content, threadId=c.threadID, postId=c.postID, is_reply=false,
threadUrl=c.makeThreadURL())
result = true result = true
proc login(c: var TForumData, name, pass: string): bool = proc login(c: var TForumData, name, pass: string): bool =
# get form data: # get form data:
const query = 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: if name.len == 0:
return c.setError("name", "Username cannot be nil.") return c.setError("name", "Username cannot be nil.")
var success = false var success = false
for row in fastRows(db, query, name): for row in fastRows(db, query, name):
if row[2] == makePassword(pass, row[4], row[2]): if row[2] == makePassword(pass, row[4], row[2]):
if row[6].len > 0: c.rank = parseEnum[Rank](row[5])
return c.setError("name", getBanErrorMsg(row[6])) let ban = getBanErrorMsg(row[6], c.rank)
if ban.len > 0:
return c.setError("name", ban)
c.userid = row[0] c.userid = row[0]
c.username = row[1] c.username = row[1]
c.userpass = row[2] c.userpass = row[2]
c.email = row[3] c.email = row[3]
c.isAdmin = row[5].parseBool
success = true success = true
break break
if success: if success:
@ -699,17 +703,24 @@ proc verifyIdentHash(c: var TForumData, name, epoch, ident: string): bool =
if row[2].parseInt > (epoch.parseInt + 60): return false if row[2].parseInt > (epoch.parseInt + 60): return false
result = newIdent == ident result = newIdent == ident
proc setBan(c: var TForumData, nick, reason: string): bool =
const query =
sql("update person set ban = ? where name = ?")
return tryExec(db, query, reason, nick)
proc deleteAll(c: var TForumData, nick: string): bool = proc deleteAll(c: var TForumData, nick: string): bool =
const query = const query =
sql("delete from post where author = (select id from person where name = ?)") sql("delete from post where author = (select id from person where name = ?)")
result = tryExec(db, query, nick) result = tryExec(db, query, nick)
result = result and updateThreads(c) >= 0 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 = proc setPassword(c: var TForumData, nick, pass: string): bool =
const query = const query =
sql("update person set password = ?, salt = ? where name = ?") sql("update person set password = ?, salt = ? where name = ?")
@ -738,17 +749,17 @@ proc getStats(c: var TForumData, simple: bool): TForumStats =
if not simple: if not simple:
var newestMemberCreation = 0 var newestMemberCreation = 0
result.activeUsers = @[] result.activeUsers = @[]
result.newestMember = ("", -1, false) result.newestMember = ("", -1)
const getUsersQuery = 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): for row in fastRows(db, getUsersQuery):
let secs = if row[3] == "": 0 else: row[3].parseint let secs = if row[3] == "": 0 else: row[3].parseint
let lastOnlineSeconds = getTime() - Time(secs) let lastOnlineSeconds = getTime() - Time(secs)
if lastOnlineSeconds < (60 * 5): # 5 minutes 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: if row[3].parseInt > newestMemberCreation:
result.newestMember = (row[1], row[0].parseInt, row[2].parseBool) result.newestMember = (row[1], row[0].parseInt)
newestMemberCreation = row[4].parseInt newestMemberCreation = row[3].parseInt
proc genPagenumNav(c: var TForumData, stats: TForumStats): string = proc genPagenumNav(c: var TForumData, stats: TForumStats): string =
result = "" result = ""
@ -873,7 +884,7 @@ proc gatherUserInfo(c: var TForumData, nick: string, ui: var TUserInfo): bool =
if uid == "": return false if uid == "": return false
result = true result = true
const totalPostsQuery = const totalPostsQuery =
sql"SELECT count(*) FROM post WHERE author = ?" sql"select count(*) from post where author = ?"
ui.posts = getValue(db, totalPostsQuery, uid).parseInt ui.posts = getValue(db, totalPostsQuery, uid).parseInt
const totalThreadsQuery = const totalThreadsQuery =
sql("select count(*) from thread where id in (select thread from post where" & sql("select count(*) from thread where id in (select thread from post where" &
@ -881,14 +892,16 @@ proc gatherUserInfo(c: var TForumData, nick: string, ui: var TUserInfo): bool =
ui.threads = getValue(db, totalThreadsQuery, uid).parseInt ui.threads = getValue(db, totalThreadsQuery, uid).parseInt
const lastOnlineQuery = const lastOnlineQuery =
sql"select strftime('%s', lastOnline) from person where id = ?" sql"""select strftime('%s', lastOnline), email, ban, status
let lastOnlineDBVal = getValue(db, lastOnlineQuery, uid) from person where id = ?"""
ui.lastOnline = if lastOnlineDBVal != "": lastOnlineDBVal.parseInt else: -1 let row = db.getRow(lastOnlineQuery, $uid)
ui.email = getValue(db, sql"select email from person where id = ?", uid) ui.lastOnline = if row[0].len > 0: row[0].parseInt else: -1
ui.ban = getValue(db, sql"select ban from person where id = ?", uid) ui.email = row[1]
ui.ban = row[2]
ui.rank = parseEnum[Rank](row[3])
proc genSetUserStatusUrl(c: var TForumData, nick: string, typ: string): string = include "forms.tmpl"
c.req.makeUri("/setUserStatus?nick=$1&type=$2" % [nick, typ]) include "main.tmpl"
proc genProfile(c: var TForumData, ui: TUserInfo): string = proc genProfile(c: var TForumData, ui: TUserInfo): string =
result = "" result = ""
@ -929,47 +942,17 @@ proc genProfile(c: var TForumData, ui: TUserInfo): string =
), ),
tr( tr(
th("Status"), th("Status"),
td(case ui.ban td($ui.rank)
of banReasonDeactivated:
"Deactivated"
of banReasonEmailUnconfirmed:
"Awaiting email confirmation"
of "":
"Active"
else:
"Banned: " & ui.ban
)
), ),
tr( tr(
th(""), th(""),
td(if c.isAdmin and ui.ban != banReasonDeactivated: td(if c.rank >= Moderator and c.rank > ui.rank:
if ui.ban == "": c.genFormSetRank(ui)
htmlgen.a(
href=c.genSetUserStatusUrl(ui.nick, "ban"),
"Ban user")
else:
htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "unban"),
"Unban user")
else: "") else: "")
), ),
tr( tr(
th(""), th(""),
td(if c.userName == ui.nick or c.isAdmin: td(if c.rank >= Moderator:
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:
htmlgen.a(href=c.genSetUserStatusUrl(ui.nick, "activate"),
"Confirm user's email")
else: ""
else: "")
),
tr(
th(""),
td(if c.isAdmin:
htmlgen.a(href=c.req.makeUri("/deleteAll?nick=$1" % ui.nick), htmlgen.a(href=c.req.makeUri("/deleteAll?nick=$1" % ui.nick),
"Delete all user's posts and threads") "Delete all user's posts and threads")
else: "") else: "")
@ -980,16 +963,13 @@ proc genProfile(c: var TForumData, ui: TUserInfo): string =
result = htmlgen.`div`(id = "profile", result = htmlgen.`div`(id = "profile",
htmlgen.`div`(id = "left", result)) htmlgen.`div`(id = "left", result))
include "forms.tmpl"
include "main.tmpl"
proc prependRe(s: string): string = proc prependRe(s: string): string =
result = if s.len == 0: result = if s.len == 0:
"" ""
elif s.startswith("Re:"): s elif s.startswith("Re:"): s
else: "Re: " & s else: "Re: " & s
template createTFD(): stmt = template createTFD() =
var c {.inject.}: TForumData var c {.inject.}: TForumData
init(c) init(c)
c.req = request c.req = request
@ -1031,7 +1011,7 @@ routes:
parseInt(@"page", c.pageNum, 0..1000_000) parseInt(@"page", c.pageNum, 0..1000_000)
if @"postid".len > 0: if @"postid".len > 0:
parseInt(@"postid", c.postId, 0..1000_000) parseInt(@"postid", c.postId, 0..1000_000)
cond (c.pageNum > 0) cond(c.pageNum > 0)
var count = 0 var count = 0
var pSubject = getThreadTitle(c.threadid, c.pageNum) var pSubject = getThreadTitle(c.threadid, c.pageNum)
cond validThreadId(c) cond validThreadId(c)
@ -1066,9 +1046,9 @@ routes:
get "/page/?@page?/?": get "/page/?@page?/?":
createTFD() createTFD()
c.isThreadsList = true c.isThreadsList = true
cond (@"page" != "") cond(@"page" != "")
parseInt(@"page", c.pageNum, 0..1000_000) parseInt(@"page", c.pageNum, 0..1000_000)
cond (c.pageNum > 0) cond(c.pageNum > 0)
var count = 0 var count = 0
let list = genThreadsList(c, count) let list = genThreadsList(c, count)
if count == 0: if count == 0:
@ -1078,7 +1058,7 @@ routes:
get "/profile/@nick/?": get "/profile/@nick/?":
createTFD() createTFD()
cond (@"nick" != "") cond(@"nick" != "")
var userinfo: TUserInfo var userinfo: TUserInfo
if gatherUserInfo(c, @"nick", userinfo): if gatherUserInfo(c, @"nick", userinfo):
resp genMain(c, c.genProfile(userinfo), resp genMain(c, c.genProfile(userinfo),
@ -1099,18 +1079,18 @@ routes:
createTFD() createTFD()
resp genMain(c, genFormRegister(c), "Register - Nim Forum") resp genMain(c, genFormRegister(c), "Register - Nim Forum")
template readIDs(): stmt = template readIDs() =
# Retrieve the threadid, postid and pagenum # Retrieve the threadid, postid and pagenum
if (@"threadid").len > 0: if (@"threadid").len > 0:
parseInt(@"threadid", c.threadId, -1..1000_000) parseInt(@"threadid", c.threadId, -1..1000_000)
if (@"postid").len > 0: if (@"postid").len > 0:
parseInt(@"postid", c.postId, -1..1000_000) parseInt(@"postid", c.postId, -1..1000_000)
template finishLogin(): stmt = template finishLogin() =
setCookie("sid", c.userpass, daysForward(7)) setCookie("sid", c.userpass, daysForward(7))
redirect(uri("/")) redirect(uri("/"))
template handleError(action: string, topText: string, isEdit: bool): stmt = template handleError(action: string, topText: string, isEdit: bool) =
if c.isPreview: if c.isPreview:
body.add genPostPreview(c, @"subject", @"content", body.add genPostPreview(c, @"subject", @"content",
c.userName, $getGMTime(getTime())) c.userName, $getGMTime(getTime()))
@ -1174,67 +1154,9 @@ routes:
resp genMain(c, genFormPost(c, "donewthread", "New thread", "", "", false), resp genMain(c, genFormPost(c, "donewthread", "New thread", "", "", false),
"New Thread - Nim Forum") "New Thread - Nim Forum")
get "/setUserStatus/?":
createTFD()
cond (@"nick" != "")
cond (@"type" != "")
var formBody = "<input type=\"hidden\" name=\"nick\" value=\"" &
@"nick" & "\">"
var del = false
var content = ""
echo("Got type: ", @"type")
case @"type"
of "ban":
formBody.add "<input type='text' name='reason' />" &
"<input type='submit' name='banBtn' value='Ban' />"
content =
htmlgen.p("Please enter a reason for banning this user:")
of "unban":
formBody.add "<input type='submit' name='unbanBtn' value='Unban' />"
content =
htmlgen.p("Are you sure you wish to unban ", htmlgen.b(@"nick"),
"?")
del = true
of "deactivate":
formBody.add "<input type='hidden' name='reason' value='" &
banReasonDeactivated & "' />" &
"<input type='submit' name='actBtn' value='Deactivate' />"
content =
htmlgen.p("Are you sure you wish to deactivate ", htmlgen.b(@"nick"),
"?")
of "activate":
formBody.add "<input type='submit' name='actBtn' value='Activate' />"
content =
htmlgen.p("Are you sure you wish to activate ", htmlgen.b(@"nick"),
"?")
del = true
formBody.add "<input type='hidden' name='del' value='" & $del & "'/>"
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 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")
get "/deleteAll/?": get "/deleteAll/?":
createTFD() createTFD()
cond (@"nick" != "") cond(@"nick" != "")
var formBody = "<input type=\"hidden\" name=\"nick\" value=\"" & var formBody = "<input type=\"hidden\" name=\"nick\" value=\"" &
@"nick" & "\">" @"nick" & "\">"
var del = false var del = false
@ -1248,8 +1170,8 @@ routes:
post "/dodeleteall/?": post "/dodeleteall/?":
createTFD() createTFD()
cond (@"nick" != "") cond(@"nick" != "")
if not c.isAdmin: if c.rank < Moderator:
resp genMain(c, "You cannot delete this user's data.", "Error - Nim Forum") resp genMain(c, "You cannot delete this user's data.", "Error - Nim Forum")
let result = deleteAll(c, @"nick") let result = deleteAll(c, @"nick")
if result: if result:
@ -1258,11 +1180,31 @@ routes:
resp genMain(c, "Failed to delete all user's posts and threads.", resp genMain(c, "Failed to delete all user's posts and threads.",
"Error - NimForum") "Error - NimForum")
post "/dosetrank/?@nick?/?":
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")
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/?": get "/setpassword/?":
createTFD() createTFD()
cond (@"nick" != "") cond(@"nick" != "")
cond (@"pass" != "") cond(@"pass" != "")
if not c.isAdmin: if c.rank < Moderator:
resp genMain(c, "You cannot change this user's pass.", "Error - Nim Forum") resp genMain(c, "You cannot change this user's pass.", "Error - Nim Forum")
let res = setPassword(c, @"nick", @"pass") let res = setPassword(c, @"nick", @"pass")
if res: if res:
@ -1272,16 +1214,16 @@ routes:
get "/activateEmail/?": get "/activateEmail/?":
createTFD() createTFD()
cond (@"nick" != "") cond(@"nick" != "")
cond (@"epoch" != "") cond(@"epoch" != "")
cond (@"ident" != "") cond(@"ident" != "")
var epoch: BiggestInt = 0 var epoch: BiggestInt = 0
cond(parseBiggestInt(@"epoch", epoch) > 0) cond(parseBiggestInt(@"epoch", epoch) > 0)
var success = false var success = false
if verifyIdentHash(c, @"nick", $epoch, @"ident"): if verifyIdentHash(c, @"nick", $epoch, @"ident"):
let ban = db.getValue(sql"select ban from person where name = ?", @"nick") let ban = parseEnum[Rank](db.getValue(sql"select status from person where name = ?", @"nick"))
if ban == banReasonEmailUnconfirmed: if ban == EmailUnconfirmed:
success = setBan(c, @"nick", "") success = setStatus(c, @"nick", Moderated, "")
if success: if success:
resp genMain(c, "Account activated", "Nim Forum") resp genMain(c, "Account activated", "Nim Forum")
@ -1290,9 +1232,9 @@ routes:
get "/emailResetPassword/?": get "/emailResetPassword/?":
createTFD() createTFD()
cond (@"nick" != "") cond(@"nick" != "")
cond (@"epoch" != "") cond(@"epoch" != "")
cond (@"ident" != "") cond(@"ident" != "")
var epoch: BiggestInt = 0 var epoch: BiggestInt = 0
cond(parseBiggestInt(@"epoch", epoch) > 0) cond(parseBiggestInt(@"epoch", epoch) > 0)
if verifyIdentHash(c, @"nick", $epoch, @"ident"): if verifyIdentHash(c, @"nick", $epoch, @"ident"):
@ -1314,10 +1256,10 @@ routes:
post "/doemailresetpassword": post "/doemailresetpassword":
createTFD() createTFD()
cond (@"nick" != "") cond(@"nick" != "")
cond (@"epoch" != "") cond(@"epoch" != "")
cond (@"ident" != "") cond(@"ident" != "")
cond (@"password" != "") cond(@"password" != "")
var epoch: BiggestInt = 0 var epoch: BiggestInt = 0
cond(parseBiggestInt(@"epoch", epoch) > 0) cond(parseBiggestInt(@"epoch", epoch) > 0)
if verifyIdentHash(c, @"nick", $epoch, @"ident"): if verifyIdentHash(c, @"nick", $epoch, @"ident"):
@ -1337,7 +1279,7 @@ routes:
post "/doresetpassword": post "/doresetpassword":
createTFD() createTFD()
echo(request.params) echo(request.params)
cond (@"nick" != "") cond(@"nick" != "")
if resetPassword(c, @"nick", @"antibot"): if resetPassword(c, @"nick", @"antibot"):
resp genMain(c, "Email sent!", "Reset Password - Nim Forum") resp genMain(c, "Email sent!", "Reset Password - Nim Forum")
@ -1359,11 +1301,11 @@ routes:
for i in 0 .. q.len-1: for i in 0 .. q.len-1:
if q[i].int < 32: q[i] = ' ' if q[i].int < 32: q[i] = ' '
elif q[i] == '\'': q[i] = '"' elif q[i] == '\'': q[i] = '"'
c.search = q.replace("\"","&quot;"); c.search = q.replace("\"","&quot;")
if @"page".len > 0: if @"page".len > 0:
parseInt(@"page", c.pageNum, 0..1000_000) parseInt(@"page", c.pageNum, 0..1000_000)
cond (c.pageNum > 0) 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 const queryFT = "fts.sql".slurp.sql
for rowFT in fastRows(db, queryFT, for rowFT in fastRows(db, queryFT,
[q,q,$ThreadsPerPage,$c.pageNum,$ThreadsPerPage,q, [q,q,$ThreadsPerPage,$c.pageNum,$ThreadsPerPage,q,
@ -1373,7 +1315,7 @@ routes:
additionalHeaders = genRSSHeaders(c), showRssLinks = true) additionalHeaders = genRSSHeaders(c), showRssLinks = true)
# tries first to read html, then to read rst, convert ot html, cache and return # 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() createTFD()
#c.isThreadsList = true #c.isThreadsList = true
var page = "" var page = ""

11
ranks.nim Normal file
View file

@ -0,0 +1,11 @@
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

View file

@ -1,7 +1,20 @@
import asyncdispatch, smtp, strutils, json, os, rst, rstgen, xmltree, strtabs, import asyncdispatch, smtp, strutils, json, os, rst, rstgen, xmltree, strtabs,
htmlparser, streams htmlparser, streams, parseutils
from times import getTime, getGMTime, format 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 type
Config* = object Config* = object
smtpAddress: string smtpAddress: string