Implement opt-out for email notifications

This commit is contained in:
Pelle Nilsson 2018-02-21 21:39:35 +01:00
parent bc4bc55025
commit c9045f5b1f
3 changed files with 95 additions and 19 deletions

View File

@ -81,6 +81,15 @@ class Comments:
' mode=1', ' mode=1',
'WHERE id=? AND mode=2'], (id, )) 'WHERE id=? AND mode=2'], (id, ))
def unsubscribe(self, id):
"""
Turn off email notifications for replies to this comment.
"""
self.db.execute([
'UPDATE comments SET',
' notification=0',
'WHERE id=?'], (id, ))
def update(self, id, data): def update(self, id, data):
""" """
Update comment :param:`id` with values from :param:`data` and return Update comment :param:`id` with values from :param:`data` and return

View File

@ -93,7 +93,7 @@ class SMTP(object):
def __iter__(self): def __iter__(self):
yield "comments.new:after-save", self.notify yield "comments.new:after-save", self.notify
def format(self, thread, comment, admin=False): def format(self, thread, comment, comment_parent, admin=False):
rv = io.StringIO() rv = io.StringIO()
@ -115,17 +115,23 @@ class SMTP(object):
rv.write("Link to comment: %s\n" % rv.write("Link to comment: %s\n" %
(local("origin") + thread["uri"] + "#isso-%i" % comment["id"])) (local("origin") + thread["uri"] + "#isso-%i" % comment["id"]))
rv.write("\n") rv.write("\n")
rv.write("---\n")
if admin: if admin:
uri = local("host") + "/id/%i" % comment["id"] uri = local("host") + "/id/%i" % comment["id"]
key = self.isso.sign(comment["id"]) key = self.isso.sign(comment["id"])
rv.write("---\n")
rv.write("Delete comment: %s\n" % (uri + "/delete/" + key)) rv.write("Delete comment: %s\n" % (uri + "/delete/" + key))
if comment["mode"] == 2: if comment["mode"] == 2:
rv.write("Activate comment: %s\n" % (uri + "/activate/" + key)) rv.write("Activate comment: %s\n" % (uri + "/activate/" + key))
else:
uri = local("host") + "/id/%i" % comment_parent["id"]
key = self.isso.sign(('unsubscribe', comment_parent["id"]))
rv.write("Unsubscribe from this conversation: %s\n" % (uri + "/unsubscribe/" + key))
rv.seek(0) rv.seek(0)
return rv.read() return rv.read()
@ -134,11 +140,11 @@ class SMTP(object):
comment_parent = self.isso.db.comments.get(comment["parent"]) comment_parent = self.isso.db.comments.get(comment["parent"])
# Notify the author that a new comment is posted if requested # Notify the author that a new comment is posted if requested
if comment_parent and "email" in comment_parent and comment_parent["notification"]: if comment_parent and "email" in comment_parent and comment_parent["notification"]:
body = self.format(thread, comment, admin=False) body = self.format(thread, comment, comment_parent, admin=False)
subject = "Re: New comment posted on %s" % thread["title"] subject = "Re: New comment posted on %s" % thread["title"]
self.sendmail(subject, body, thread, comment, to=comment_parent["email"]) self.sendmail(subject, body, thread, comment, to=comment_parent["email"])
body = self.format(thread, comment, admin=True) body = self.format(thread, comment, None, admin=True)
self.sendmail(thread["title"], body, thread, comment) self.sendmail(thread["title"], body, thread, comment)
def sendmail(self, subject, body, thread, comment, to=None): def sendmail(self, subject, body, thread, comment, to=None):

View File

@ -87,21 +87,22 @@ class API(object):
ACCEPT = set(['text', 'author', 'website', 'email', 'parent', 'title', 'notification']) ACCEPT = set(['text', 'author', 'website', 'email', 'parent', 'title', 'notification'])
VIEWS = [ VIEWS = [
('fetch', ('GET', '/')), ('fetch', ('GET', '/')),
('new', ('POST', '/new')), ('new', ('POST', '/new')),
('count', ('GET', '/count')), ('count', ('GET', '/count')),
('counts', ('POST', '/count')), ('counts', ('POST', '/count')),
('view', ('GET', '/id/<int:id>')), ('view', ('GET', '/id/<int:id>')),
('edit', ('PUT', '/id/<int:id>')), ('edit', ('PUT', '/id/<int:id>')),
('delete', ('DELETE', '/id/<int:id>')), ('delete', ('DELETE', '/id/<int:id>')),
('moderate', ('GET', '/id/<int:id>/<any(edit,activate,delete):action>/<string:key>')), ('unsubscribe', ('GET', '/id/<int:id>/unsubscribe/<string:key>')),
('moderate', ('POST', '/id/<int:id>/<any(edit,activate,delete):action>/<string:key>')), ('moderate', ('GET', '/id/<int:id>/<any(edit,activate,delete):action>/<string:key>')),
('like', ('POST', '/id/<int:id>/like')), ('moderate', ('POST', '/id/<int:id>/<any(edit,activate,delete):action>/<string:key>')),
('dislike', ('POST', '/id/<int:id>/dislike')), ('like', ('POST', '/id/<int:id>/like')),
('demo', ('GET', '/demo')), ('dislike', ('POST', '/id/<int:id>/dislike')),
('preview', ('POST', '/preview')), ('demo', ('GET', '/demo')),
('login', ('POST', '/login')), ('preview', ('POST', '/preview')),
('admin', ('GET', '/admin')) ('login', ('POST', '/login')),
('admin', ('GET', '/admin'))
] ]
def __init__(self, isso, hasher): def __init__(self, isso, hasher):
@ -477,6 +478,66 @@ class API(object):
resp.headers.add("X-Set-Cookie", cookie("isso-%i" % id)) resp.headers.add("X-Set-Cookie", cookie("isso-%i" % id))
return resp return resp
"""
@api {get} /id/:id/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} key
The key to authenticate the subscriber.
@apiExample {curl} Unsubscribe from replies to comment with id 13:
curl -X GET 'https://comments.example.com/id/13/unsubscribe/TODO-COMPUTE-HASH'
@apiSuccessExample {html} Using GET:
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;script&gt;
if (confirm('Delete: Are you sure?')) {
xhr = new XMLHttpRequest;
xhr.open('POST', window.location.href);
xhr.send(null);
}
&lt;/script&gt;
@apiSuccessExample Using POST:
Yo
"""
def unsubscribe(self, environ, request, id, key):
try:
rv = self.isso.unsign(key, max_age=2**32)
except (BadSignature, SignatureExpired):
raise Forbidden
if rv[0] != 'unsubscribe' or rv[1] != id:
raise Forbidden
item = self.comments.get(id)
if item is None:
raise NotFound
with self.isso.lock:
self.comments.unsubscribe(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 @api {post} /id/:id/:action/key moderate
@apiGroup Comment @apiGroup Comment