You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
isso/isso/db/comments.py

180 lines
6.4 KiB

# -*- encoding: utf-8 -*-
import time
import sqlite3
from isso.utils import Bloomfilter
class Comments:
"""Hopefully DB-independend SQL to store, modify and retrieve all
comment-related actions. Here's a short scheme overview:
| tid (thread id) | cid (comment id) | parent | ... | likes | remote_addr |
+-----------------+------------------+--------+-----+-------+-------------+
| 1 | 1 | null | ... | BLOB | 127.0.0.0 |
| 1 | 2 | 1 | ... | BLOB | 127.0.0.0 |
+-----------------+------------------+--------+-----+-------+-------------+
The tuple (tid, cid) is unique and thus primary key.
"""
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):
"""
Add a new comment to the database and return public fields as dict.
Initializes voter bloom array with provided :param:`remote_addr` and
adds a new thread to the `main.threads` table.
"""
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'),
c.get('created') or time.time(), None, self.db.mode, c['remote_addr'],
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()))
# def activate(self, path, id):
# """Activate comment id if pending and return comment for (path, id)."""
# with sqlite3.connect(self.dbpath) as con:
# con.execute("UPDATE comments SET mode=1 WHERE path=? AND id=? AND mode=2", (path, id))
# return self.get(path, id)
def update(self, id, data):
"""
Update an existing comment, but only writeable fields such as text,
author, email, website and parent. This method should set the modified
field to the current time.
"""
self.db.execute([
'UPDATE comments SET',
','.join(key + '=' + '?' for key in data),
'WHERE id=?;'],
data.values() + [id])
return self.get(id)
def get(self, id):
rv = self.db.execute('SELECT * FROM comments WHERE id=?', (id, )).fetchone()
if rv:
return dict(zip(Comments.fields, rv))
return None
def fetch(self, uri, mode=5):
"""
Return all comments for `path` with `mode`.
"""
rv = self.db.execute([
'SELECT comments.* FROM comments INNER JOIN threads ON',
' threads.uri=? AND comments.tid=threads.id AND (? | comments.mode) = ?'
'ORDER BY id ASC;'], (uri, mode, mode)).fetchall()
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)
def like(self, id, remote_addr):
"""+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:
return 0
likes, dislikes, voters = rv
if likes + dislikes >= 142:
return likes
bf = Bloomfilter(bytearray(voters), likes + dislikes)
if remote_addr in bf:
return likes
bf.add(remote_addr)
self.db.execute([
'UPDATE comments SET',
' likes = likes + 1, voters = ?',
'WHERE id=?;'], (buffer(bf.array), id))
return likes + 1
def count(self, uri):
"""
return count of comments for uri.
"""
return self.db.execute([
'SELECT COUNT(comments.id) FROM comments INNER JOIN threads ON',
' threads.uri=? AND comments.tid=threads.id AND comments.mode=1;'],
(uri, )).fetchone()