commit
9d8afecc5c
2 changed files with 88 additions and 75 deletions
80
forum.nim
80
forum.nim
|
|
@ -8,8 +8,8 @@
|
||||||
|
|
||||||
import
|
import
|
||||||
os, strutils, times, md5, strtabs, cgi, math, db_sqlite, matchers,
|
os, strutils, times, md5, strtabs, cgi, math, db_sqlite, matchers,
|
||||||
rst, rstgen, captchas, scgi, jester, asyncdispatch, asyncnet, cache, sequtils,
|
captchas, scgi, jester, asyncdispatch, asyncnet, cache, sequtils,
|
||||||
parseutils, utils, htmlparser, xmltree, streams, random
|
parseutils, utils, random, rst
|
||||||
|
|
||||||
when not defined(windows):
|
when not defined(windows):
|
||||||
import bcrypt # TODO
|
import bcrypt # TODO
|
||||||
|
|
@ -75,7 +75,6 @@ type
|
||||||
|
|
||||||
var
|
var
|
||||||
db: TDbConn
|
db: TDbConn
|
||||||
docConfig: StringTableRef
|
|
||||||
isFTSAvailable: bool
|
isFTSAvailable: bool
|
||||||
config: Config
|
config: Config
|
||||||
|
|
||||||
|
|
@ -436,70 +435,6 @@ proc isPreview(c: TForumData): bool =
|
||||||
proc isDelete(c: TForumData): bool =
|
proc isDelete(c: TForumData): bool =
|
||||||
result = c.req.params.hasKey("delete")
|
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 .. <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, newNode, n)
|
|
||||||
else:
|
|
||||||
blockquoteFinish(currentBlockquote, newNode, n)
|
|
||||||
of xnText:
|
|
||||||
if n.text[0] == '\10':
|
|
||||||
newNode.add(n)
|
|
||||||
else:
|
|
||||||
blockquoteFinish(currentBlockquote, newNode, n)
|
|
||||||
else:
|
|
||||||
blockquoteFinish(currentBlockquote, newNode, n)
|
|
||||||
result = $newNode
|
|
||||||
except:
|
|
||||||
echo("[WARNING] Could not parse rst html.")
|
|
||||||
|
|
||||||
proc validateRst(c: var TForumData, content: string): bool =
|
proc validateRst(c: var TForumData, content: string): bool =
|
||||||
result = true
|
result = true
|
||||||
try:
|
try:
|
||||||
|
|
@ -678,6 +613,9 @@ proc rateLimitCheck(c: var TForumData): bool =
|
||||||
if last300s > 6: return true
|
if last300s > 6: return true
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
proc makeThreadURL(c: var TForumData): string =
|
||||||
|
c.req.makeUri("/t/" & $c.threadId)
|
||||||
|
|
||||||
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)
|
||||||
|
|
@ -695,7 +633,7 @@ proc reply(c: var TForumData): bool =
|
||||||
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,
|
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
|
result = true
|
||||||
|
|
||||||
proc newThread(c: var TForumData): bool =
|
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(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,
|
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
|
result = true
|
||||||
|
|
||||||
proc login(c: var TForumData, name, pass: string): bool =
|
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))
|
lastUrl = c.req.makeUri("/page/" & $(totalPages))
|
||||||
nextUrl = c.req.makeUri("/page/" & $(c.pageNum+1))
|
nextUrl = c.req.makeUri("/page/" & $(c.pageNum+1))
|
||||||
else:
|
else:
|
||||||
firstUrl = c.req.makeUri("/t/" & $c.threadId)
|
firstUrl = c.makeThreadURL()
|
||||||
if c.pageNum == 1:
|
if c.pageNum == 1:
|
||||||
prevUrl = firstUrl
|
prevUrl = firstUrl
|
||||||
else:
|
else:
|
||||||
|
|
@ -1456,8 +1394,6 @@ routes:
|
||||||
textPage "static/search-help"
|
textPage "static/search-help"
|
||||||
|
|
||||||
when isMainModule:
|
when isMainModule:
|
||||||
docConfig = rstgen.defaultConfig()
|
|
||||||
docConfig["doc.smiley_format"] = "/images/smilieys/$1.png"
|
|
||||||
randomize()
|
randomize()
|
||||||
db = open(connection="nimforum.db", user="postgres", password="",
|
db = open(connection="nimforum.db", user="postgres", password="",
|
||||||
database="nimforum")
|
database="nimforum")
|
||||||
|
|
|
||||||
83
utils.nim
83
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
|
from times import getTime, getGMTime, format
|
||||||
|
|
||||||
type
|
type
|
||||||
|
|
@ -9,6 +10,11 @@ type
|
||||||
smtpPassword: string
|
smtpPassword: string
|
||||||
mlistAddress: string
|
mlistAddress: string
|
||||||
|
|
||||||
|
var docConfig: StringTableRef
|
||||||
|
|
||||||
|
docConfig = rstgen.defaultConfig()
|
||||||
|
docConfig["doc.smiley_format"] = "/images/smilieys/$1.png"
|
||||||
|
|
||||||
proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config =
|
proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config =
|
||||||
result = Config(smtpAddress: "", smtpPort: 25, smtpUser: "",
|
result = Config(smtpAddress: "", smtpPort: 25, smtpUser: "",
|
||||||
smtpPassword: "", mlistAddress: "")
|
smtpPassword: "", mlistAddress: "")
|
||||||
|
|
@ -22,6 +28,70 @@ proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config =
|
||||||
except:
|
except:
|
||||||
echo("[WARNING] Couldn't read config file: ", filename)
|
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 .. <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, newNode, n)
|
||||||
|
else:
|
||||||
|
blockquoteFinish(currentBlockquote, newNode, n)
|
||||||
|
of xnText:
|
||||||
|
if n.text[0] == '\10':
|
||||||
|
newNode.add(n)
|
||||||
|
else:
|
||||||
|
blockquoteFinish(currentBlockquote, newNode, n)
|
||||||
|
else:
|
||||||
|
blockquoteFinish(currentBlockquote, newNode, n)
|
||||||
|
result = $newNode
|
||||||
|
except:
|
||||||
|
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.} =
|
proc sendMail(config: Config, subject, message, recipient: string, from_addr = "forum@nim-lang.org", otherHeaders:seq[(string, string)] = @[]) {.async.} =
|
||||||
if config.smtpAddress.len == 0:
|
if config.smtpAddress.len == 0:
|
||||||
echo("[WARNING] Cannot send mail: no smtp server configured (smtpAddress).")
|
echo("[WARNING] Cannot send mail: no smtp server configured (smtpAddress).")
|
||||||
|
|
@ -42,7 +112,7 @@ proc sendMail(config: Config, subject, message, recipient: string, from_addr = "
|
||||||
|
|
||||||
await client.sendMail(from_addr, toList, $encoded)
|
await client.sendMail(from_addr, toList, $encoded)
|
||||||
|
|
||||||
proc sendMailToMailingList*(config: Config, username, user_email_addr, subject, message: string, thread_id=0, post_id=0, is_reply=false) {.async.} =
|
proc sendMailToMailingList*(config: Config, username, user_email_addr, subject, message: string, threadUrl: string, thread_id=0, post_id=0, is_reply=false) {.async.} =
|
||||||
# send message to a mailing list
|
# send message to a mailing list
|
||||||
if config.mlistAddress.len == 0:
|
if config.mlistAddress.len == 0:
|
||||||
echo("[WARNING] Cannot send mail: no mlistAddress configured.")
|
echo("[WARNING] Cannot send mail: no mlistAddress configured.")
|
||||||
|
|
@ -67,7 +137,14 @@ proc sendMailToMailingList*(config: Config, username, user_email_addr, subject,
|
||||||
let msg_id = "<forum-tid-$#@nim-lang.org>" % $thread_id
|
let msg_id = "<forum-tid-$#@nim-lang.org>" % $thread_id
|
||||||
otherHeaders.add(("Message-ID", msg_id))
|
otherHeaders.add(("Message-ID", msg_id))
|
||||||
|
|
||||||
await sendMail(config, subject, message, config.mlistAddress, from_addr=from_addr, otherHeaders=otherHeaders)
|
var processedMsg: string
|
||||||
|
try:
|
||||||
|
processedMsg = rstToHTML(message) & "<hr/><a href=\"" & threadUrl & "\" style=\"font-size:small\">View thread on Nim forum</a>"
|
||||||
|
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.} =
|
proc sendPassReset*(config: Config, email, user, resetUrl: string) {.async.} =
|
||||||
let message = """Hello $1,
|
let message = """Hello $1,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue