support for moderated comments, part of #13
* add moderated = true to your configuration (defaults to false) * comment activation via email to the Isso owner
This commit is contained in:
parent
79f8c1157e
commit
11246f51dc
@ -63,6 +63,7 @@ rules = Map([
|
|||||||
|
|
||||||
Rule('/', methods=['GET'], endpoint=views.comment.fetch),
|
Rule('/', methods=['GET'], endpoint=views.comment.fetch),
|
||||||
Rule('/count', methods=['GET'], endpoint=views.comment.count),
|
Rule('/count', methods=['GET'], endpoint=views.comment.count),
|
||||||
|
Rule('/activate/<string:auth>', endpoint=views.comment.activate),
|
||||||
Rule('/admin/', endpoint=views.admin.index),
|
Rule('/admin/', endpoint=views.admin.index),
|
||||||
|
|
||||||
Rule('/check-ip', endpoint=views.comment.checkip)
|
Rule('/check-ip', endpoint=views.comment.checkip)
|
||||||
@ -85,8 +86,8 @@ class Isso(object):
|
|||||||
def sign(self, obj):
|
def sign(self, obj):
|
||||||
return self.signer.dumps(obj)
|
return self.signer.dumps(obj)
|
||||||
|
|
||||||
def unsign(self, obj):
|
def unsign(self, obj, max_age=None):
|
||||||
return self.signer.loads(obj, max_age=self.conf.getint('general', 'max-age'))
|
return self.signer.loads(obj, max_age=max_age or self.conf.getint('general', 'max-age'))
|
||||||
|
|
||||||
def markdown(self, text):
|
def markdown(self, text):
|
||||||
return misaka.html(text, extensions=misaka.EXT_STRIKETHROUGH \
|
return misaka.html(text, extensions=misaka.EXT_STRIKETHROUGH \
|
||||||
|
@ -40,7 +40,7 @@ class Config:
|
|||||||
"[general]",
|
"[general]",
|
||||||
"dbpath = /tmp/isso.db", "secretkey = %r" % binascii.b2a_hex(os.urandom(24)),
|
"dbpath = /tmp/isso.db", "secretkey = %r" % binascii.b2a_hex(os.urandom(24)),
|
||||||
"host = http://localhost:8080/", "passphrase = p@$$w0rd",
|
"host = http://localhost:8080/", "passphrase = p@$$w0rd",
|
||||||
"max-age = 900",
|
"max-age = 900", "moderated = false",
|
||||||
"[server]",
|
"[server]",
|
||||||
"host = localhost", "port = 8080", "reload = off",
|
"host = localhost", "port = 8080", "reload = off",
|
||||||
"[SMTP]",
|
"[SMTP]",
|
||||||
|
@ -51,7 +51,7 @@ class Comments:
|
|||||||
' ?, ?, ?, ?, ?',
|
' ?, ?, ?, ?, ?',
|
||||||
'FROM threads WHERE threads.uri = ?;'], (
|
'FROM threads WHERE threads.uri = ?;'], (
|
||||||
c.get('parent'),
|
c.get('parent'),
|
||||||
c.get('created') or time.time(), None, self.db.mode, c['remote_addr'],
|
c.get('created') or time.time(), None, c["mode"], c['remote_addr'],
|
||||||
c['text'], c.get('author'), c.get('email'), c.get('website'), buffer(
|
c['text'], c.get('author'), c.get('email'), c.get('website'), buffer(
|
||||||
Bloomfilter(iterable=[c['remote_addr']]).array),
|
Bloomfilter(iterable=[c['remote_addr']]).array),
|
||||||
uri)
|
uri)
|
||||||
@ -61,11 +61,12 @@ class Comments:
|
|||||||
'SELECT *, MAX(c.id) FROM comments AS c INNER JOIN threads ON threads.uri = ?',
|
'SELECT *, MAX(c.id) FROM comments AS c INNER JOIN threads ON threads.uri = ?',
|
||||||
(uri, )).fetchone()))
|
(uri, )).fetchone()))
|
||||||
|
|
||||||
# def activate(self, path, id):
|
def activate(self, id):
|
||||||
# """Activate comment id if pending and return comment for (path, id)."""
|
"""Activate comment id if pending and return comment for (path, id)."""
|
||||||
# with sqlite3.connect(self.dbpath) as con:
|
self.db.execute([
|
||||||
# con.execute("UPDATE comments SET mode=1 WHERE path=? AND id=? AND mode=2", (path, id))
|
'UPDATE comments SET',
|
||||||
# return self.get(path, id)
|
' mode=1',
|
||||||
|
'WHERE id=? AND mode=2'], (id, ))
|
||||||
|
|
||||||
def update(self, id, data):
|
def update(self, id, data):
|
||||||
"""
|
"""
|
||||||
|
@ -4,7 +4,7 @@ from smtplib import SMTP, SMTP_SSL
|
|||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
|
|
||||||
def format(comment, permalink, remote_addr):
|
def format(comment, permalink, remote_addr, activation_key=None):
|
||||||
|
|
||||||
rv = []
|
rv = []
|
||||||
rv.append("%s schrieb:" % (comment["author"] or "Jemand"))
|
rv.append("%s schrieb:" % (comment["author"] or "Jemand"))
|
||||||
@ -18,6 +18,10 @@ def format(comment, permalink, remote_addr):
|
|||||||
rv.append("IP Adresse: %s" % remote_addr)
|
rv.append("IP Adresse: %s" % remote_addr)
|
||||||
rv.append("Link zum Kommentar: %s" % permalink)
|
rv.append("Link zum Kommentar: %s" % permalink)
|
||||||
|
|
||||||
|
if activation_key:
|
||||||
|
rv.append("")
|
||||||
|
rv.append("Kommentar freischalten: %s" % activation_key)
|
||||||
|
|
||||||
return u'\n'.join(rv)
|
return u'\n'.join(rv)
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,6 +67,7 @@ def new(app, environ, request, uri):
|
|||||||
if data.get(field):
|
if data.get(field):
|
||||||
data[field] = cgi.escape(data[field])
|
data[field] = cgi.escape(data[field])
|
||||||
|
|
||||||
|
data['mode'] = (app.conf.getboolean('general', 'moderated') and 2) or 1
|
||||||
data['remote_addr'] = utils.anonymize(str(request.remote_addr))
|
data['remote_addr'] = utils.anonymize(str(request.remote_addr))
|
||||||
|
|
||||||
with app.lock:
|
with app.lock:
|
||||||
@ -83,8 +84,14 @@ def new(app, environ, request, uri):
|
|||||||
except db.IssoDBException:
|
except db.IssoDBException:
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
href = (app.conf.get('general', 'host').rstrip("/") + uri + "#isso-%i" % rv["id"])
|
host = app.conf.get('general', 'host').rstrip("/")
|
||||||
app.notify(title, notify.format(rv, href, utils.anonymize(str(request.remote_addr))))
|
href = host + uri + "#isso-%i" % rv["id"]
|
||||||
|
auth = None
|
||||||
|
|
||||||
|
if app.conf.getboolean('general', 'moderated'):
|
||||||
|
auth = host + environ["SCRIPT_NAME"] + "/activate/" + app.sign(str(rv["id"]))
|
||||||
|
|
||||||
|
app.notify(title, notify.format(rv, href, utils.anonymize(str(request.remote_addr)), auth))
|
||||||
|
|
||||||
# save checksum of text into cookie, so mallory can't modify/delete a comment, if
|
# save checksum of text into cookie, so mallory can't modify/delete a comment, if
|
||||||
# he add a comment, then removed it but not the signed cookie.
|
# he add a comment, then removed it but not the signed cookie.
|
||||||
@ -146,11 +153,8 @@ def single(app, environ, request, id):
|
|||||||
|
|
||||||
data['modified'] = time.time()
|
data['modified'] = time.time()
|
||||||
|
|
||||||
try:
|
with app.lock:
|
||||||
rv = app.db.comments.update(id, data)
|
rv = app.db.comments.update(id, data)
|
||||||
except sqlite3.Error:
|
|
||||||
logging.exception('uncaught SQLite3 exception')
|
|
||||||
abort(400)
|
|
||||||
|
|
||||||
for key in set(rv.keys()) - FIELDS:
|
for key in set(rv.keys()) - FIELDS:
|
||||||
rv.pop(key)
|
rv.pop(key)
|
||||||
@ -218,5 +222,18 @@ def count(app, environ, request, uri):
|
|||||||
return Response(json.dumps(rv), 200, content_type='application/json')
|
return Response(json.dumps(rv), 200, content_type='application/json')
|
||||||
|
|
||||||
|
|
||||||
|
def activate(app, environ, request, auth):
|
||||||
|
|
||||||
|
try:
|
||||||
|
id = app.unsign(auth, max_age=2**32)
|
||||||
|
except (BadSignature, SignatureExpired):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
with app.lock:
|
||||||
|
app.db.comments.activate(id)
|
||||||
|
|
||||||
|
return Response("Yo", 200)
|
||||||
|
|
||||||
|
|
||||||
def checkip(app, env, req):
|
def checkip(app, env, req):
|
||||||
return Response(utils.anonymize(str(req.remote_addr)), 200)
|
return Response(utils.anonymize(str(req.remote_addr)), 200)
|
||||||
|
@ -256,3 +256,34 @@ class TestComments(unittest.TestCase):
|
|||||||
|
|
||||||
self.put('/id/1', data=json.dumps({"text": "Typo"}))
|
self.put('/id/1', data=json.dumps({"text": "Typo"}))
|
||||||
assert loads(self.get('/id/1').data)["text"] == "<p>Typo</p>\n"
|
assert loads(self.get('/id/1').data)["text"] == "<p>Typo</p>\n"
|
||||||
|
|
||||||
|
|
||||||
|
class TestModeratedComments(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
fd, self.path = tempfile.mkstemp()
|
||||||
|
conf = core.Config.load(None)
|
||||||
|
conf.set("general", "dbpath", self.path)
|
||||||
|
conf.set("general", "moderated", "true")
|
||||||
|
conf.set("guard", "enabled", "off")
|
||||||
|
|
||||||
|
class App(Isso, core.Mixin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.app = App(conf)
|
||||||
|
self.app.wsgi_app = FakeIP(self.app.wsgi_app)
|
||||||
|
self.client = Client(self.app, Response)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
os.unlink(self.path)
|
||||||
|
|
||||||
|
def testAddComment(self):
|
||||||
|
|
||||||
|
rv = self.client.post('/new?uri=test', data=json.dumps({"text": "..."}))
|
||||||
|
assert rv.status_code == 202
|
||||||
|
|
||||||
|
assert self.client.get('/id/1').status_code == 200
|
||||||
|
assert self.client.get('/?uri=test').status_code == 404
|
||||||
|
|
||||||
|
self.app.db.comments.activate(1)
|
||||||
|
assert self.client.get('/?uri=test').status_code == 200
|
||||||
|
Loading…
Reference in New Issue
Block a user