admin.article

This commit is contained in:
p23 2024-12-16 17:08:35 +00:00
parent 4c6efb3884
commit d122fcfb7a
11 changed files with 192 additions and 116 deletions

4
app.py
View File

@ -9,7 +9,7 @@ from utils.pgclass import Base, SQLuser
from utils.platform_consts import PLIST_ROOT from utils.platform_consts import PLIST_ROOT
from blueprints.article import article from blueprints.article import article
from blueprints.log import log from blueprints.log import log
# from blueprints.admin import admin from blueprints.admin import admin
# env # env
PG_HOST = os.getenv("PG_HOST", None) PG_HOST = os.getenv("PG_HOST", None)
@ -62,7 +62,7 @@ app.config["SECRET_KEY"] = os.urandom(64)
# register blueprints # register blueprints
app.register_blueprint(article, url_prefix = "/article") app.register_blueprint(article, url_prefix = "/article")
app.register_blueprint(log , url_prefix = "/log") app.register_blueprint(log , url_prefix = "/log")
# app.register_blueprint(admin , url_prefix = "/admin") app.register_blueprint(admin , url_prefix = "/admin")
# logger # logger
logger.logger("server.start", "Server is running") logger.logger("server.start", "Server is running")

View File

@ -12,9 +12,8 @@ from flask import Blueprint, request, jsonify, make_response, g, abort
from bcrypt import hashpw, gensalt, checkpw from bcrypt import hashpw, gensalt, checkpw
from functools import wraps from functools import wraps
from utils import pgclass, setting_loader, logger from utils import pgclass, setting_loader, logger, dbhelper
from utils.misc import error, internal_json2protobuf from utils.misc import error, internal_json2protobuf
from utils.dbhelper import db, solo_article_fetcher, multi_article_fetcher, solo_file_fetcher, solo_article_remover
from utils.platform_consts import PLIST, PLIST_ROOT from utils.platform_consts import PLIST, PLIST_ROOT
from protobuf_files import niming_pb2 from protobuf_files import niming_pb2
@ -39,7 +38,7 @@ def role_required(permreq: list):
# db 驗證帳號是否正確 # db 驗證帳號是否正確
table = pgclass.SQLuser table = pgclass.SQLuser
with db.getsession() as session: with dbhelper.db.getsession() as session:
res = session.query(table).filter(table.user == jwtdata["user"], table.id == jwtdata["id"]).first() res = session.query(table).filter(table.user == jwtdata["user"], table.id == jwtdata["id"]).first()
if res is None: return error("You do not have permission to view this page."), 401 if res is None: return error("You do not have permission to view this page."), 401
@ -70,7 +69,7 @@ def login():
# db # db
table = pgclass.SQLuser table = pgclass.SQLuser
with db.getsession() as session: u = session.query(table).filter(table.user==username).first() with dbhelper.db.getsession() as session: u = session.query(table).filter(table.user==username).first()
# auth # auth
if u is None: return error("Login Failed"), 401 # 找不到用戶 if u is None: return error("Login Failed"), 401 # 找不到用戶
if not checkpw(password.encode("utf-8"), u.password.encode("utf-8")): return error("Login Failed"), 401 # 密碼沒法跟hash對上 if not checkpw(password.encode("utf-8"), u.password.encode("utf-8")): return error("Login Failed"), 401 # 密碼沒法跟hash對上
@ -103,7 +102,7 @@ def user_me():
@role_required([]) @role_required([])
def user_list(): def user_list():
table = pgclass.SQLuser table = pgclass.SQLuser
with db.getsession() as session: users = session.query(table).all() with dbhelper.db.getsession() as session: users = session.query(table).all()
res = [ {"id":u.id, "user":u.user, "permission":u.permission} for u in users ] res = [ {"id":u.id, "user":u.user, "permission":u.permission} for u in users ]
return jsonify(res), 200 return jsonify(res), 200
@ -111,7 +110,7 @@ def user_list():
@role_required([]) @role_required([])
def user_get(id:int): def user_get(id:int):
table = pgclass.SQLuser table = pgclass.SQLuser
with db.getsession() as session: u = session.query(table).filter(table.id==int(id)).first() with dbhelper.db.getsession() as session: u = session.query(table).filter(table.id==int(id)).first()
if u is None: return error("User not found"), 404 if u is None: return error("User not found"), 404
return jsonify({"id":u.id, "user":u.user, "permission":u.permission}), 200 return jsonify({"id":u.id, "user":u.user, "permission":u.permission}), 200
@ -121,7 +120,7 @@ def user_del(id:int):
# db # db
table = pgclass.SQLuser table = pgclass.SQLuser
with db.getsession() as session: with dbhelper.db.getsession() as session:
opuser = g.opuser # user who requested opuser = g.opuser # user who requested
# check root # check root
@ -141,7 +140,7 @@ def user_del(id:int):
def user_add(): def user_add():
# db # db
table = pgclass.SQLuser table = pgclass.SQLuser
with db.getsession() as session: with dbhelper.db.getsession() as session:
# user who requested # user who requested
opuser = g.opuser opuser = g.opuser
@ -176,50 +175,98 @@ def user_add():
#################### ####################
# list / get / pend / delete / fileget # list / get / pend / delete / fileget
@admin.route("/article/file/<int:id>", methods = ["GET"]) @admin.route("/article/file/<fnhash>", methods = ["GET"])
@role_required(["article.read"]) @role_required(["article.read"])
def article_fileget(id:int): def article_fileget(fnhash:str):
resp, code = solo_file_fetcher("admin", id) resp, code = dbhelper.solo_file_fetcher("admin", fnhash)
return resp, code return resp, code
@admin.route('/article/list', methods = ["GET"]) @admin.route('/article/list', methods = ["GET"])
@role_required(["article.read"]) @role_required(["article.read"])
def article_list(): def article_list():
res, code = multi_article_fetcher("admin", request.args.get("page"), 80) res, code = dbhelper.multi_article_fetcher("admin", request.args.get("page"), 80)
return res, code return res, code
@admin.route("/article/<int:id>", methods=["GET"])
@role_required(["article.read"])
def article_read(id:int):
res, code = solo_article_fetcher("admin", id)
if code == 200:
return internal_json2protobuf(res), code
return res, code
@admin.route("/article/<int:id>", methods=["DELETE"]) def check_key(type:str, key:str | int) -> str | int:
@role_required(["article.del"]) if type == 'a':
def article_del(id:int): if not (len(key) > 0 and key.isdigit()):
opuser = g.opuser return abort(400)
outkey = int(key) # id
result, code = solo_article_remover("admin", id=id) elif type == 'c':
if not code == 200: if not (len(key) > 0):
return result, code return abort(400)
outkey = str(key) # sha1
logger.logger("article.delete", "User:%s deleted post (id=%d): last_status=%s"%(opuser.user, result["id"], result["mark"])) else:
return abort(404)
return niming_pb2.FetchResponse( return outkey
posts = [ niming_pb2.FetchResponse.Message(id = result["id"], mark = result["mark"]) ]
).SerializeToString(), 200
@admin.route("/article/<int:id>", methods=["PUT"])
# get article / comment
@admin.route("/article/<type>/<key>", methods=["GET"])
@role_required(["article.read"])
def article_read(type:str, key:str):
key = check_key(type, key)
if type == 'a':
res, code = dbhelper.solo_article_fetcher("admin", key)
elif type == 'c':
res, code = dbhelper.solo_comment_fetcher("admin", key)
if code == 200:
return internal_json2protobuf(role="admin", otype=type, original=[res]), code
return abort(code)
# delete article / comment
@admin.route("/article/<type>/<key>", methods=["DELETE"])
@role_required(["article.del"])
def article_del(type:str, key:str):
key = check_key(type, key)
opuser = g.opuser
if type == 'a':
rtype = niming_pb2.AdminFetchPostResponse
result, code = dbhelper.solo_article_remover(role="admin", id=key, opuser=opuser.user)
elif type == 'c':
rtype = niming_pb2.AdminFetchCommentResponse
result, code = dbhelper.solo_comment_remover(role="admin", sha1=key, opuser=opuser.user)
if not code == 200: # Exception
return abort(code)
if type == 'a':
appobj = rtype.Message(id=result["id"], mark=result["mark"])
elif type == 'c':
appobj = rtype.Message(sha1=result["sha1"], mark=result["mark"])
ret = rtype()
ret.posts.append(appobj)
return ret.SerializeToString(), 200
# pend article / comment
@admin.route("/article/<type>/<key>", methods=["PUT"])
@role_required(["article.pend"]) @role_required(["article.pend"])
def article_pend(id:int): def article_pend(type:str, key:str):
key = check_key(type, key)
# 找到本體
if type == 'a':
tg, code = dbhelper.solo_article_fetcher(role="admin", key=key)
elif type == 'c':
tg, code = dbhelper.solo_comment_fetcher(role="admin", key=key)
if code != 200:
return abort(code)
# db # db
table = pgclass.SQLarticle mark = pgclass.SQLmark
with db.getsession() as session: with dbhelper.db.getsession() as session:
# 確保文章存在 res = session.query(mark).filter(mark.hash==tg["hash"]).first()
res = session.query(table).filter(table.id==int(id)).first() if res is None:
if res is None: return abort(404) return abort(404)
# 如果文章已經公開 # 如果文章已經公開
if res.mark == "visible": if res.mark == "visible":
@ -230,7 +277,7 @@ def article_pend(id:int):
# run IG Post # run IG Post
return abort(200) return "OK", 200
else: else:
return abort(500) return abort(500)
@ -255,4 +302,4 @@ def setting_edit():
if d == 0: return error("Failed"), 401 if d == 0: return error("Failed"), 401
logger.logger("setting.modify", "User:%s modified settings: %s"%(opuser, json.dumps(request.json))) logger.logger("setting.modify", "User:%s modified settings: %s"%(opuser, json.dumps(request.json)))
return jsonify(d), 200 return jsonify(d), 200

View File

@ -6,19 +6,6 @@ from utils import pgclass, setting_loader, dbhelper
from utils.misc import internal_json2protobuf, error_proto from utils.misc import internal_json2protobuf, error_proto
from protobuf_files import niming_pb2 from protobuf_files import niming_pb2
"""
TODO:
- 修復錯誤
- article fetch general 還是會顯示 pending 的留言
- IG post ( Po文刪文只PO本體文章 )
- 檔案傳輸加低畫質版本(縮圖)
- log 的方式之後要重新設計 > 正規化
- IP Record (deploy之前配合rev proxy)
- gunicorn
- 檔案完成但是再看看要不要讓發文者持sha256存取自己發的文的檔案
"""
article = Blueprint('article', __name__) article = Blueprint('article', __name__)
# 匿名文列表 # 匿名文列表
@ -54,7 +41,7 @@ def owner_getarticle(type:str, key:str):
return abort(400) return abort(400)
key = str(key) # sha1 key = str(key) # sha1
else: else:
return abort(400) return abort(404)
# 獲取指定文章/留言 # 獲取指定文章/留言
if request.method == "GET": if request.method == "GET":
@ -63,8 +50,8 @@ def owner_getarticle(type:str, key:str):
elif type == 'c': # 留言 elif type == 'c': # 留言
resfn, code = dbhelper.solo_comment_fetcher("owner", key=key, hash=sha256) resfn, code = dbhelper.solo_comment_fetcher("owner", key=key, hash=sha256)
if code == 200: if code == 200:
return internal_json2protobuf({"type":type, "data":[resfn]}), code return internal_json2protobuf(role="owner", otype=type, original=[resfn]), code
return resfn, code return abort(code)
# 刪除指定文章/留言 # 刪除指定文章/留言
elif request.method == "DELETE": elif request.method == "DELETE":
if type == 'a': if type == 'a':
@ -74,8 +61,8 @@ def owner_getarticle(type:str, key:str):
rtype = niming_pb2.FetchCommentResponse rtype = niming_pb2.FetchCommentResponse
result, code = dbhelper.solo_comment_remover("owner", hash=sha256, sha1=key) result, code = dbhelper.solo_comment_remover("owner", hash=sha256, sha1=key)
if not code == 200: if not code == 200: # Exception
return result, code return abort(code)
if type == 'a': if type == 'a':
ret = rtype(posts=[ rtype.Message(id=result["id"]) ]) ret = rtype(posts=[ rtype.Message(id=result["id"]) ])
@ -90,8 +77,8 @@ def owner_getarticle(type:str, key:str):
def getarticle(id:int): def getarticle(id:int):
resfn, code = dbhelper.solo_article_fetcher("general", key=id) resfn, code = dbhelper.solo_article_fetcher("general", key=id)
if code == 200: if code == 200:
return internal_json2protobuf({"type":'a', "data":[resfn]}), code return internal_json2protobuf(role="general", otype='a', original=[resfn]), code
return resfn, code return abort(code)
# 獲取指定文章的留言 # 獲取指定文章的留言
@ -99,8 +86,8 @@ def getarticle(id:int):
def getcomment(sha1:str): def getcomment(sha1:str):
resfn, code = dbhelper.solo_comment_fetcher("general", key=sha1) resfn, code = dbhelper.solo_comment_fetcher("general", key=sha1)
if code == 200: if code == 200:
return internal_json2protobuf({"type":'c', "data":[resfn]}), code return internal_json2protobuf(role="general", otype='c', original=[resfn]), code
return resfn, code return abort(code)
# 上傳文章 / 留言 # 上傳文章 / 留言

View File

@ -41,6 +41,34 @@ message FetchCommentResponse {
string sha1 = 1; string sha1 = 1;
string content = 2; string content = 2;
} }
// Several comment info
repeated Message posts = 1;
}
// FOR ADMIN
message AdminFetchPostResponse {
message Message {
uint64 id = 1;
string content = 2;
repeated string files_hash = 3;
optional string igid = 4;
repeated string comments_hash = 5;
string ip = 6;
string hash = 7;
string mark = 8;
}
// Several post info // Several post info
repeated Message posts = 1; repeated Message posts = 1;
}
message AdminFetchCommentResponse {
message Message {
string sha1 = 1;
string content = 2;
string ip = 3;
string hash = 4;
string mark = 5;
}
// Several comment info
repeated Message posts = 1;
} }

View File

@ -13,15 +13,15 @@ _sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cniming.proto\"@\n\x04Post\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x10\n\x03ref\x18\x02 \x01(\x03H\x00\x88\x01\x01\x12\r\n\x05\x66iles\x18\x03 \x03(\x0c\x42\x06\n\x04_ref\"q\n\x0cPostResponse\x12\x17\n\x06status\x18\x01 \x01(\x0e\x32\x07.Status\x12\x0c\n\x04hash\x18\x02 \x01(\t\x12\n\n\x02id\x18\x03 \x01(\x04\x12\x1b\n\x0e\x66\x61iled_message\x18\x04 \x01(\tH\x00\x88\x01\x01\x42\x11\n\x0f_failed_message\"\xad\x01\n\x11\x46\x65tchPostResponse\x12)\n\x05posts\x18\x01 \x03(\x0b\x32\x1a.FetchPostResponse.Message\x1am\n\x07Message\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\x12\x12\n\nfiles_hash\x18\x03 \x03(\t\x12\x11\n\x04igid\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x15\n\rcomments_hash\x18\x05 \x03(\tB\x07\n\x05_igid\"n\n\x14\x46\x65tchCommentResponse\x12,\n\x05posts\x18\x01 \x03(\x0b\x32\x1d.FetchCommentResponse.Message\x1a(\n\x07Message\x12\x0c\n\x04sha1\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t*!\n\x06Status\x12\n\n\x06\x46\x61iled\x10\x00\x12\x0b\n\x07Success\x10\x01\x62\x06proto3') DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cniming.proto\"@\n\x04Post\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x10\n\x03ref\x18\x02 \x01(\x03H\x00\x88\x01\x01\x12\r\n\x05\x66iles\x18\x03 \x03(\x0c\x42\x06\n\x04_ref\"q\n\x0cPostResponse\x12\x17\n\x06status\x18\x01 \x01(\x0e\x32\x07.Status\x12\x0c\n\x04hash\x18\x02 \x01(\t\x12\n\n\x02id\x18\x03 \x01(\x04\x12\x1b\n\x0e\x66\x61iled_message\x18\x04 \x01(\tH\x00\x88\x01\x01\x42\x11\n\x0f_failed_message\"\xad\x01\n\x11\x46\x65tchPostResponse\x12)\n\x05posts\x18\x01 \x03(\x0b\x32\x1a.FetchPostResponse.Message\x1am\n\x07Message\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\x12\x12\n\nfiles_hash\x18\x03 \x03(\t\x12\x11\n\x04igid\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x15\n\rcomments_hash\x18\x05 \x03(\tB\x07\n\x05_igid\"n\n\x14\x46\x65tchCommentResponse\x12,\n\x05posts\x18\x01 \x03(\x0b\x32\x1d.FetchCommentResponse.Message\x1a(\n\x07Message\x12\x0c\n\x04sha1\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\"\xe0\x01\n\x16\x41\x64minFetchPostResponse\x12.\n\x05posts\x18\x01 \x03(\x0b\x32\x1f.AdminFetchPostResponse.Message\x1a\x95\x01\n\x07Message\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\x12\x12\n\nfiles_hash\x18\x03 \x03(\t\x12\x11\n\x04igid\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x15\n\rcomments_hash\x18\x05 \x03(\t\x12\n\n\x02ip\x18\x06 \x01(\t\x12\x0c\n\x04hash\x18\x07 \x01(\t\x12\x0c\n\x04mark\x18\x08 \x01(\tB\x07\n\x05_igid\"\xa0\x01\n\x19\x41\x64minFetchCommentResponse\x12\x31\n\x05posts\x18\x01 \x03(\x0b\x32\".AdminFetchCommentResponse.Message\x1aP\n\x07Message\x12\x0c\n\x04sha1\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\x12\n\n\x02ip\x18\x03 \x01(\t\x12\x0c\n\x04hash\x18\x04 \x01(\t\x12\x0c\n\x04mark\x18\x05 \x01(\t*!\n\x06Status\x12\n\n\x06\x46\x61iled\x10\x00\x12\x0b\n\x07Success\x10\x01\x62\x06proto3')
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'niming_pb2', globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'niming_pb2', globals())
if _descriptor._USE_C_DESCRIPTORS == False: if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None DESCRIPTOR._options = None
_STATUS._serialized_start=485 _STATUS._serialized_start=875
_STATUS._serialized_end=518 _STATUS._serialized_end=908
_POST._serialized_start=16 _POST._serialized_start=16
_POST._serialized_end=80 _POST._serialized_end=80
_POSTRESPONSE._serialized_start=82 _POSTRESPONSE._serialized_start=82
@ -34,4 +34,12 @@ if _descriptor._USE_C_DESCRIPTORS == False:
_FETCHCOMMENTRESPONSE._serialized_end=483 _FETCHCOMMENTRESPONSE._serialized_end=483
_FETCHCOMMENTRESPONSE_MESSAGE._serialized_start=443 _FETCHCOMMENTRESPONSE_MESSAGE._serialized_start=443
_FETCHCOMMENTRESPONSE_MESSAGE._serialized_end=483 _FETCHCOMMENTRESPONSE_MESSAGE._serialized_end=483
_ADMINFETCHPOSTRESPONSE._serialized_start=486
_ADMINFETCHPOSTRESPONSE._serialized_end=710
_ADMINFETCHPOSTRESPONSE_MESSAGE._serialized_start=561
_ADMINFETCHPOSTRESPONSE_MESSAGE._serialized_end=710
_ADMINFETCHCOMMENTRESPONSE._serialized_start=713
_ADMINFETCHCOMMENTRESPONSE._serialized_end=873
_ADMINFETCHCOMMENTRESPONSE_MESSAGE._serialized_start=793
_ADMINFETCHCOMMENTRESPONSE_MESSAGE._serialized_end=873
# @@protoc_insertion_point(module_scope) # @@protoc_insertion_point(module_scope)

View File

@ -1 +1 @@
{"Check_Before_Post": true, "JWT_Valid_Time": 604800, "Niming_Max_Word": 500, "Attachment_Count": 5, "Attachment_Size": 209715200, "Allowed_MIME": ["image/jpeg", "image/pjpeg", "image/png", "image/heic", "image/heif", "video/mp4", "video/quicktime", "video/hevc", "image/webp"]} {"Check_Before_Post": false, "JWT_Valid_Time": 604800, "Niming_Max_Word": 500, "Attachment_Count": 5, "Attachment_Size": 209715200, "Allowed_MIME": ["image/jpeg", "image/pjpeg", "image/png", "image/heic", "image/heif", "video/mp4", "video/quicktime", "video/hevc", "image/webp"]}

View File

@ -5,7 +5,7 @@ import secrets
import hashlib import hashlib
import os import os
from flask import make_response, Response, abort, request from flask import make_response, Response, request
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from sqlalchemy import desc, update, Engine, text, delete from sqlalchemy import desc, update, Engine, text, delete
import pytz import pytz
@ -146,7 +146,7 @@ def solo_comment_uploader(content:str, ref:int) -> Tuple[int | str, str]:
def solo_article_fetcher(role:str, key:int, hash:str=None) -> Tuple[Dict, int]: # admin, owner, general def solo_article_fetcher(role:str, key:int, hash:str=None) -> Tuple[Dict, int]: # admin, owner, general
with db.getsession() as session: with db.getsession() as session:
# article fetch # article fetch
stmt="SELECT posts.id, posts.content, posts.file_list, meta.igid, posts.hash, meta.ip " \ stmt="SELECT posts.id, posts.content, posts.file_list, meta.igid, posts.hash, meta.ip, pmark.mark " \
+"FROM posts " \ +"FROM posts " \
+"INNER JOIN mark AS pmark ON posts.hash=pmark.hash " \ +"INNER JOIN mark AS pmark ON posts.hash=pmark.hash " \
+"INNER JOIN article_meta AS meta ON posts.hash=meta.hash " +"INNER JOIN article_meta AS meta ON posts.hash=meta.hash "
@ -159,7 +159,7 @@ def solo_article_fetcher(role:str, key:int, hash:str=None) -> Tuple[Dict, int]:
result = session.execute(text(stmt), {"id":key, "hash":hash}) result = session.execute(text(stmt), {"id":key, "hash":hash})
res = result.first() res = result.first()
if res is None: if res is None:
return abort(404) return {}, 404
# comment fetch # comment fetch
stmt="SELECT c.sha1 " \ stmt="SELECT c.sha1 " \
@ -185,7 +185,7 @@ def solo_article_fetcher(role:str, key:int, hash:str=None) -> Tuple[Dict, int]:
if role == "admin": if role == "admin":
one["ip"] = res[5] one["ip"] = res[5]
if role == "owner" or role == "admin": one["mark"] = res[6]
one["hash"] = res[4] one["hash"] = res[4]
return one, 200 return one, 200
@ -214,7 +214,7 @@ def solo_comment_fetcher(role:str, key:str, hash:str=None) -> Tuple[Dict, int]:
# 對管理員sha1查詢不設檢查 # 對管理員sha1查詢不設檢查
arta = session.execute(text(stmt), {'sha1':key}).first() arta = session.execute(text(stmt), {'sha1':key}).first()
if arta is None: if arta is None:
return abort(404) return {}, 404
# mapping # mapping
one = { one = {
@ -224,7 +224,7 @@ def solo_comment_fetcher(role:str, key:str, hash:str=None) -> Tuple[Dict, int]:
if role == "admin": if role == "admin":
one["ip"] = arta[5] one["ip"] = arta[5]
if role == "owner" or role == "admin": one["mark"] = arta[3]
one["hash"] = arta[6] one["hash"] = arta[6]
return one, 200 return one, 200
@ -234,17 +234,23 @@ def solo_comment_fetcher(role:str, key:str, hash:str=None) -> Tuple[Dict, int]:
def multi_article_fetcher(role:str, page:str, count:int) -> Tuple[bytes, int]: # general, admin def multi_article_fetcher(role:str, page:str, count:int) -> Tuple[bytes, int]: # general, admin
# checker # checker
if page is None or not page.isdigit(): if page is None or not page.isdigit():
return abort(400) return b"", 400
page = int(page)*count page = int(page)*count
# proto
if role == "admin":
pcl = niming_pb2.AdminFetchPostResponse
else:
pcl = niming_pb2.FetchPostResponse
resfn = pcl()
# db
article = pgclass.SQLarticle article = pgclass.SQLarticle
article_meta = pgclass.SQLmeta article_meta = pgclass.SQLmeta
article_mark = pgclass.SQLmark article_mark = pgclass.SQLmark
resfn = niming_pb2.FetchPostResponse()
with db.getsession() as session: with db.getsession() as session:
# query # query
res = session.query(article.id, article.content, article.file_list, article_meta.igid, article.hash, article_meta.ip) res = session.query(article.id, article.content, article.file_list, article_meta.igid, article.hash, article_meta.ip, article_mark.mark)
res = res.join(article_meta, article_meta.hash==article.hash) res = res.join(article_meta, article_meta.hash==article.hash)
res = res.join(article_mark, article_mark.hash==article.hash) res = res.join(article_mark, article_mark.hash==article.hash)
if role == "general": if role == "general":
@ -253,23 +259,24 @@ def multi_article_fetcher(role:str, page:str, count:int) -> Tuple[bytes, int]: #
# mapping # mapping
for r in res: for r in res:
one = niming_pb2.FetchPostResponse.Message( one = pcl.Message(
id = r[0], id = r[0],
content = r[1], content = r[1],
igid = r[3], igid = r[3],
) )
if r[2]: # files if r[2]: # files
one.files_hash.extend(r[2]) one.files_hash.extend(r[2])
if role == "admin": # 如果是管理員 多給ip 跟 hash # proto那邊沒支援 if role == "admin": # 如果是管理員 多給 ip, hash, mark
one.hash = r[4] one.hash = r[4]
one.ip = r[5] one.ip = r[5]
one.mark = r[6]
resfn.posts.append(one) resfn.posts.append(one)
return resfn.SerializeToString(), 200 return resfn.SerializeToString(), 200
# 刪除單一文章 # 刪除單一文章
def solo_article_remover(role:str, hash:str=None, id:int=None) -> Tuple[Dict, int]: # admin, owner def solo_article_remover(role:str, hash:str=None, id:int=None, opuser:str=None) -> Tuple[Dict, int]: # admin, owner
article = pgclass.SQLarticle article = pgclass.SQLarticle
article_mark = pgclass.SQLmark article_mark = pgclass.SQLmark
with db.getsession() as session: with db.getsession() as session:
@ -281,7 +288,7 @@ def solo_article_remover(role:str, hash:str=None, id:int=None) -> Tuple[Dict, in
elif role == "owner": elif role == "owner":
pres = pres.filter(article.id == id, article.hash == hash).first() pres = pres.filter(article.id == id, article.hash == hash).first()
if pres is None: # 如果本體不存在 if pres is None: # 如果本體不存在
return abort(404) return {}, 404
# 獲取本體的留言們(hash) # 獲取本體的留言們(hash)
stmt="SELECT c.hash as chash " \ stmt="SELECT c.hash as chash " \
@ -303,19 +310,21 @@ def solo_article_remover(role:str, hash:str=None, id:int=None) -> Tuple[Dict, in
# 刪除檔案 # 刪除檔案
err = s3helper.multi_file_remover(pres[3]) err = s3helper.multi_file_remover(pres[3])
if err: if err:
return abort(500) return {}, 500
session.commit() session.commit()
# logger # logger
logger.logger("delpost", "Delete post (id=%d): last_status=%s" 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"
%(int(pres[0]), str(pres[2]))) %(int(pres[0]), str(pres[2])))
return {"id":pres[0], "mark":pres[2]}, 200 return {"id":pres[0], "mark":pres[2]}, 200
# 刪除單一留言 # 刪除單一留言
def solo_comment_remover(role:str, hash:str=None, sha1:str=None) -> Tuple[Dict, int]: def solo_comment_remover(role:str, hash:str=None, sha1:str=None, opuser:str=None) -> Tuple[Dict, int]:
article_mark = pgclass.SQLmark article_mark = pgclass.SQLmark
with db.getsession() as session: with db.getsession() as session:
# 獲取留言本體 # 獲取留言本體
@ -328,7 +337,7 @@ def solo_comment_remover(role:str, hash:str=None, sha1:str=None) -> Tuple[Dict,
stmt += "WHERE c.sha1 = :sha1 AND c.hash = :hash" stmt += "WHERE c.sha1 = :sha1 AND c.hash = :hash"
cres = session.execute(text(stmt), {'sha1':sha1, 'hash':hash}).first() cres = session.execute(text(stmt), {'sha1':sha1, 'hash':hash}).first()
if cres is None: # 如果不存在 if cres is None: # 如果不存在
return abort(404) return {}, 404
# 刪除留言本體 # 刪除留言本體
stmt="UPDATE posts " \ stmt="UPDATE posts " \
@ -340,16 +349,18 @@ def solo_comment_remover(role:str, hash:str=None, sha1:str=None) -> Tuple[Dict,
session.execute(text(stmt), {'sha1':cres[1], 'hash':cres[2]}) session.execute(text(stmt), {'sha1':cres[1], 'hash':cres[2]})
# 刪除留言mark # 刪除留言mark
mark = session.query(article_mark.mark).filter(article_mark.hash == cres[2]) mark = session.query(article_mark.mark).filter(article_mark.hash == cres[2]).first()
stmt = delete(article_mark).where(article_mark.hash == cres[2]) stmt = delete(article_mark).where(article_mark.hash == cres[2])
session.execute(stmt) session.execute(stmt)
session.commit() session.commit()
logger.logger("delcomment", "Delete comment (sha1=%s): last_status=%s" logtype = "comment.delete" if role == "admin" else "delcomment"
%(cres[1], str(mark))) loguser = "User:%s "%opuser if role == "admin" else ""
logger.logger(logtype, loguser+"Delete comment (sha1=%s): last_status=%s"
%(cres[1], str(mark[0])))
return {"sha1":cres[1], "mark":mark}, 200 return {"sha1":cres[1], "mark":mark[0]}, 200
# 獲取檔案 # 獲取檔案

View File

@ -13,14 +13,14 @@ def error_proto(message:str) -> bytes:
).SerializeToString() ).SerializeToString()
def internal_json2protobuf(original:dict) -> bytes: def internal_json2protobuf(role:str, otype:str, original:list) -> bytes:
otype = original["type"]
if otype == 'a': if otype == 'a':
rtype = niming_pb2.FetchPostResponse if role == "admin": rtype = niming_pb2.AdminFetchPostResponse
else: rtype = niming_pb2.FetchPostResponse
elif otype == 'c': elif otype == 'c':
rtype = niming_pb2.FetchCommentResponse if role == "admin": rtype = niming_pb2.AdminFetchCommentResponse
else: rtype = niming_pb2.FetchCommentResponse
original = original["data"]
res = rtype() res = rtype()
for o in original: for o in original:
@ -32,8 +32,16 @@ def internal_json2protobuf(original:dict) -> bytes:
if "igid" in o: ob.igid = o["igid"] if "igid" in o: ob.igid = o["igid"]
if "files_hash" in o: ob.files_hash.extend(o["files_hash"]) if "files_hash" in o: ob.files_hash.extend(o["files_hash"])
if "comments_hash" in o: ob.comments_hash.extend(o["comments_hash"]) if "comments_hash" in o: ob.comments_hash.extend(o["comments_hash"])
# admin
if "mark" in o: ob.mark = o["mark"]
if "hash" in o: ob.hash = o["hash"]
if "ip" in o: ob.ip = o["ip"]
elif otype == "c": elif otype == "c":
ob = rtype.Message(sha1=o["sha1"]) ob = rtype.Message(sha1=o["sha1"])
# admin
if "mark" in o: ob.mark = o["mark"]
if "ip" in o: ob.ip = o["ip"]
if "hash" in o: ob.hash = o["hash"]
ob.content = o["content"] ob.content = o["content"]

View File

@ -1,4 +1,4 @@
from sqlalchemy import Column, String, TIMESTAMP, func, BIGINT, LargeBinary, ARRAY from sqlalchemy import Column, String, TIMESTAMP, func, BIGINT, ARRAY
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy_utils.types.pg_composite import CompositeType from sqlalchemy_utils.types.pg_composite import CompositeType
from sqlalchemy.ext.mutable import MutableList from sqlalchemy.ext.mutable import MutableList
@ -19,7 +19,7 @@ comment_type = CompositeType(
) )
# posts # post
class SQLarticle(Base): class SQLarticle(Base):
__tablename__ = 'posts' __tablename__ = 'posts'
@ -57,20 +57,8 @@ class SQLlog(Base):
message = Column(String) message = Column(String)
source = Column(String) source = Column(String)
def __repr__(self):
return f"<log(id={self.id}, created_at={self.created_at}, message={self.message}, source={self.source})>"
# deprecated
class SQLfile(Base):
__tablename__ = 'files'
id = Column(BIGINT, primary_key=True)
created_at = Column(TIMESTAMP(timezone=True), server_default=func.now())
type = Column(String)
binary = Column(LargeBinary)
# user
class SQLuser(Base): class SQLuser(Base):
__tablename__ = 'users' __tablename__ = 'users'
@ -78,6 +66,3 @@ class SQLuser(Base):
user = Column(String) user = Column(String)
password = Column(String) # hash , sha512 password = Column(String) # hash , sha512
permission = Column(ARRAY(String)) permission = Column(ARRAY(String))
def __repr__(self):
return f"<user(id={self.id}, user={self.user}, password={self.password}, permission={self.permission})>"

View File

@ -5,7 +5,10 @@ PLIST_ROOT = PLIST + ["usermgr"]
# event type # event type
EVENT_TYPE = { EVENT_TYPE = {
"general": ["newpost", "delpost", "newcomment", "delcomment"], "general": ["newpost", "delpost", "newcomment", "delcomment"],
"admin": ["login", "user.create", "user.delete", "article.delete", "article.pend", "setting.modify"], "admin": ["login", "user.create", "user.delete", # user
"article.delete", "comment.delete", # article
"article.pend", "comment.pend", # comment
"setting.modify"], # settings
"server": ["server.start"] "server": ["server.start"]
} }

View File

@ -7,7 +7,6 @@ import io
import sys import sys
import minio import minio
from minio.deleteobjects import DeleteObject
S3_BUCKET:str = os.getenv("S3_BUCKET") S3_BUCKET:str = os.getenv("S3_BUCKET")