parent comment, child comment, add SQLite adapter and banner

This commit is contained in:
BennyThink
2021-06-17 12:37:37 +08:00
parent 2e594507e5
commit c8c0e5e1e8
11 changed files with 351 additions and 43 deletions

1
.gitignore vendored
View File

@@ -124,3 +124,4 @@ certs/*
data/*
logs/*
**/.DS_Store
/yyetsweb/yyets.sqlite

157
API.md
View File

@@ -210,6 +210,60 @@
# 评论
评论的基本数据格式: `children` 字段为 array/list可套娃另外一条评论目前暂时只支持两层也不打算支持更多的啦
评论的 `resource_id` 必须相同
## 普通评论
```json
{
"username": "Benny",
"date": "2021-06-17 10:54:19",
"browser": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.13; rv:85.1) Gecko/20100101 Firefox/85.1",
"content": "test",
"resource_id": 233,
"id": "60cab95baa7f515ea291392b",
"children": [
],
"children_count": 0
}
```
## 嵌套评论
```json
{
"username": "Benny",
"date": "2021-06-17 10:54:19",
"browser": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.13; rv:85.1) Gecko/20100101 Firefox/85.1",
"content": "test",
"resource_id": 233,
"id": "60cab95baa7f515ea291392b",
"children": [
{
"username": "Alex",
"date": "2021-05-31 16:58:21",
"browser": "PostmanRuntime/7.28.0",
"content": "评论17",
"id": "60c838a12a5620b7e4ba5dfc",
"resource_id": 233
},
{
"username": "Paul",
"date": "2021-05-22 16:58:21",
"browser": "PostmanRuntime/7.28.0",
"content": "评论14",
"id": "60c838a12a5620b7e4ba1111",
"resource_id": 233
}
],
"children_count": 2
}
```
## 获取评论
* GET `/api/comment`
@@ -217,31 +271,79 @@
分页支持URL参数
* resource_id: 资源idid为233是留言板id为-1会返回最新评论
* size: 每页评论数量默认5(或者其他数值)
* page: 当前页
* size: 每页评论数量默认5
* page: 当前页默认1
* inner_size: 内嵌评论数量默认5
* inner_page: 内嵌评论当前页默认1
返回
普通评论
```json
{
"data": [
{
"date": "2018-09-18 11:12:15",
"username": "uuua2",
"content": "tdaadd",
"id": 2
"username": "Benny",
"date": "2021-06-17 10:54:19",
"browser": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.13; rv:85.1) Gecko/20100101 Firefox/85.1",
"content": "test",
"resource_id": 233,
"id": "60cab95baa7f515ea291392b",
"children": [],
"children_count": 0
}
],
"count": 1,
"resource_id": 233
}
```
楼中楼
```json
{
"data": [
{
"username": "Benny",
"date": "2021-06-17 10:54:19",
"browser": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.13; rv:85.1) Gecko/20100101 Firefox/85.1",
"content": "test",
"resource_id": 233,
"id": "60cab95baa7f515ea291392b"
},
{
"date": "2018-09-01 11:12:15",
"username": "abcd",
"content": "tdaadd",
"id": 1
"username": "Benny",
"date": "2021-06-15 10:54:19",
"browser": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.13; rv:85.1) Gecko/20100101 Firefox/85.1",
"content": "test8888",
"resource_id": 233,
"id": "60cab95baa7f515ea2988888",
"children": [
{
"username": "Alex",
"date": "2021-05-31 16:58:21",
"browser": "PostmanRuntime/7.28.0",
"content": "评论17",
"id": "60c838a12a5620b7e4ba5dfc",
"resource_id": 233
},
{
"username": "Paul",
"date": "2021-05-22 16:58:21",
"browser": "PostmanRuntime/7.28.0",
"content": "评论14",
"id": "60c838a12a5620b7e4ba1111",
"resource_id": 233
}
],
"children_count": 2
}
],
"count": 2,
"resource_id": 39301
"resource_id": 233
}
```
## 获取验证码
@@ -255,6 +357,10 @@
`resource_id` 从URL中获取id是上一步验证码的那个随机字符串id `captcha` 是用户输入的验证码
### 提交新评论
只需要提供如下四项信息即可
```json
{
"resource_id": 39301,
@@ -272,13 +378,38 @@
}
```
### 提交楼中楼评论
还需要额外提供一个 `comment_id`,也就是 UUID`60c838a12a5620b7e4ba5dfc`
```json
{
"resource_id": 39301,
"content": "评论内容",
"id": "1234abc",
"captcha": "38op",
"comment_id": "60c838a12a5620b7e4ba5dfc"
}
```
## 删除评论,软删除
* DELETE `/api/comment`提交json数据
删除子评论
```json
{
"id": "60cab935e9f929e09c91392a"
"parent_id": "60cab935e9f929e09c91392a",
"child_id": "60cab935e9f929e09c91392a1111111"
}
```
```json
{
"parent_id": "60cab935e9f929e09c91392a"
}
```

View File

@@ -8,5 +8,5 @@ pymongo==3.11.2
tornado==6.0.4
redis==3.5.3
captcha==0.3
passlib==1.7.4
fakeredis==1.5.0

View File

@@ -7,11 +7,13 @@
__author__ = "Benny <benny.think@gmail.com>"
import uuid
import pymongo
import os
import time
from http import HTTPStatus
from datetime import timedelta, date
from datetime import timedelta, date, datetime
from bson.objectid import ObjectId
import requests
@@ -102,16 +104,33 @@ class BlacklistMongoResource(BlacklistResource):
class CommentMongoResource(CommentResource, Mongo):
@staticmethod
def convert_objectid(data):
def __init__(self):
super().__init__()
self.page = 1
self.size = 5
def convert_objectid(self, data):
final_data = []
for item in data:
item["id"] = str(item["_id"])
item.pop("_id")
final_data.append(item)
# legacy issues
if item.get("children") is None:
item["children"] = []
# 嵌套评论同样也要支持分页
# 新评论在上
item["children"].reverse()
item["children_count"] = len(item["children"])
item["children"] = item["children"][(self.page - 1) * self.size: self.page * self.size]
return final_data
def get_comment(self, resource_id: int, page: int, size: int) -> dict:
def get_comment(self, resource_id: int, page: int, size: int, **kwargs) -> dict:
inner_page = kwargs.get("inner_page", 1)
inner_size = kwargs.get("inner_size", 5)
self.page = inner_page
self.size = inner_size
condition = {"resource_id": resource_id, "deleted_at": {"$exists": False}}
if resource_id == -1:
condition.pop("resource_id")
@@ -125,11 +144,11 @@ class CommentMongoResource(CommentResource, Mongo):
"resource_id": resource_id
}
def add_comment(self, captcha: str, captcha_id: int, content: str, resource_id: int, ip: str,
username: str, browser: str) -> dict:
def add_comment(self, captcha: str, captcha_id: int, content: str, resource_id: int,
ip: str, username: str, browser: str, comment_id=None) -> dict:
returned = {"status_code": 0, "message": ""}
verify_result = CaptchaResource().verify_code(captcha, captcha_id)
verify_result["status"] = 1
if not verify_result["status"]:
returned["status_code"] = HTTPStatus.BAD_REQUEST
returned["message"] = verify_result
@@ -140,26 +159,45 @@ class CommentMongoResource(CommentResource, Mongo):
returned["status_code"] = HTTPStatus.NOT_FOUND
returned["message"] = "资源不存在"
return returned
# TODO 楼中楼
# ObjectId.from_datetime(ObjectId().generation_time)
construct = {
if comment_id:
exists = self.db["comment"].find_one({"_id": ObjectId(comment_id)})
if not exists:
returned["status_code"] = HTTPStatus.NOT_FOUND
returned["message"] = "评论不存在"
return returned
basic_comment = {
"username": username,
"ip": ip,
"date": ts_date(),
"browser": browser,
"content": content,
# "_id": ObjectId.from_datetime(ObjectId().generation_time),
"resource_id": resource_id
}
self.db["comment"].insert_one(construct)
if comment_id is None:
# 普通评论
basic_comment["children"] = []
self.db["comment"].insert_one(basic_comment)
else:
# 嵌套评论
object_id = uuid.uuid1().hex
basic_comment["id"] = object_id
self.db["comment"].find_one_and_update({"_id": ObjectId(comment_id)},
{"$push": {"children": basic_comment}}
)
returned["status_code"] = HTTPStatus.CREATED
returned["message"] = "评论成功"
return returned
def delete_comment(self, obj_id: str):
def delete_comment(self, parent_id: str, child_id: str = None):
current_time = ts_date()
count = self.db["comment"].update_one({"_id": ObjectId(obj_id), "deleted_at": {"$exists": False}},
{"$set": {"deleted_at": current_time}}).modified_count
if child_id is None:
count = self.db["comment"].update_one({"_id": ObjectId(parent_id), "deleted_at": {"$exists": False}},
{"$set": {"deleted_at": current_time}}).modified_count
else:
count = self.db["comment"].update_one({"_id": ObjectId(parent_id), "deleted_at": {"$exists": False}},
{"$pull": {"children": {"id": child_id}}}).modified_count
returned = {"status_code": 0, "message": "", "count": -1}
if count == 0:

68
yyetsweb/SQLite.py Normal file
View File

@@ -0,0 +1,68 @@
#!/usr/local/bin/python3
# coding: utf-8
# YYeTsBot - SQLite.py
# 6/17/21 12:53
#
__author__ = "Benny <benny.think@gmail.com>"
import json
import sqlite3
import logging
from database import ResourceResource
logging.warning("\n\n%s\n### SQLite adapter is immature! Only search and view resource is available for now. ###\n%s\n",
"#" * 87, "#" * 87)
class SQLite:
def __init__(self):
self.con = sqlite3.connect("yyets.sqlite", check_same_thread=False)
self.cur = self.con.cursor()
def __del__(self):
self.con.close()
class FakeSQLiteResource:
pass
class ResourceSQLiteResource(ResourceResource, SQLite):
def get_resource_data(self, resource_id: int, username=None) -> dict:
self.cur.execute("SELECT data FROM yyets WHERE id=?", (resource_id,))
data = self.cur.fetchone()
return json.loads(data[0])
def search_resource(self, keyword: str) -> dict:
Query = """
SELECT id, cnname, enname, aliasname FROM yyets WHERE
cnname LIKE ? or enname LIKE ? or aliasname LIKE ?;
"""
keyword = f"%{keyword}%"
self.cur.execute(Query, (keyword, keyword, keyword))
data = self.cur.fetchall()
final_data = []
for item in data:
single = {
"data": {
"info": {
"id": item[0],
"cnname": item[1],
"enname": item[2],
"aliasname": item[3],
}
}
}
final_data.append(single)
return dict(data=list(final_data))
if __name__ == '__main__':
r = ResourceSQLiteResource()
# r.get_resource_data(80000)
a = r.search_resource("NIGERUHA")
print(json.dumps(a, ensure_ascii=False))

View File

@@ -11,10 +11,12 @@ import json
import logging
import random
import re
import os
import string
import base64
import redis
import fakeredis
from captcha.image import ImageCaptcha
predefined_str = re.sub(r"[1l0oOI]", "", string.ascii_letters + string.digits)
@@ -22,7 +24,10 @@ predefined_str = re.sub(r"[1l0oOI]", "", string.ascii_letters + string.digits)
class Redis:
def __init__(self):
self.r = redis.StrictRedis(host="redis", decode_responses=True, db=2)
if os.getenv("DISABLE_REDIS"):
self.r = fakeredis.FakeStrictRedis()
else:
self.r = redis.StrictRedis(host="redis", decode_responses=True, db=2)
def __del__(self):
self.r.close()
@@ -140,15 +145,14 @@ class NameResource:
class CommentResource:
def get_comment(self, resource_id: int, page: int, size: int) -> dict:
def get_comment(self, resource_id: int, page: int, size: int, **kwargs) -> dict:
pass
def add_comment(self, captcha: str, captcha_id: int, content: str, resource_id: int, ip: str,
username: str, browser: str) -> dict:
username: str, browser: str, comment_id=None) -> dict:
pass
def delete_comment(self, payload: dict):
def delete_comment(self, parent_id: str, child_id: str = None):
pass

View File

@@ -25,9 +25,12 @@ from tornado import web, escape, gen
from database import Redis, AntiCrawler, CaptchaResource
escape.json_encode = lambda value: json.dumps(value, ensure_ascii=False)
logging.basicConfig(level=logging.INFO)
adapter = os.getenv("adapter") or "Mongo"
logging.info("%s Running with %s. %s", "#" * 10, adapter, "#" * 10)
class BaseHandler(web.RequestHandler):
executor = ThreadPoolExecutor(200)
@@ -257,10 +260,12 @@ class CommentHandler(BaseHandler):
resource_id = int(self.get_argument("resource_id", "0"))
size = int(self.get_argument("size", "5"))
page = int(self.get_argument("page", "1"))
inner_size = int(self.get_argument("inner_size", "5"))
inner_page = int(self.get_argument("inner_page", "1"))
if not resource_id:
self.set_status(HTTPStatus.BAD_REQUEST)
return {"status": False, "message": "请提供resource id"}
comment_data = self.instance.get_comment(resource_id, page, size)
comment_data = self.instance.get_comment(resource_id, page, size, inner_size=inner_size, inner_page=inner_page)
self.hide_phone((comment_data["data"]))
return comment_data
@@ -271,11 +276,14 @@ class CommentHandler(BaseHandler):
captcha_id = payload["id"]
content = payload["content"]
resource_id = payload["resource_id"]
comment_id = payload.get("comment_id")
real_ip = AntiCrawler(self).get_real_ip()
username = self.get_current_user()
browser = self.request.headers['user-agent']
result = self.instance.add_comment(captcha, captcha_id, content, resource_id, real_ip, username, browser)
result = self.instance.add_comment(captcha, captcha_id, content, resource_id, real_ip,
username, browser, comment_id)
self.set_status(result["status_code"])
return result
@@ -285,8 +293,11 @@ class CommentHandler(BaseHandler):
# payload = {"id": "obj_id"}
payload = json.loads(self.request.body)
username = self.get_current_user()
parent_id = payload["parent_id"]
child_id = payload.get("child_id")
if self.instance.is_admin(username):
result = self.instance.delete_comment(payload["id"])
result = self.instance.delete_comment(parent_id, child_id)
self.set_status(result["status_code"])
return result
else:

View File

@@ -121,7 +121,7 @@
<form action="search.html">
<label>
<input name="kw" type="text">
<input name="keyword" type="text">
</label>
<input type="submit" value="搜索">
</form>

View File

@@ -0,0 +1,46 @@
#!/usr/local/bin/python3
# coding: utf-8
# YYeTsBot - convert_to_sqlite.py
# 6/17/21 12:41
#
__author__ = "Benny <benny.think@gmail.com>"
import json
import pymongo
import sqlite3
mongo = pymongo.MongoClient()
yyets = mongo["zimuzu"]["yyets"]
con = sqlite3.connect("yyets.sqlite")
cur = con.cursor()
TABLE_SQL = """
CREATE TABLE IF NOT EXISTS yyets
(
id int,
cnname text,
enname text,
aliasname text,
views int,
data text
);
"""
cur.execute(TABLE_SQL)
INSERT_SQL = """
INSERT INTO yyets VALUES (?, ?, ?, ?, ?, ?);
"""
for resource in yyets.find(projection={"_id": False}):
resource_id = resource["data"]["info"]["id"]
cnname = resource["data"]["info"]["cnname"]
enname = resource["data"]["info"]["enname"]
aliasname = resource["data"]["info"]["aliasname"]
views = resource["data"]["info"]["views"]
cur.execute(INSERT_SQL, (resource_id, cnname, enname, aliasname, views, json.dumps(resource, ensure_ascii=False)))
con.commit()
con.close()

View File

@@ -62,7 +62,7 @@
</h2>
<form action="search.html">
<label>
<input name="kw" id="kw" type="text">
<input name="keyword" id="keyword" type="text">
</label>
<input type="submit" value="搜索">
</form>
@@ -85,12 +85,12 @@
<script src="js/common.js"></script>
<script>
let kwe = document.URL.split("kw=")[1];
let kwe = document.URL.split("keyword=")[1];
let kw = decodeURI(kwe).toLowerCase().replace(" ", "");
// const axios = require('axios');
// Make a request for a user with a given ID
axios.get('/api/resource?kw=' + kw)
axios.get('/api/resource?keyword=' + kw)
.then(function (response) {
// handle success
doSearch(response.data.data)
@@ -106,7 +106,7 @@
function doSearch(data) {
let search = document.getElementById("kw");
let search = document.getElementById("keyword");
if (kw !== "undefined") {
search.value = kw;
}

View File

@@ -78,4 +78,13 @@ if __name__ == "__main__":
options.parse_command_line()
p = options.options.p
h = options.options.h
banner = """
▌ ▌ ▌ ▌ ▀▛▘
▝▞ ▝▞ ▞▀▖ ▌ ▞▀▘
▌ ▌ ▛▀ ▌ ▝▀▖
▘ ▘ ▝▀▘ ▘ ▀▀
Lazarus came back from the dead. By @Bennythink
"""
print(banner)
RunServer.run_server(port=p, host=h)