Reply notification (#443)
Merging @pellenilsson reply notification PR * Added reply notification for commenter * Removed debug info * Bugfix * Add notification column to database if needed * Make SMTP connections thread safe * Include link to comment in email notifications * Implement opt-out for email notifications * Fix faulty check for parent comment * Support notifications also for replies * Don't send notification when someone responds to his/her own comment * Make unsubscribe work with notifications for replies * Correct hash in 'unsubscribe' API example * Introduce public-endpoint setting * Fix whitespace issue * Postpone notifications to users until comment has been approved by moderator * New setting general.reply-notifications * Add client-side configuration setting reply-notifications * Documentation for reply notifications
This commit is contained in:
commit
53d5ad441c
@ -77,6 +77,11 @@ data-isso-require-email
|
||||
|
||||
Set to `true` when spam guard is configured with `require-email = true`.
|
||||
|
||||
data-isso-reply-notifications
|
||||
-------------------
|
||||
|
||||
Set to `true` when reply notifications is configured with `reply-notifications = true`.
|
||||
|
||||
data-isso-max-comments-top and data-isso-max-comments-nested
|
||||
------------------------------------------------------------
|
||||
|
||||
|
@ -88,6 +88,14 @@ notify
|
||||
Send notifications via SMTP on new comments with activation (if
|
||||
moderated) and deletion links.
|
||||
|
||||
reply-notifications
|
||||
Allow users to request E-mail notifications for replies to their post.
|
||||
|
||||
It is highly recommended to also turn on moderation when enabling this
|
||||
setting, as Isso can otherwise be easily exploited for sending spam.
|
||||
|
||||
Do not forget to configure the client accordingly.
|
||||
|
||||
log-file
|
||||
Log console messages to file instead of standard out.
|
||||
|
||||
@ -156,6 +164,12 @@ listen
|
||||
|
||||
Does not apply for `uWSGI`.
|
||||
|
||||
public-endpoint
|
||||
public URL that Isso is accessed from by end users. Should always be
|
||||
a http:// or https:// absolute address. If left blank, automatic
|
||||
detection is attempted. Normally only needs to be specified if
|
||||
different than the `listen` setting.
|
||||
|
||||
reload
|
||||
reload application, when the source code has changed. Useful for
|
||||
development. Only works with the internal webserver.
|
||||
|
@ -96,13 +96,16 @@ class Isso(object):
|
||||
super(Isso, self).__init__(conf)
|
||||
|
||||
subscribers = []
|
||||
smtp_backend = False
|
||||
for backend in conf.getlist("general", "notify"):
|
||||
if backend == "stdout":
|
||||
subscribers.append(Stdout(None))
|
||||
elif backend in ("smtp", "SMTP"):
|
||||
subscribers.append(SMTP(self))
|
||||
smtp_backend = True
|
||||
else:
|
||||
logger.warn("unknown notification backend '%s'", backend)
|
||||
if smtp_backend or conf.getboolean("general", "reply-notifications"):
|
||||
subscribers.append(SMTP(self))
|
||||
|
||||
self.signal = ext.Signal(*subscribers)
|
||||
|
||||
|
@ -239,6 +239,9 @@
|
||||
#fff 10px,
|
||||
#fff 20px
|
||||
);
|
||||
.isso-postbox > .form-wrapper > .notification-section {
|
||||
display: none;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
@media screen and (max-width:600px) {
|
||||
.isso-postbox > .form-wrapper > .auth-section .input-wrapper {
|
||||
|
@ -23,7 +23,7 @@ class Comments:
|
||||
'mode', # status of the comment 1 = valid, 2 = pending,
|
||||
# 4 = soft-deleted (cannot hard delete because of replies)
|
||||
'remote_addr', 'text', 'author', 'email', 'website',
|
||||
'likes', 'dislikes', 'voters']
|
||||
'likes', 'dislikes', 'voters', 'notification']
|
||||
|
||||
def __init__(self, db):
|
||||
|
||||
@ -33,7 +33,12 @@ class Comments:
|
||||
' tid REFERENCES threads(id), id INTEGER PRIMARY KEY, parent INTEGER,',
|
||||
' created FLOAT NOT NULL, modified FLOAT, mode INTEGER, remote_addr VARCHAR,',
|
||||
' text VARCHAR, author VARCHAR, email VARCHAR, website VARCHAR,',
|
||||
' likes INTEGER DEFAULT 0, dislikes INTEGER DEFAULT 0, voters BLOB NOT NULL);'])
|
||||
' likes INTEGER DEFAULT 0, dislikes INTEGER DEFAULT 0, voters BLOB NOT NULL,',
|
||||
' notification INTEGER NOT NULL DEFAULT 0);'])
|
||||
try:
|
||||
self.db.execute(['ALTER TABLE comments ADD COLUMN notification INTEGER NOT NULL DEFAULT 0;'])
|
||||
except:
|
||||
pass
|
||||
|
||||
def add(self, uri, c):
|
||||
"""
|
||||
@ -50,16 +55,16 @@ class Comments:
|
||||
'INSERT INTO comments (',
|
||||
' tid, parent,'
|
||||
' created, modified, mode, remote_addr,',
|
||||
' text, author, email, website, voters )',
|
||||
' text, author, email, website, voters, notification)',
|
||||
'SELECT',
|
||||
' threads.id, ?,',
|
||||
' ?, ?, ?, ?,',
|
||||
' ?, ?, ?, ?, ?',
|
||||
' ?, ?, ?, ?, ?, ?',
|
||||
'FROM threads WHERE threads.uri = ?;'], (
|
||||
c.get('parent'),
|
||||
c.get('created') or time.time(), None, c["mode"], c['remote_addr'],
|
||||
c['text'], c.get('author'), c.get('email'), c.get('website'), buffer(
|
||||
Bloomfilter(iterable=[c['remote_addr']]).array),
|
||||
Bloomfilter(iterable=[c['remote_addr']]).array), c.get('notification'),
|
||||
uri)
|
||||
)
|
||||
|
||||
@ -76,6 +81,15 @@ class Comments:
|
||||
' mode=1',
|
||||
'WHERE id=? AND mode=2'], (id, ))
|
||||
|
||||
def unsubscribe(self, email, id):
|
||||
"""
|
||||
Turn off email notifications for replies to this comment.
|
||||
"""
|
||||
self.db.execute([
|
||||
'UPDATE comments SET',
|
||||
' notification=0',
|
||||
'WHERE email=? AND (id=? OR parent=?);'], (email, id, id))
|
||||
|
||||
def update(self, id, data):
|
||||
"""
|
||||
Update comment :param:`id` with values from :param:`data` and return
|
||||
|
@ -25,6 +25,9 @@ class Threads(object):
|
||||
def __getitem__(self, uri):
|
||||
return Thread(*self.db.execute("SELECT * FROM threads WHERE uri=?", (uri, )).fetchone())
|
||||
|
||||
def get(self, id):
|
||||
return Thread(*self.db.execute("SELECT * FROM threads WHERE id=?", (id, )).fetchone())
|
||||
|
||||
def new(self, uri, title):
|
||||
self.db.execute(
|
||||
"INSERT INTO threads (uri, title) VALUES (?, ?)", (uri, title))
|
||||
|
@ -14,6 +14,11 @@ from email.utils import formatdate
|
||||
from email.header import Header
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
try:
|
||||
from urllib.parse import quote
|
||||
except ImportError:
|
||||
from urllib import quote
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger("isso")
|
||||
|
||||
@ -31,37 +36,10 @@ else:
|
||||
from _thread import start_new_thread
|
||||
|
||||
|
||||
class SMTP(object):
|
||||
class SMTPConnection(object):
|
||||
|
||||
def __init__(self, isso):
|
||||
|
||||
self.isso = isso
|
||||
self.conf = isso.conf.section("smtp")
|
||||
gh = isso.conf.get("general", "host")
|
||||
if type(gh) == str:
|
||||
self.general_host = gh
|
||||
#if gh is not a string then gh is a list
|
||||
else:
|
||||
self.general_host = gh[0]
|
||||
|
||||
# test SMTP connectivity
|
||||
try:
|
||||
with self:
|
||||
logger.info("connected to SMTP server")
|
||||
except (socket.error, smtplib.SMTPException):
|
||||
logger.exception("unable to connect to SMTP server")
|
||||
|
||||
if uwsgi:
|
||||
def spooler(args):
|
||||
try:
|
||||
self._sendmail(args[b"subject"].decode("utf-8"),
|
||||
args["body"].decode("utf-8"))
|
||||
except smtplib.SMTPConnectError:
|
||||
return uwsgi.SPOOL_RETRY
|
||||
else:
|
||||
return uwsgi.SPOOL_OK
|
||||
|
||||
uwsgi.spooler = spooler
|
||||
def __init__(self, conf):
|
||||
self.conf = conf
|
||||
|
||||
def __enter__(self):
|
||||
klass = (smtplib.SMTP_SSL if self.conf.get(
|
||||
@ -91,10 +69,40 @@ class SMTP(object):
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.client.quit()
|
||||
|
||||
def __iter__(self):
|
||||
yield "comments.new:after-save", self.notify
|
||||
class SMTP(object):
|
||||
|
||||
def format(self, thread, comment):
|
||||
def __init__(self, isso):
|
||||
|
||||
self.isso = isso
|
||||
self.conf = isso.conf.section("smtp")
|
||||
self.public_endpoint = isso.conf.get("server", "public-endpoint") or local("host")
|
||||
self.admin_notify = any((n in ("smtp", "SMTP")) for n in isso.conf.getlist("general", "notify"))
|
||||
self.reply_notify = isso.conf.getboolean("general", "reply-notifications")
|
||||
|
||||
# test SMTP connectivity
|
||||
try:
|
||||
with SMTPConnection(self.conf):
|
||||
logger.info("connected to SMTP server")
|
||||
except (socket.error, smtplib.SMTPException):
|
||||
logger.exception("unable to connect to SMTP server")
|
||||
|
||||
if uwsgi:
|
||||
def spooler(args):
|
||||
try:
|
||||
self._sendmail(args[b"subject"].decode("utf-8"),
|
||||
args["body"].decode("utf-8"))
|
||||
except smtplib.SMTPConnectError:
|
||||
return uwsgi.SPOOL_RETRY
|
||||
else:
|
||||
return uwsgi.SPOOL_OK
|
||||
|
||||
uwsgi.spooler = spooler
|
||||
|
||||
def __iter__(self):
|
||||
yield "comments.new:after-save", self.notify_new
|
||||
yield "comments.activate", self.notify_activated
|
||||
|
||||
def format(self, thread, comment, parent_comment, recipient=None, admin=False):
|
||||
|
||||
rv = io.StringIO()
|
||||
|
||||
@ -107,40 +115,74 @@ class SMTP(object):
|
||||
rv.write(comment["text"] + "\n")
|
||||
rv.write("\n")
|
||||
|
||||
if comment["website"]:
|
||||
rv.write("User's URL: %s\n" % comment["website"])
|
||||
if admin:
|
||||
if comment["website"]:
|
||||
rv.write("User's URL: %s\n" % comment["website"])
|
||||
|
||||
rv.write("IP address: %s\n" % comment["remote_addr"])
|
||||
|
||||
rv.write("IP address: %s\n" % comment["remote_addr"])
|
||||
rv.write("Link to comment: %s\n" %
|
||||
(local("origin") + thread["uri"] + "#isso-%i" % comment["id"]))
|
||||
rv.write("\n")
|
||||
|
||||
uri = self.general_host + "/id/%i" % comment["id"]
|
||||
key = self.isso.sign(comment["id"])
|
||||
|
||||
rv.write("---\n")
|
||||
rv.write("Delete comment: %s\n" % (uri + "/delete/" + key))
|
||||
|
||||
if comment["mode"] == 2:
|
||||
rv.write("Activate comment: %s\n" % (uri + "/activate/" + key))
|
||||
if admin:
|
||||
uri = self.public_endpoint + "/id/%i" % comment["id"]
|
||||
key = self.isso.sign(comment["id"])
|
||||
|
||||
rv.write("Delete comment: %s\n" % (uri + "/delete/" + key))
|
||||
|
||||
if comment["mode"] == 2:
|
||||
rv.write("Activate comment: %s\n" % (uri + "/activate/" + key))
|
||||
|
||||
else:
|
||||
uri = self.public_endpoint + "/id/%i" % parent_comment["id"]
|
||||
key = self.isso.sign(('unsubscribe', recipient))
|
||||
|
||||
rv.write("Unsubscribe from this conversation: %s\n" % (uri + "/unsubscribe/" + quote(recipient) + "/" + key))
|
||||
|
||||
rv.seek(0)
|
||||
return rv.read()
|
||||
|
||||
def notify(self, thread, comment):
|
||||
def notify_new(self, thread, comment):
|
||||
if self.admin_notify:
|
||||
body = self.format(thread, comment, None, admin=True)
|
||||
self.sendmail(thread["title"], body, thread, comment)
|
||||
|
||||
body = self.format(thread, comment)
|
||||
if comment["mode"] == 1:
|
||||
self.notify_users(thread, comment)
|
||||
|
||||
def notify_activated(self, thread, comment):
|
||||
self.notify_users(thread, comment)
|
||||
|
||||
def notify_users(self, thread, comment):
|
||||
if self.reply_notify and "parent" in comment and comment["parent"] is not None:
|
||||
# Notify interested authors that a new comment is posted
|
||||
notified = []
|
||||
parent_comment = self.isso.db.comments.get(comment["parent"])
|
||||
comments_to_notify = [parent_comment] if parent_comment is not None else []
|
||||
comments_to_notify += self.isso.db.comments.fetch(thread["uri"], mode=1, parent=comment["parent"])
|
||||
for comment_to_notify in comments_to_notify:
|
||||
email = comment_to_notify["email"]
|
||||
if "email" in comment_to_notify and comment_to_notify["notification"] and email not in notified \
|
||||
and comment_to_notify["id"] != comment["id"] and email != comment["email"]:
|
||||
body = self.format(thread, comment, parent_comment, email, admin=False)
|
||||
subject = "Re: New comment posted on %s" % thread["title"]
|
||||
self.sendmail(subject, body, thread, comment, to=email)
|
||||
notified.append(email)
|
||||
|
||||
def sendmail(self, subject, body, thread, comment, to=None):
|
||||
if uwsgi:
|
||||
uwsgi.spool({b"subject": thread["title"].encode("utf-8"),
|
||||
b"body": body.encode("utf-8")})
|
||||
uwsgi.spool({b"subject": subject.encode("utf-8"),
|
||||
b"body": body.encode("utf-8"),
|
||||
b"to": to})
|
||||
else:
|
||||
start_new_thread(self._retry, (thread["title"], body))
|
||||
start_new_thread(self._retry, (subject, body, to))
|
||||
|
||||
def _sendmail(self, subject, body):
|
||||
def _sendmail(self, subject, body, to=None):
|
||||
|
||||
from_addr = self.conf.get("from")
|
||||
to_addr = self.conf.get("to")
|
||||
to_addr = to or self.conf.get("to")
|
||||
|
||||
msg = MIMEText(body, 'plain', 'utf-8')
|
||||
msg['From'] = from_addr
|
||||
@ -148,13 +190,13 @@ class SMTP(object):
|
||||
msg['Date'] = formatdate(localtime=True)
|
||||
msg['Subject'] = Header(subject, 'utf-8')
|
||||
|
||||
with self as con:
|
||||
with SMTPConnection(self.conf) as con:
|
||||
con.sendmail(from_addr, to_addr, msg.as_string())
|
||||
|
||||
def _retry(self, subject, body):
|
||||
def _retry(self, subject, body, to):
|
||||
for x in range(5):
|
||||
try:
|
||||
self._sendmail(subject, body)
|
||||
self._sendmail(subject, body, to)
|
||||
except smtplib.SMTPConnectError:
|
||||
time.sleep(60)
|
||||
else:
|
||||
@ -187,5 +229,5 @@ class Stdout(object):
|
||||
def _delete_comment(self, id):
|
||||
logger.info('comment %i deleted', id)
|
||||
|
||||
def _activate_comment(self, id):
|
||||
logger.info("comment %s activated" % id)
|
||||
def _activate_comment(self, thread, comment):
|
||||
logger.info("comment %(id)s activated" % thread)
|
||||
|
@ -7,6 +7,7 @@ define(function() {
|
||||
"reply-to-self": false,
|
||||
"require-email": false,
|
||||
"require-author": false,
|
||||
"reply-notifications": false,
|
||||
"max-comments-top": "inf",
|
||||
"max-comments-nested": 5,
|
||||
"reveal-on-click": 5,
|
||||
|
@ -90,6 +90,8 @@ define(function() {
|
||||
this.focus = function() { node.focus() };
|
||||
this.scrollIntoView = function(args) { node.scrollIntoView(args) };
|
||||
|
||||
this.checked = function() { return node.checked; };
|
||||
|
||||
this.setAttribute = function(key, value) { node.setAttribute(key, value) };
|
||||
this.getAttribute = function(key) { return node.getAttribute(key) };
|
||||
|
||||
|
@ -6,6 +6,7 @@ define({
|
||||
"postbox-preview": "Preview",
|
||||
"postbox-edit": "Edit",
|
||||
"postbox-submit": "Submit",
|
||||
"postbox-notification": "Subscribe to email notification of replies",
|
||||
|
||||
"num-comments": "One Comment\n{{ n }} Comments",
|
||||
"no-comments": "No Comments Yet",
|
||||
|
@ -6,6 +6,7 @@ define({
|
||||
"postbox-preview": "Aperçu",
|
||||
"postbox-edit": "Éditer",
|
||||
"postbox-submit": "Soumettre",
|
||||
"postbox-notification": "S'abonner aux notifications de réponses",
|
||||
"num-comments": "{{ n }} commentaire\n{{ n }} commentaires",
|
||||
"no-comments": "Aucun commentaire pour l'instant",
|
||||
"atom-feed": "Flux Atom",
|
||||
|
@ -40,6 +40,17 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n",
|
||||
return true;
|
||||
};
|
||||
|
||||
// only display notification checkbox if email is filled in
|
||||
var email_edit = function() {
|
||||
if (config["reply-notifications"] && $("[name='email']", el).value.length > 0) {
|
||||
$(".notification-section", el).show();
|
||||
} else {
|
||||
$(".notification-section", el).hide();
|
||||
}
|
||||
};
|
||||
$("[name='email']", el).on("input", email_edit);
|
||||
email_edit();
|
||||
|
||||
// email is not optional if this config parameter is set
|
||||
if (config["require-email"]) {
|
||||
$("[name='email']", el).setAttribute("placeholder",
|
||||
@ -89,7 +100,8 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n",
|
||||
author: author, email: email, website: website,
|
||||
text: utils.text($(".textarea", el).innerHTML),
|
||||
parent: parent || null,
|
||||
title: $("#isso-thread").getAttribute("data-title") || null
|
||||
title: $("#isso-thread").getAttribute("data-title") || null,
|
||||
notification: $("[name=notification]", el).checked() ? 1 : 0,
|
||||
}).then(function(comment) {
|
||||
$(".textarea", el).innerHTML = "";
|
||||
$(".textarea", el).blur();
|
||||
|
@ -25,3 +25,7 @@ div(class='isso-postbox')
|
||||
p(class='post-action')
|
||||
input(type='button' name='edit'
|
||||
value=i18n('postbox-edit'))
|
||||
section(class='notification-section')
|
||||
label
|
||||
input(type='checkbox' name='notification')
|
||||
= i18n('postbox-notification')
|
||||
|
@ -32,6 +32,10 @@ try:
|
||||
from urlparse import urlparse
|
||||
except ImportError:
|
||||
from urllib.parse import urlparse
|
||||
try:
|
||||
from urllib import unquote
|
||||
except ImportError:
|
||||
from urllib.parse import unquote
|
||||
try:
|
||||
from StringIO import StringIO
|
||||
except ImportError:
|
||||
@ -93,28 +97,29 @@ def xhr(func):
|
||||
class API(object):
|
||||
|
||||
FIELDS = set(['id', 'parent', 'text', 'author', 'website',
|
||||
'mode', 'created', 'modified', 'likes', 'dislikes', 'hash', 'gravatar_image'])
|
||||
'mode', 'created', 'modified', 'likes', 'dislikes', 'hash', 'gravatar_image', 'notification'])
|
||||
|
||||
# comment fields, that can be submitted
|
||||
ACCEPT = set(['text', 'author', 'website', 'email', 'parent', 'title'])
|
||||
ACCEPT = set(['text', 'author', 'website', 'email', 'parent', 'title', 'notification'])
|
||||
|
||||
VIEWS = [
|
||||
('fetch', ('GET', '/')),
|
||||
('new', ('POST', '/new')),
|
||||
('count', ('GET', '/count')),
|
||||
('counts', ('POST', '/count')),
|
||||
('feed', ('GET', '/feed')),
|
||||
('view', ('GET', '/id/<int:id>')),
|
||||
('edit', ('PUT', '/id/<int:id>')),
|
||||
('delete', ('DELETE', '/id/<int:id>')),
|
||||
('moderate', ('GET', '/id/<int:id>/<any(edit,activate,delete):action>/<string:key>')),
|
||||
('moderate', ('POST', '/id/<int:id>/<any(edit,activate,delete):action>/<string:key>')),
|
||||
('like', ('POST', '/id/<int:id>/like')),
|
||||
('dislike', ('POST', '/id/<int:id>/dislike')),
|
||||
('demo', ('GET', '/demo')),
|
||||
('preview', ('POST', '/preview')),
|
||||
('login', ('POST', '/login')),
|
||||
('admin', ('GET', '/admin'))
|
||||
('fetch', ('GET', '/')),
|
||||
('new', ('POST', '/new')),
|
||||
('count', ('GET', '/count')),
|
||||
('counts', ('POST', '/count')),
|
||||
('feed', ('GET', '/feed')),
|
||||
('view', ('GET', '/id/<int:id>')),
|
||||
('edit', ('PUT', '/id/<int:id>')),
|
||||
('delete', ('DELETE', '/id/<int:id>')),
|
||||
('unsubscribe', ('GET', '/id/<int:id>/unsubscribe/<string:email>/<string:key>')),
|
||||
('moderate', ('GET', '/id/<int:id>/<any(edit,activate,delete):action>/<string:key>')),
|
||||
('moderate', ('POST', '/id/<int:id>/<any(edit,activate,delete):action>/<string:key>')),
|
||||
('like', ('POST', '/id/<int:id>/like')),
|
||||
('dislike', ('POST', '/id/<int:id>/dislike')),
|
||||
('demo', ('GET', '/demo')),
|
||||
('preview', ('POST', '/preview')),
|
||||
('login', ('POST', '/login')),
|
||||
('admin', ('GET', '/admin'))
|
||||
]
|
||||
|
||||
def __init__(self, isso, hasher):
|
||||
@ -492,6 +497,70 @@ class API(object):
|
||||
resp.headers.add("X-Set-Cookie", cookie("isso-%i" % id))
|
||||
return resp
|
||||
|
||||
"""
|
||||
@api {get} /id/:id/:email/key unsubscribe
|
||||
@apiGroup Comment
|
||||
@apiDescription
|
||||
Opt out from getting any further email notifications about replies to a particular comment. In order to use this endpoint, the requestor needs a `key` that is usually obtained from an email sent out by isso.
|
||||
|
||||
@apiParam {number} id
|
||||
The id of the comment to unsubscribe from replies to.
|
||||
@apiParam {string} email
|
||||
The email address of the subscriber.
|
||||
@apiParam {string} key
|
||||
The key to authenticate the subscriber.
|
||||
|
||||
@apiExample {curl} Unsubscribe Alice from replies to comment with id 13:
|
||||
curl -X GET 'https://comments.example.com/id/13/unsubscribe/alice@example.com/WyJ1bnN1YnNjcmliZSIsImFsaWNlQGV4YW1wbGUuY29tIl0.DdcH9w.Wxou-l22ySLFkKUs7RUHnoM8Kos'
|
||||
|
||||
@apiSuccessExample {html} Using GET:
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script>
|
||||
if (confirm('Delete: Are you sure?')) {
|
||||
xhr = new XMLHttpRequest;
|
||||
xhr.open('POST', window.location.href);
|
||||
xhr.send(null);
|
||||
}
|
||||
</script>
|
||||
|
||||
@apiSuccessExample Using POST:
|
||||
Yo
|
||||
"""
|
||||
|
||||
def unsubscribe(self, environ, request, id, email, key):
|
||||
email = unquote(email)
|
||||
|
||||
try:
|
||||
rv = self.isso.unsign(key, max_age=2**32)
|
||||
except (BadSignature, SignatureExpired):
|
||||
raise Forbidden
|
||||
|
||||
if rv[0] != 'unsubscribe' or rv[1] != email:
|
||||
raise Forbidden
|
||||
|
||||
item = self.comments.get(id)
|
||||
|
||||
if item is None:
|
||||
raise NotFound
|
||||
|
||||
with self.isso.lock:
|
||||
self.comments.unsubscribe(email, id)
|
||||
|
||||
modal = (
|
||||
"<!DOCTYPE html>"
|
||||
"<html>"
|
||||
"<head>"
|
||||
" <title>Successfully unsubscribed</title>"
|
||||
"</head>"
|
||||
"<body>"
|
||||
" <p>You have been unsubscribed from replies in the given conversation.</p>"
|
||||
"</body>"
|
||||
"</html>")
|
||||
|
||||
return Response(modal, 200, content_type="text/html")
|
||||
|
||||
"""
|
||||
@api {post} /id/:id/:action/key moderate
|
||||
@apiGroup Comment
|
||||
@ -554,9 +623,12 @@ class API(object):
|
||||
return Response(modal, 200, content_type="text/html")
|
||||
|
||||
if action == "activate":
|
||||
if item['mode'] == 1:
|
||||
return Response("Already activated", 200)
|
||||
with self.isso.lock:
|
||||
self.comments.activate(id)
|
||||
self.signal("comments.activate", id)
|
||||
thread = self.threads.get(item['tid'])
|
||||
self.signal("comments.activate", thread, item)
|
||||
return Response("Yo", 200)
|
||||
elif action == "edit":
|
||||
data = request.get_json()
|
||||
|
@ -9,6 +9,7 @@ dbpath = /var/isso/comments.db
|
||||
host = http://isso-dev.local/
|
||||
max-age = 15m
|
||||
notify = stdout
|
||||
reply-notifications = false
|
||||
log-file = /var/log/isso.log
|
||||
admin_password = strong_default_password_for_isso_admin
|
||||
|
||||
|
@ -43,6 +43,11 @@ max-age = 15m
|
||||
# moderated) and deletion links.
|
||||
notify = stdout
|
||||
|
||||
# Allow users to request E-mail notifications for replies to their post.
|
||||
# WARNING: It is highly recommended to also turn on moderation when enabling
|
||||
# this setting, as Isso can otherwise be easily exploited for sending spam.
|
||||
reply-notifications=false
|
||||
|
||||
# Log console messages to file instead of standard output.
|
||||
log-file =
|
||||
|
||||
@ -78,6 +83,11 @@ purge-after = 30d
|
||||
# for details). Does not apply for uWSGI.
|
||||
listen = http://localhost:8080
|
||||
|
||||
# public URL that Isso is accessed from by end users. Should always be a
|
||||
# http:// or https:// absolute address. If left blank, automatic detection is
|
||||
# attempted.
|
||||
public-endpoint =
|
||||
|
||||
# reload application, when the source code has changed. Useful for development.
|
||||
# Only works with the internal webserver.
|
||||
reload = off
|
||||
|
Loading…
Reference in New Issue
Block a user