2013-09-19 16:30:46 +00:00
|
|
|
# -*- encoding: utf-8 -*-
|
|
|
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
from isso.utils import Bloomfilter
|
2013-10-09 14:28:54 +00:00
|
|
|
from isso.compat import buffer
|
2013-09-19 16:30:46 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Comments:
|
|
|
|
"""Hopefully DB-independend SQL to store, modify and retrieve all
|
|
|
|
comment-related actions. Here's a short scheme overview:
|
|
|
|
|
2013-12-17 12:30:37 +00:00
|
|
|
| tid (thread id) | id (comment id) | parent | ... | voters | remote_addr |
|
|
|
|
+-----------------+-----------------+--------+-----+--------+-------------+
|
|
|
|
| 1 | 1 | null | ... | BLOB | 127.0.0.0 |
|
|
|
|
| 1 | 2 | 1 | ... | BLOB | 127.0.0.0 |
|
|
|
|
+-----------------+-----------------+--------+-----+--------+-------------+
|
2013-09-19 16:30:46 +00:00
|
|
|
|
2013-12-17 12:30:37 +00:00
|
|
|
The tuple (tid, id) is unique and thus primary key.
|
2013-09-19 16:30:46 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
fields = ['tid', 'id', 'parent', 'created', 'modified', 'mode', 'remote_addr',
|
|
|
|
'text', 'author', 'email', 'website', 'likes', 'dislikes', 'voters']
|
|
|
|
|
|
|
|
def __init__(self, db):
|
|
|
|
|
|
|
|
self.db = db
|
|
|
|
self.db.execute([
|
|
|
|
'CREATE TABLE IF NOT EXISTS 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);'])
|
|
|
|
|
|
|
|
def add(self, uri, c):
|
|
|
|
"""
|
2013-12-17 12:30:37 +00:00
|
|
|
Add new comment to DB and return a mapping of :attribute:`fields` and
|
|
|
|
database values.
|
2013-09-19 16:30:46 +00:00
|
|
|
"""
|
2014-04-20 16:00:28 +00:00
|
|
|
|
2014-04-21 08:23:45 +00:00
|
|
|
if c.get("parent") is not None:
|
|
|
|
ref = self.get(c["parent"])
|
|
|
|
if ref.get("parent") is not None:
|
|
|
|
c["parent"] = ref["parent"]
|
2014-04-20 16:00:28 +00:00
|
|
|
|
2013-09-19 16:30:46 +00:00
|
|
|
self.db.execute([
|
|
|
|
'INSERT INTO comments (',
|
|
|
|
' tid, parent,'
|
|
|
|
' created, modified, mode, remote_addr,',
|
|
|
|
' text, author, email, website, voters )',
|
|
|
|
'SELECT',
|
|
|
|
' threads.id, ?,',
|
|
|
|
' ?, ?, ?, ?,',
|
|
|
|
' ?, ?, ?, ?, ?',
|
|
|
|
'FROM threads WHERE threads.uri = ?;'], (
|
|
|
|
c.get('parent'),
|
2013-10-13 13:00:54 +00:00
|
|
|
c.get('created') or time.time(), None, c["mode"], c['remote_addr'],
|
2013-09-19 16:30:46 +00:00
|
|
|
c['text'], c.get('author'), c.get('email'), c.get('website'), buffer(
|
|
|
|
Bloomfilter(iterable=[c['remote_addr']]).array),
|
|
|
|
uri)
|
|
|
|
)
|
|
|
|
|
|
|
|
return dict(zip(Comments.fields, self.db.execute(
|
|
|
|
'SELECT *, MAX(c.id) FROM comments AS c INNER JOIN threads ON threads.uri = ?',
|
|
|
|
(uri, )).fetchone()))
|
|
|
|
|
2013-10-13 13:00:54 +00:00
|
|
|
def activate(self, id):
|
2013-12-17 12:30:37 +00:00
|
|
|
"""
|
|
|
|
Activate comment id if pending.
|
|
|
|
"""
|
2013-10-13 13:00:54 +00:00
|
|
|
self.db.execute([
|
|
|
|
'UPDATE comments SET',
|
|
|
|
' mode=1',
|
|
|
|
'WHERE id=? AND mode=2'], (id, ))
|
2013-09-19 16:30:46 +00:00
|
|
|
|
|
|
|
def update(self, id, data):
|
|
|
|
"""
|
2013-12-17 12:30:37 +00:00
|
|
|
Update comment :param:`id` with values from :param:`data` and return
|
|
|
|
updated comment.
|
2013-09-19 16:30:46 +00:00
|
|
|
"""
|
|
|
|
self.db.execute([
|
|
|
|
'UPDATE comments SET',
|
|
|
|
','.join(key + '=' + '?' for key in data),
|
|
|
|
'WHERE id=?;'],
|
2013-10-09 14:28:54 +00:00
|
|
|
list(data.values()) + [id])
|
2013-09-19 16:30:46 +00:00
|
|
|
|
|
|
|
return self.get(id)
|
|
|
|
|
|
|
|
def get(self, id):
|
2013-12-17 12:30:37 +00:00
|
|
|
"""
|
|
|
|
Search for comment :param:`id` and return a mapping of :attr:`fields`
|
|
|
|
and values.
|
|
|
|
"""
|
2013-09-19 16:30:46 +00:00
|
|
|
rv = self.db.execute('SELECT * FROM comments WHERE id=?', (id, )).fetchone()
|
|
|
|
if rv:
|
|
|
|
return dict(zip(Comments.fields, rv))
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
2014-04-20 13:16:43 +00:00
|
|
|
def fetch(self, uri, mode=5, after=0, parent='any', order_by='id', limit=None):
|
2013-09-19 16:30:46 +00:00
|
|
|
"""
|
2013-12-17 12:30:37 +00:00
|
|
|
Return comments for :param:`uri` with :param:`mode`.
|
2013-09-19 16:30:46 +00:00
|
|
|
"""
|
2014-04-20 13:16:43 +00:00
|
|
|
sql = [ 'SELECT comments.* FROM comments INNER JOIN threads ON',
|
|
|
|
' threads.uri=? AND comments.tid=threads.id AND (? | comments.mode) = ?',
|
|
|
|
' AND comments.created>?']
|
2013-09-19 16:30:46 +00:00
|
|
|
|
2014-04-20 13:16:43 +00:00
|
|
|
sql_args = [uri, mode, mode, after]
|
|
|
|
|
|
|
|
if parent != 'any':
|
2014-05-03 08:47:48 +00:00
|
|
|
if parent is None:
|
2014-04-20 13:16:43 +00:00
|
|
|
sql.append('AND comments.parent IS NULL')
|
|
|
|
else:
|
|
|
|
sql.append('AND comments.parent=?')
|
|
|
|
sql_args.append(parent)
|
|
|
|
|
2014-11-13 12:56:40 +00:00
|
|
|
# custom sanitization
|
|
|
|
if order_by not in ['id', 'created', 'modified', 'likes', 'dislikes']:
|
|
|
|
order_by = 'id'
|
|
|
|
sql.append('ORDER BY ')
|
|
|
|
sql.append(order_by)
|
|
|
|
sql.append(' ASC')
|
2014-04-20 13:16:43 +00:00
|
|
|
|
2014-05-03 08:47:48 +00:00
|
|
|
if limit:
|
2014-04-20 13:16:43 +00:00
|
|
|
sql.append('LIMIT ?')
|
|
|
|
sql_args.append(limit)
|
|
|
|
|
|
|
|
rv = self.db.execute(sql, sql_args).fetchall()
|
2013-09-19 16:30:46 +00:00
|
|
|
for item in rv:
|
|
|
|
yield dict(zip(Comments.fields, item))
|
|
|
|
|
|
|
|
def _remove_stale(self):
|
|
|
|
|
|
|
|
sql = ('DELETE FROM',
|
|
|
|
' comments',
|
|
|
|
'WHERE',
|
|
|
|
' mode=4 AND id NOT IN (',
|
|
|
|
' SELECT',
|
|
|
|
' parent',
|
|
|
|
' FROM',
|
|
|
|
' comments',
|
|
|
|
' WHERE parent IS NOT NULL)')
|
|
|
|
|
|
|
|
while self.db.execute(sql).rowcount:
|
|
|
|
continue
|
|
|
|
|
|
|
|
def delete(self, id):
|
|
|
|
"""
|
|
|
|
Delete a comment. There are two distinctions: a comment is referenced
|
|
|
|
by another valid comment's parent attribute or stand-a-lone. In this
|
|
|
|
case the comment can't be removed without losing depending comments.
|
|
|
|
Hence, delete removes all visible data such as text, author, email,
|
|
|
|
website sets the mode field to 4.
|
|
|
|
|
|
|
|
In the second case this comment can be safely removed without any side
|
|
|
|
effects."""
|
|
|
|
|
|
|
|
refs = self.db.execute('SELECT * FROM comments WHERE parent=?', (id, )).fetchone()
|
|
|
|
|
|
|
|
if refs is None:
|
|
|
|
self.db.execute('DELETE FROM comments WHERE id=?', (id, ))
|
|
|
|
self._remove_stale()
|
|
|
|
return None
|
|
|
|
|
|
|
|
self.db.execute('UPDATE comments SET text=? WHERE id=?', ('', id))
|
|
|
|
self.db.execute('UPDATE comments SET mode=? WHERE id=?', (4, id))
|
|
|
|
for field in ('author', 'website'):
|
|
|
|
self.db.execute('UPDATE comments SET %s=? WHERE id=?' % field, (None, id))
|
|
|
|
|
|
|
|
self._remove_stale()
|
|
|
|
return self.get(id)
|
|
|
|
|
2013-10-01 12:42:00 +00:00
|
|
|
def vote(self, upvote, id, remote_addr):
|
2013-09-19 16:30:46 +00:00
|
|
|
"""+1 a given comment. Returns the new like count (may not change because
|
|
|
|
the creater can't vote on his/her own comment and multiple votes from the
|
|
|
|
same ip address are ignored as well)."""
|
|
|
|
|
|
|
|
rv = self.db.execute(
|
|
|
|
'SELECT likes, dislikes, voters FROM comments WHERE id=?', (id, )) \
|
|
|
|
.fetchone()
|
|
|
|
|
|
|
|
if rv is None:
|
2013-10-01 12:42:00 +00:00
|
|
|
return None
|
2013-09-19 16:30:46 +00:00
|
|
|
|
|
|
|
likes, dislikes, voters = rv
|
|
|
|
if likes + dislikes >= 142:
|
2013-10-01 12:42:00 +00:00
|
|
|
return {'likes': likes, 'dislikes': dislikes}
|
2013-09-19 16:30:46 +00:00
|
|
|
|
|
|
|
bf = Bloomfilter(bytearray(voters), likes + dislikes)
|
|
|
|
if remote_addr in bf:
|
2013-10-01 12:42:00 +00:00
|
|
|
return {'likes': likes, 'dislikes': dislikes}
|
2013-09-19 16:30:46 +00:00
|
|
|
|
|
|
|
bf.add(remote_addr)
|
|
|
|
self.db.execute([
|
|
|
|
'UPDATE comments SET',
|
2013-10-01 12:42:00 +00:00
|
|
|
' likes = likes + 1,' if upvote else 'dislikes = dislikes + 1,',
|
|
|
|
' voters = ?'
|
2013-09-19 16:30:46 +00:00
|
|
|
'WHERE id=?;'], (buffer(bf.array), id))
|
|
|
|
|
2013-10-01 12:42:00 +00:00
|
|
|
if upvote:
|
|
|
|
return {'likes': likes + 1, 'dislikes': dislikes}
|
|
|
|
return {'likes': likes, 'dislikes': dislikes + 1}
|
2013-09-19 16:30:46 +00:00
|
|
|
|
2014-05-27 14:10:15 +00:00
|
|
|
def reply_count(self, url, mode=5, after=0):
|
2014-04-20 13:16:43 +00:00
|
|
|
"""
|
|
|
|
Return comment count for main thread and all reply threads for one url.
|
|
|
|
"""
|
|
|
|
|
2014-05-27 14:10:15 +00:00
|
|
|
sql = ['SELECT comments.parent,count(*)',
|
|
|
|
'FROM comments INNER JOIN threads ON',
|
|
|
|
' threads.uri=? AND comments.tid=threads.id AND',
|
|
|
|
' (? | comments.mode = ?) AND',
|
|
|
|
' comments.created > ?',
|
|
|
|
'GROUP BY comments.parent']
|
2014-04-20 13:16:43 +00:00
|
|
|
|
2014-05-27 14:10:15 +00:00
|
|
|
return dict(self.db.execute(sql, [url, mode, mode, after]).fetchall())
|
2014-04-20 13:16:43 +00:00
|
|
|
|
2014-03-25 17:38:17 +00:00
|
|
|
def count(self, *urls):
|
2013-09-19 16:30:46 +00:00
|
|
|
"""
|
2014-03-25 17:38:17 +00:00
|
|
|
Return comment count for one ore more urls..
|
2013-09-19 16:30:46 +00:00
|
|
|
"""
|
2014-03-25 17:38:17 +00:00
|
|
|
|
|
|
|
threads = dict(self.db.execute([
|
|
|
|
'SELECT threads.uri, COUNT(comments.id) FROM comments',
|
2014-03-25 18:01:07 +00:00
|
|
|
'LEFT OUTER JOIN threads ON threads.id = tid AND comments.mode = 1',
|
2014-03-25 17:38:17 +00:00
|
|
|
'GROUP BY threads.uri'
|
|
|
|
]).fetchall())
|
|
|
|
|
|
|
|
return [threads.get(url, 0) for url in urls]
|
2013-10-24 12:16:14 +00:00
|
|
|
|
|
|
|
def purge(self, delta):
|
2013-12-17 12:30:37 +00:00
|
|
|
"""
|
|
|
|
Remove comments older than :param:`delta`.
|
|
|
|
"""
|
2013-10-24 12:16:14 +00:00
|
|
|
self.db.execute([
|
|
|
|
'DELETE FROM comments WHERE mode = 2 AND ? - created > ?;'
|
|
|
|
], (time.time(), delta))
|
|
|
|
self._remove_stale()
|