niming_backend/utils/dbhelper.py

400 lines
14 KiB
Python
Raw Permalink Normal View History

2024-11-19 21:22:01 +08:00
from typing import Tuple, Dict, List
2024-12-09 03:24:22 +08:00
from datetime import datetime
import time
import secrets
import hashlib
import os
2024-11-19 21:22:01 +08:00
2024-12-17 01:08:35 +08:00
from flask import make_response, Response, request
2024-12-09 03:25:03 +08:00
from sqlalchemy.orm import sessionmaker
2024-12-12 11:30:51 +08:00
from sqlalchemy import desc, update, Engine, text, delete
2024-12-09 03:24:22 +08:00
import pytz
2024-11-19 21:22:01 +08:00
2024-12-18 03:07:56 +08:00
from utils import pgclass, setting_loader, s3helper, logger, ighelper
2024-11-26 17:38:28 +08:00
from utils.misc import error
2024-11-25 21:51:50 +08:00
from protobuf_files import niming_pb2
2024-11-19 21:22:01 +08:00
2024-12-09 03:24:22 +08:00
class DB:
2024-11-19 21:22:01 +08:00
_engine = None
@classmethod
2024-11-19 23:29:01 +08:00
def __init__(cls, engine):
2024-12-09 03:24:22 +08:00
cls._engine:Engine = engine
2024-11-19 21:22:01 +08:00
@classmethod
2024-11-19 23:29:01 +08:00
def getsession(cls):
Session = sessionmaker(bind=cls._engine)
2024-11-19 21:22:01 +08:00
return Session()
2024-12-09 03:24:22 +08:00
db:DB = None
2024-12-12 11:30:51 +08:00
TIMEZONE:str = os.getenv("TIMEZONE")
2024-12-09 03:24:22 +08:00
# 上傳單一文章
def solo_article_uploader(content:str, file_list, fmimes:List[str]) -> Tuple[int, str]:
# loadset
opt = setting_loader.loadset()
chk_before_post = opt["Check_Before_Post"]
# hash
seed = content + str(time.time()) + str(secrets.token_urlsafe(nbytes=16))
hash = hashlib.sha256(seed.encode()).hexdigest()
# IP
ip = request.remote_addr
2024-12-18 03:07:56 +08:00
# tmp igid
2024-12-09 03:24:22 +08:00
igid = None
# mark
if chk_before_post: mark = "pending"
else: mark = "visible"
# posting
article = pgclass.SQLarticle
article_mark = pgclass.SQLmark
article_metadata = pgclass.SQLmeta
result_id = 0
2024-12-18 03:07:56 +08:00
with db.getsession() as session:
try:
2024-12-09 03:24:22 +08:00
# file processor
fnlist, err = s3helper.multi_file_uploader(file_list, fmimes)
if err:
return 0, ""
2024-12-12 11:30:51 +08:00
# db processor (meta, article, mark)
metaa = article_metadata(ip=ip, igid=igid, hash=hash)
posta = article(content=content, hash=hash, file_list=fnlist)
marka = article_mark(hash=hash, mark=mark)
2024-12-09 03:24:22 +08:00
2024-12-12 11:30:51 +08:00
session.add(metaa)
2024-12-09 03:24:22 +08:00
session.add(posta)
session.add(marka)
# commit
session.commit()
result_id = int(posta.id)
2024-12-18 03:07:56 +08:00
except:
2024-12-23 02:03:55 +08:00
session.rollback()
2024-12-18 03:07:56 +08:00
return 0, ""
2024-12-09 03:24:22 +08:00
2024-12-18 03:07:56 +08:00
# logger
logger.logger("newpost", "New post (id=%d): %s"%(result_id, mark))
# ig posting
if not chk_before_post: # 如果不用審核
result, err = ighelper.request_upload(result_id)
if err or result["result"] == "Canceled delete post request":
return 0, ""
return result_id, hash
2024-12-09 03:24:22 +08:00
# 上傳單一留言
def solo_comment_uploader(content:str, ref:int) -> Tuple[int | str, str]:
# loadset
opt = setting_loader.loadset()
chk_before_post = opt["Check_Before_Post"]
# hash
seed = content + str(time.time()) + str(secrets.token_urlsafe(nbytes=16))
hash = hashlib.sha256(seed.encode()).hexdigest()
sha1 = hashlib.sha1(seed.encode()).hexdigest()
# IP
ip = request.remote_addr
# mark
if chk_before_post: mark = "pending"
else: mark = "visible"
# posting
article = pgclass.SQLarticle
article_mark = pgclass.SQLmark
2024-12-18 03:07:56 +08:00
with db.getsession() as session:
try:
2024-12-09 03:24:22 +08:00
# article processor
cda = {
"content":content,
"ip":ip,
"hash":hash,
2024-12-12 11:30:51 +08:00
"created_at":datetime.now(pytz.timezone(TIMEZONE)),
2024-12-09 03:24:22 +08:00
"sha1":sha1
}
session.execute(
update(article)
.where(article.id == ref)
.values(comment_list=article.comment_list + [cda])
)
# mark processor
marka = article_mark(hash=hash,
mark=mark)
session.add(marka)
# commit
session.commit()
# logger
logger.logger("newcomment", "New comment %s points to %d: %s"%(sha1, ref, mark))
return sha1, hash
2024-12-18 03:07:56 +08:00
except Exception as e:
2024-12-23 02:03:55 +08:00
session.rollback()
2024-12-18 03:07:56 +08:00
return 0, ""
2024-12-09 03:24:22 +08:00
2024-11-26 09:17:44 +08:00
2024-11-19 21:22:01 +08:00
# role (general) (owner) (admin)
# 獲取單一文章
2024-12-12 11:30:51 +08:00
def solo_article_fetcher(role:str, key:int, hash:str=None) -> Tuple[Dict, int]: # admin, owner, general
2024-11-19 21:22:01 +08:00
with db.getsession() as session:
2024-12-12 11:30:51 +08:00
# article fetch
2024-12-17 01:08:35 +08:00
stmt="SELECT posts.id, posts.content, posts.file_list, meta.igid, posts.hash, meta.ip, pmark.mark " \
2024-12-12 11:30:51 +08:00
+"FROM posts " \
+"INNER JOIN mark AS pmark ON posts.hash=pmark.hash " \
+"INNER JOIN article_meta AS meta ON posts.hash=meta.hash "
if role == "owner": # 驗證id/hash可以看到本體(無驗證)
2024-12-09 03:24:22 +08:00
stmt += "WHERE posts.id = :id AND posts.hash = :hash"
2024-12-12 11:30:51 +08:00
elif role == "admin": # 驗證id可以看到本體(無驗證)
2024-12-09 03:24:22 +08:00
stmt += "WHERE posts.id = :id"
2024-12-12 11:30:51 +08:00
elif role == "general": # 驗證id可以看到本體(visible)
stmt += "WHERE posts.id=:id AND pmark.mark='visible'"
result = session.execute(text(stmt), {"id":key, "hash":hash})
2024-12-09 03:24:22 +08:00
res = result.first()
2024-11-26 09:17:44 +08:00
if res is None:
2024-12-17 01:08:35 +08:00
return {}, 404
2024-12-12 11:30:51 +08:00
# comment fetch
stmt="SELECT c.sha1 " \
+"FROM posts " \
+"INNER JOIN unnest(posts.comment_list) AS c ON c=ANY(posts.comment_list) " \
+"INNER JOIN mark AS cmark ON c.hash=cmark.hash " \
+"WHERE posts.id=:id"
if role == "general": # 留言sha1(visible)
stmt+=" AND cmark.mark='visible'"
result = session.execute(text(stmt), {"id":res[0]})
cres = result.all()
2024-11-19 21:22:01 +08:00
# mapping
2024-11-26 09:17:44 +08:00
one = {
"id": res[0],
2024-12-09 03:24:22 +08:00
"content": res[1],
"igid": res[3],
2024-11-26 09:17:44 +08:00
}
2024-12-12 11:30:51 +08:00
if res[2]: # files
one["files_hash"] = res[2]
if res[4]: # comments
one["comments_hash"] = [ c[0] for c in cres ]
2024-11-26 09:17:44 +08:00
if role == "admin":
2024-12-12 11:30:51 +08:00
one["ip"] = res[5]
2024-12-17 01:08:35 +08:00
one["mark"] = res[6]
2024-12-12 11:30:51 +08:00
one["hash"] = res[4]
2024-12-09 03:24:22 +08:00
return one, 200
2024-12-09 14:10:58 +08:00
2024-12-09 03:24:22 +08:00
# role (general) (owner) (admin)
# 獲取單一留言
2024-12-12 11:30:51 +08:00
def solo_comment_fetcher(role:str, key:str, hash:str=None) -> Tuple[Dict, int]: # admin, owner, general
2024-12-09 03:24:22 +08:00
with db.getsession() as session:
# query
2024-12-12 11:30:51 +08:00
stmt="SELECT posts.id AS parent_id, posts.hash AS parent_hash, pmark.mark AS parent_mark, cmark.mark AS comment_mark, c.* " \
+"FROM posts " \
+"INNER JOIN unnest(posts.comment_list) AS c ON c=ANY(posts.comment_list) " \
+"JOIN mark AS pmark ON posts.hash=pmark.hash " \
+"JOIN mark AS cmark ON c.hash=cmark.hash " \
+"WHERE c.sha1=:sha1 "
2024-12-09 03:24:22 +08:00
if role == "general":
2024-12-12 11:30:51 +08:00
# 對一般用戶sha1查詢確保本體跟留言可見
stmt += "AND pmark.mark='visible' AND cmark.mark='visible'"
arta = session.execute(text(stmt), {'sha1':key}).first()
2024-12-09 03:24:22 +08:00
elif role == "owner":
2024-12-12 11:30:51 +08:00
# 對發文者sha1查詢sha256查詢不設檢查
stmt += "AND c.hash=:hash"
arta = session.execute(text(stmt), {'sha1':key, 'hash':hash}).first()
2024-12-09 03:24:22 +08:00
elif role == "admin":
2024-12-12 11:30:51 +08:00
# 對管理員sha1查詢不設檢查
arta = session.execute(text(stmt), {'sha1':key}).first()
2024-12-09 03:24:22 +08:00
if arta is None:
2024-12-17 01:08:35 +08:00
return {}, 404
2024-12-09 03:24:22 +08:00
# mapping
one = {
2024-12-12 11:30:51 +08:00
"content": arta[4],
"sha1": arta[8]
2024-12-09 03:24:22 +08:00
}
if role == "admin":
2024-12-12 11:30:51 +08:00
one["ip"] = arta[5]
2024-12-17 01:08:35 +08:00
one["mark"] = arta[3]
2024-12-12 11:30:51 +08:00
one["hash"] = arta[6]
2024-11-26 09:17:44 +08:00
return one, 200
2024-11-19 21:22:01 +08:00
# 獲取文章列表
2024-11-26 09:17:44 +08:00
def multi_article_fetcher(role:str, page:str, count:int) -> Tuple[bytes, int]: # general, admin
2024-11-19 21:22:01 +08:00
# checker
2024-11-25 21:51:50 +08:00
if page is None or not page.isdigit():
2024-12-17 01:08:35 +08:00
return b"", 400
2024-11-26 09:17:44 +08:00
page = int(page)*count
2024-11-19 21:22:01 +08:00
2024-12-17 01:08:35 +08:00
# proto
if role == "admin":
pcl = niming_pb2.AdminFetchPostResponse
else:
pcl = niming_pb2.FetchPostResponse
resfn = pcl()
# db
2024-12-09 03:24:22 +08:00
article = pgclass.SQLarticle
article_meta = pgclass.SQLmeta
article_mark = pgclass.SQLmark
2024-11-19 21:22:01 +08:00
with db.getsession() as session:
# query
2024-12-17 01:08:35 +08:00
res = session.query(article.id, article.content, article.file_list, article_meta.igid, article.hash, article_meta.ip, article_mark.mark)
2024-12-09 03:24:22 +08:00
res = res.join(article_meta, article_meta.hash==article.hash)
res = res.join(article_mark, article_mark.hash==article.hash)
2024-11-19 21:22:01 +08:00
if role == "general":
2024-12-09 03:24:22 +08:00
res = res.filter(article_mark.mark == "visible")
res = res.order_by(desc(article.id)).offset(page).limit(count).all()
2024-11-26 09:17:44 +08:00
2024-11-19 21:22:01 +08:00
# mapping
for r in res:
2024-12-17 01:08:35 +08:00
one = pcl.Message(
2024-12-09 03:24:22 +08:00
id = r[0],
content = r[1],
igid = r[3],
2024-11-26 09:17:44 +08:00
)
2024-12-12 11:30:51 +08:00
if r[2]: # files
one.files_hash.extend(r[2])
2024-12-17 01:08:35 +08:00
if role == "admin": # 如果是管理員 多給 ip, hash, mark
2024-12-09 03:24:22 +08:00
one.hash = r[4]
one.ip = r[5]
2024-12-17 01:08:35 +08:00
one.mark = r[6]
2024-11-26 09:17:44 +08:00
resfn.posts.append(one)
2024-11-19 21:22:01 +08:00
2024-11-26 09:17:44 +08:00
return resfn.SerializeToString(), 200
2024-11-19 21:22:01 +08:00
2024-12-09 03:24:22 +08:00
# 刪除單一文章
2024-12-17 01:08:35 +08:00
def solo_article_remover(role:str, hash:str=None, id:int=None, opuser:str=None) -> Tuple[Dict, int]: # admin, owner
2024-12-09 03:24:22 +08:00
article = pgclass.SQLarticle
article_mark = pgclass.SQLmark
2024-12-18 03:07:56 +08:00
article_meta = pgclass.SQLmeta
2024-11-19 21:22:01 +08:00
with db.getsession() as session:
# 獲取本體
2024-12-18 03:07:56 +08:00
pres = session.query(article.id, article.hash, article_mark.mark, article.file_list, article_meta.igid) \
.join(article_mark, article.hash==article_mark.hash) \
.join(article_meta, article.hash==article_meta.hash)
2024-11-19 21:22:01 +08:00
if role == "admin":
2024-12-12 11:30:51 +08:00
pres = pres.filter(article.id == id).first()
2024-11-26 09:17:44 +08:00
elif role == "owner":
2024-12-12 11:30:51 +08:00
pres = pres.filter(article.id == id, article.hash == hash).first()
2024-12-09 03:24:22 +08:00
if pres is None: # 如果本體不存在
2024-12-17 01:08:35 +08:00
return {}, 404
2024-12-09 03:24:22 +08:00
# 獲取本體的留言們(hash)
stmt="SELECT c.hash as chash " \
2024-12-12 11:30:51 +08:00
+"FROM posts, unnest(posts.comment_list) AS c " \
+"WHERE posts.id = :id"
2024-12-09 03:24:22 +08:00
cres = session.execute(text(stmt), {'id':pres[0]}).all()
# 刪除本體
stmt = delete(article).where(article.hash == pres[1])
session.execute(stmt)
# 刪除 mark (本體 & 留言)
stmt = delete(article_mark).where(article_mark.hash == pres[1])
session.execute(stmt)
for c in cres:
stmt = delete(article_mark).where(article_mark.hash == c[0])
session.execute(stmt)
# 刪除檔案
err = s3helper.multi_file_remover(pres[3])
if err:
2024-12-17 01:08:35 +08:00
return {}, 500
2024-12-09 03:24:22 +08:00
2024-11-19 21:22:01 +08:00
session.commit()
2024-12-18 03:07:56 +08:00
# 刪除IG貼文
igid = pres[4]
if igid:
result, err = ighelper.request_delete(aid=pres[0], code=igid)
# 錯誤檢查
if err or result["result"] == "Canceled upload post request":
return {}, 500
2024-12-09 03:24:22 +08:00
# logger
2024-12-17 01:08:35 +08:00
logtype = "article.delete" if role == "admin" else "delpost"
loguser = "User:%s "%opuser if role == "admin" else ""
logger.logger(logtype, loguser+"Delete post (id=%d): last_status=%s"
2024-12-09 03:24:22 +08:00
%(int(pres[0]), str(pres[2])))
return {"id":pres[0], "mark":pres[2]}, 200
# 刪除單一留言
2024-12-17 01:08:35 +08:00
def solo_comment_remover(role:str, hash:str=None, sha1:str=None, opuser:str=None) -> Tuple[Dict, int]:
2024-12-09 03:24:22 +08:00
article_mark = pgclass.SQLmark
with db.getsession() as session:
# 獲取留言本體
stmt="SELECT posts.id AS parent, c.sha1, c.hash " \
2024-12-12 11:30:51 +08:00
+"FROM posts, unnest(posts.comment_list) AS c "
2024-12-09 03:24:22 +08:00
if role == "admin":
stmt += "WHERE c.sha1 = :sha1"
2024-12-12 11:30:51 +08:00
cres = session.execute(text(stmt), {'sha1':sha1}).first()
2024-12-09 03:24:22 +08:00
elif role == 'owner':
stmt += "WHERE c.sha1 = :sha1 AND c.hash = :hash"
2024-12-12 11:30:51 +08:00
cres = session.execute(text(stmt), {'sha1':sha1, 'hash':hash}).first()
2024-12-09 03:24:22 +08:00
if cres is None: # 如果不存在
2024-12-17 01:08:35 +08:00
return {}, 404
2024-12-09 03:24:22 +08:00
2024-12-12 11:30:51 +08:00
# 刪除留言本體
2024-12-09 03:24:22 +08:00
stmt="UPDATE posts " \
+"SET comment_list = ARRAY(" \
+"SELECT c " \
+"FROM unnest(comment_list) AS c " \
+"WHERE (c.sha1, c.hash) != (:sha1, :hash)" \
+")"
session.execute(text(stmt), {'sha1':cres[1], 'hash':cres[2]})
2024-12-12 11:30:51 +08:00
# 刪除留言mark
2024-12-17 01:08:35 +08:00
mark = session.query(article_mark.mark).filter(article_mark.hash == cres[2]).first()
2024-12-09 03:24:22 +08:00
stmt = delete(article_mark).where(article_mark.hash == cres[2])
session.execute(stmt)
session.commit()
2024-12-17 01:08:35 +08:00
logtype = "comment.delete" if role == "admin" else "delcomment"
loguser = "User:%s "%opuser if role == "admin" else ""
logger.logger(logtype, loguser+"Delete comment (sha1=%s): last_status=%s"
%(cres[1], str(mark[0])))
2024-12-09 03:24:22 +08:00
2024-12-17 01:08:35 +08:00
return {"sha1":cres[1], "mark":mark[0]}, 200
2024-11-26 09:17:44 +08:00
2024-11-19 21:22:01 +08:00
# 獲取檔案
2024-12-09 03:24:22 +08:00
def solo_file_fetcher(role:str, fnhash:str) -> Tuple[Response, int]: # general, admin
2024-11-19 21:22:01 +08:00
with db.getsession() as session:
2024-12-12 11:30:51 +08:00
arta="SELECT posts.id FROM posts " \
+"INNER JOIN mark ON posts.hash=mark.hash " \
+"WHERE :fnhash=ANY (posts.file_list) "
2024-11-19 21:22:01 +08:00
if role == "general":
2024-12-09 14:10:58 +08:00
arta += "AND mark.mark = 'visible'"
arta = session.execute(text(arta), {'fnhash':fnhash}).first()
2024-12-09 03:24:22 +08:00
if arta is None: # 檢查文章本體是否存在/可以閱覽
2024-11-19 21:22:01 +08:00
return error("File not found"), 404
2024-12-09 03:24:22 +08:00
# fetch file
f, err = s3helper.solo_file_fetcher(fnhash)
if err:
return error("File not found"), 404
resp = make_response(f["binary"])
resp.headers.set("Content-Type", f["mime"])
resp.headers.set("Content-Disposition", f"attachment; filename=file_{fnhash}")
return resp, 200