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/* data/*
logs/* logs/*
**/.DS_Store **/.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` * GET `/api/comment`
@@ -217,31 +271,79 @@
分页支持URL参数 分页支持URL参数
* resource_id: 资源idid为233是留言板id为-1会返回最新评论 * resource_id: 资源idid为233是留言板id为-1会返回最新评论
* size: 每页评论数量默认5(或者其他数值) * size: 每页评论数量默认5
* page: 当前页 * page: 当前页默认1
* inner_size: 内嵌评论数量默认5
* inner_page: 内嵌评论当前页默认1
返回 返回
普通评论
```json ```json
{ {
"data": [ "data": [
{ {
"date": "2018-09-18 11:12:15", "username": "Benny",
"username": "uuua2", "date": "2021-06-17 10:54:19",
"content": "tdaadd", "browser": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.13; rv:85.1) Gecko/20100101 Firefox/85.1",
"id": 2 "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": "Benny",
"username": "abcd", "date": "2021-06-15 10:54:19",
"content": "tdaadd", "browser": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.13; rv:85.1) Gecko/20100101 Firefox/85.1",
"id": 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, "count": 2,
"resource_id": 39301 "resource_id": 233
} }
``` ```
## 获取验证码 ## 获取验证码
@@ -255,6 +357,10 @@
`resource_id` 从URL中获取id是上一步验证码的那个随机字符串id `captcha` 是用户输入的验证码 `resource_id` 从URL中获取id是上一步验证码的那个随机字符串id `captcha` 是用户输入的验证码
### 提交新评论
只需要提供如下四项信息即可
```json ```json
{ {
"resource_id": 39301, "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数据 * DELETE `/api/comment`提交json数据
删除子评论
```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 tornado==6.0.4
redis==3.5.3 redis==3.5.3
captcha==0.3 captcha==0.3
passlib==1.7.4 passlib==1.7.4
fakeredis==1.5.0

View File

@@ -7,11 +7,13 @@
__author__ = "Benny <benny.think@gmail.com>" __author__ = "Benny <benny.think@gmail.com>"
import uuid
import pymongo import pymongo
import os import os
import time import time
from http import HTTPStatus from http import HTTPStatus
from datetime import timedelta, date from datetime import timedelta, date, datetime
from bson.objectid import ObjectId from bson.objectid import ObjectId
import requests import requests
@@ -102,16 +104,33 @@ class BlacklistMongoResource(BlacklistResource):
class CommentMongoResource(CommentResource, Mongo): class CommentMongoResource(CommentResource, Mongo):
@staticmethod def __init__(self):
def convert_objectid(data): super().__init__()
self.page = 1
self.size = 5
def convert_objectid(self, data):
final_data = [] final_data = []
for item in data: for item in data:
item["id"] = str(item["_id"]) item["id"] = str(item["_id"])
item.pop("_id") item.pop("_id")
final_data.append(item) 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 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}} condition = {"resource_id": resource_id, "deleted_at": {"$exists": False}}
if resource_id == -1: if resource_id == -1:
condition.pop("resource_id") condition.pop("resource_id")
@@ -125,11 +144,11 @@ class CommentMongoResource(CommentResource, Mongo):
"resource_id": resource_id "resource_id": resource_id
} }
def add_comment(self, captcha: str, captcha_id: int, content: str, resource_id: int, ip: str, def add_comment(self, captcha: str, captcha_id: int, content: str, resource_id: int,
username: str, browser: str) -> dict: ip: str, username: str, browser: str, comment_id=None) -> dict:
returned = {"status_code": 0, "message": ""} returned = {"status_code": 0, "message": ""}
verify_result = CaptchaResource().verify_code(captcha, captcha_id) verify_result = CaptchaResource().verify_code(captcha, captcha_id)
verify_result["status"] = 1
if not verify_result["status"]: if not verify_result["status"]:
returned["status_code"] = HTTPStatus.BAD_REQUEST returned["status_code"] = HTTPStatus.BAD_REQUEST
returned["message"] = verify_result returned["message"] = verify_result
@@ -140,26 +159,45 @@ class CommentMongoResource(CommentResource, Mongo):
returned["status_code"] = HTTPStatus.NOT_FOUND returned["status_code"] = HTTPStatus.NOT_FOUND
returned["message"] = "资源不存在" returned["message"] = "资源不存在"
return returned return returned
# TODO 楼中楼
# ObjectId.from_datetime(ObjectId().generation_time) if comment_id:
construct = { 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, "username": username,
"ip": ip, "ip": ip,
"date": ts_date(), "date": ts_date(),
"browser": browser, "browser": browser,
"content": content, "content": content,
# "_id": ObjectId.from_datetime(ObjectId().generation_time),
"resource_id": resource_id "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["status_code"] = HTTPStatus.CREATED
returned["message"] = "评论成功" returned["message"] = "评论成功"
return returned return returned
def delete_comment(self, obj_id: str): def delete_comment(self, parent_id: str, child_id: str = None):
current_time = ts_date() current_time = ts_date()
count = self.db["comment"].update_one({"_id": ObjectId(obj_id), "deleted_at": {"$exists": False}}, if child_id is None:
{"$set": {"deleted_at": current_time}}).modified_count 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} returned = {"status_code": 0, "message": "", "count": -1}
if count == 0: 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 logging
import random import random
import re import re
import os
import string import string
import base64 import base64
import redis import redis
import fakeredis
from captcha.image import ImageCaptcha from captcha.image import ImageCaptcha
predefined_str = re.sub(r"[1l0oOI]", "", string.ascii_letters + string.digits) 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: class Redis:
def __init__(self): 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): def __del__(self):
self.r.close() self.r.close()
@@ -140,15 +145,14 @@ class NameResource:
class CommentResource: 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 pass
def add_comment(self, captcha: str, captcha_id: int, content: str, resource_id: int, ip: str, 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 pass
def delete_comment(self, parent_id: str, child_id: str = None):
def delete_comment(self, payload: dict):
pass pass

View File

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

View File

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

View File

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