to_protobuf

This commit is contained in:
p23 2024-11-26 01:17:44 +00:00
parent 07cb2ac2cc
commit bf454e8f27
9 changed files with 236 additions and 218 deletions

View File

@ -9,9 +9,10 @@ from bcrypt import hashpw, gensalt, checkpw
from functools import wraps
from utils import pgclass, setting_loader, logger
from utils.misc import error
from utils.misc import error, internal_json2protobuf, error_proto
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 protobuf_files import niming_pb2
admin = Blueprint("admin", __name__)
@ -180,14 +181,16 @@ def article_fileget(id:int):
@admin.route('/article/list', methods = ["GET"])
@role_required(["article.read"])
def article_list():
res, code = multi_article_fetcher("admin", request.args.get("page"), 30)
return jsonify(res), code
res, code = multi_article_fetcher("admin", request.args.get("page"), 80)
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)
return jsonify(res), code
if code == 200:
return internal_json2protobuf(res), code
return res, code
@admin.route("/article/<int:id>", methods=["DELETE"])
@role_required(["article.del"])
@ -195,10 +198,15 @@ def article_del(id:int):
opuser = g.opuser
result, code = solo_article_remover("admin", id=id)
if "error" in result: return jsonify(result), code
if not code == 200:
return result, code
logger.logger("article.delete", "User:%s deleted post (id=%d with comments %s): last_status=%s"%(opuser.user, result["id"], result["rcl"], result["mark"]))
return jsonify({"result":"OK"}), 200
logger.logger("article.delete", "User:%s deleted post (id=%d): last_status=%s"%(opuser.user, result["id"], result["mark"]))
return niming_pb2.FetchResponse(
status = niming_pb2.Status.Success,
posts = [ niming_pb2.FetchResponse.Message(id = result["id"], mark = result["mark"]) ]
).SerializeToString(), 200
@admin.route("/article/<int:id>", methods=["PUT"])
@role_required(["article.pend"])
@ -208,20 +216,20 @@ def article_pend(id:int):
with db.getsession() as session:
# 確保文章存在
res = session.query(table).filter(table.id==int(id)).first()
if res is None: return error("Post not found"), 404
if res is None: return error_proto("fetch", "Post not found"), 404
# 如果文章已經公開
if res.mark == "visible":
return error("Post is already visible."), 400
return error_proto("fetch", "Post is already visible."), 400
elif res.mark == "pending":
res.mark = "visible"
session.commit()
# run IG Post
return jsonify({"result":"OK"}), 200
return niming_pb2.FetchResponse(status=niming_pb2.Status.Success).SerializeToString(), 200
else:
return error("Post mark error"), 500
return error_proto("fetch", "Post mark error"), 500
####################
# Setting Area #

View File

@ -1,5 +1,6 @@
import time
import hashlib
import secrets
import magic
from flask import Blueprint, request, jsonify
@ -26,40 +27,54 @@ article = Blueprint('article', __name__)
@article.route('/list', methods = ["GET"])
def listing():
res, code = multi_article_fetcher("general", request.args.get("page"), 30)
res = internal_json2protobuf(res)
return res, code
# 獲取匿名文附檔
@article.route("/file/<int:id>", methods=["GET"])
def getfile(id:int):
resp, code = solo_file_fetcher("general", id)
return resp, code
# 只有發文者可以看到的獲取指定文章
# 只有發文者可以做到的刪除文章
@article.route("/own/<sha256>", methods = ["GET", "DELETE"])
def owner_getarticle(sha256:str):
table = pgclass.SQLarticle
ftab = pgclass.SQLfile
@article.route("/own/<int:id>", methods = ["GET", "DELETE"])
def owner_getarticle(id:int):
# arguments
sha256 = request.args.get("hash", None)
if not sha256:
return error("Arguments error"), 400
sha256 = str(sha256)
# 獲取指定文章
if request.method == "GET":
resfn, code = solo_article_fetcher("owner", key=sha256)
return jsonify(resfn), code
resfn, code = solo_article_fetcher("owner", key=(sha256, id))
if code == 200:
return internal_json2protobuf(resfn), code
return resfn, code
# 刪除指定文章跟他們的留言、檔案
elif request.method == "DELETE":
result, code = solo_article_remover("general", hash=sha256)
if "error" in result: return jsonify(result), code
result, code = solo_article_remover("owner", hash=sha256, id=id)
if not code == 200:
return result, code
logger.logger("delpost", "Delete post (id=%d): last_status=%s"
%(result["id"], str(result["mark"])))
return niming_pb2.FetchResponse(
status = niming_pb2.Status.Success,
posts = [ niming_pb2.FetchResponse.Message(id = result["id"], mark = result["mark"]) ]
).SerializeToString(), 200
logger.logger("delpost", "Delete post (id=%d with comments %s): last_status=%s"
%(result["id"], str(result["rcl"]), str(result["mark"])))
return jsonify({"result":"OK"}), code
# 獲取指定文章
@article.route("/<int:id>", methods = ["GET"])
def getarticle(id:int):
resfn, code = solo_article_fetcher("general", key=id)
return jsonify(resfn), code
if code == 200:
return internal_json2protobuf(resfn), code
return resfn, code
# 上傳文章 / 留言
@article.route("/", methods = ["POST"])
@ -70,56 +85,46 @@ def posting():
opt = setting_loader.loadset()
chk_before_post = opt["Check_Before_Post"]
maxword = opt["Niming_Max_Word"]
# protobuf parse
recv = niming_pb2.Post()
try: recv.ParseFromString(request.data)
except DecodeError: return error_proto("Protobuf decode error"), 400
except DecodeError: return error_proto("post", "Protobuf decode error"), 400
# content
# content and check
ctx = str(recv.content)
if len(ctx) == 0 or len(ctx) > maxword: # length check
return error_proto("no content or too many words"), 400
return error_proto("post", "no content or too many words"), 400
# hash
seed = ctx + str(time.time())
seed = ctx + str(time.time()) + str(secrets.token_urlsafe(nbytes=16))
hash = hashlib.sha256(seed.encode()).hexdigest()
# SQL start
table = pgclass.SQLarticle
with db.getsession() as session:
# reference
# reference and check
ref = int(recv.ref)
if not (ref == 0): # 如果ref不是0
# 檢查是不是指向存在的文章
chk = session.query(table).filter(table.id == ref, table.mark == "visible").first()
if chk is None: return error_proto("Invalid Reference"), 400
if ref != 0:
# 檢查指向的文章是否也是留言
if not(chk.reference is None): return error_proto("Invalid Reference"), 400
reftg, code = solo_article_fetcher(role="general", key=ref)
if code != 200 or reftg["reference"]:
return error_proto("post", "Invalid Reference"), 400
else:
ref = None
# file processing
# file processing and check
files = recv.files
# check - size
atts = opt["Attachment_Count"]
sizelimit = opt["Attachment_Size"]
if len(files) > atts: return error_proto("Too many files"), 400
if len(files) > atts: return error_proto("post", "Too many files"), 400
for f in files:
if len(f) <= 0 or len(f) > sizelimit: return error_proto("File size error"), 400
if len(f) <= 0 or len(f) > sizelimit: return error_proto("post", "File size error"), 400
# check - mimetype
allowed_mime = opt["Allowed_MIME"]
fmimes = []
for f in files:
mime = magic.Magic(mime=True)
type = mime.from_buffer(f)
if not(type in allowed_mime): return error_proto("File format error"), 400
# run processor
ftab = pgclass.SQLfile
for f in files:
mime = magic.Magic(mime=True)
type = mime.from_buffer(f)
fsql = ftab(reference = hash, binary = f, type = type)
session.add(fsql)
if not(type in allowed_mime): return error_proto("post", "File format error"), 400
fmimes.append(type)
# IP
ip = request.remote_addr
@ -136,26 +141,40 @@ def posting():
else: mark = "visible"
# posting
table = pgclass.SQLarticle
ftab = pgclass.SQLfile
try:
with db.getsession() as session:
# post
data = table(hash = hash, content = ctx, igid = igid, mark = mark, reference = ref, ip = ip)
session.add(data)
# file processor
fmidx = 0
fidarr = []
for f in files:
fsql = ftab(reference = hash, binary = f, type = fmimes[fmidx])
fidarr.append(fsql)
session.add(fsql)
fmidx += 1
# first commit
session.commit()
# set file list
data.file_list = [ fid.id for fid in fidarr ]
session.commit() # second commit
result_id = data.id
except:
return error_proto("post", "Create new post failed"), 400
result, code = solo_article_fetcher(role="owner", key=hash)
# logger
logger.logger("newpost", "New post (id=%d point to %s): %s"%(result["id"], ref, mark))
logger.logger("newpost", "New post (id=%d point to %s): %s"%(result_id, ref, mark))
# to protobuf
proto = niming_pb2.PostResponse()
proto.status = niming_pb2.PostStatus.Success
proto.hash = hash
proto.id = int(result["id"])
proto_stres = proto.SerializeToString()
rr = niming_pb2.PostResponse()
rr.ParseFromString(proto_stres)
print(rr.hash)
print(proto_stres)
return proto_stres, code
# to protobuf & return
proto_stres = niming_pb2.PostResponse(
status = niming_pb2.Status.Success,
hash = hash,
id = int(result_id)
).SerializeToString()
return proto_stres, 200
# 介面全部改成protobuf傳輸
# 檔案傳輸加低畫質版本(縮圖)

View File

@ -8,14 +8,14 @@ message Post {
repeated bytes files = 3;
}
enum PostStatus {
enum Status {
Failed = 0;
Success = 1;
}
// The response of the posting, defining what should return.
message PostResponse {
PostStatus status = 1;
Status status = 1;
string hash = 2;
uint64 id = 3;
optional string failed_message = 4;
@ -30,7 +30,14 @@ message FetchResponse {
// request files through /article/file/<id> with MIME type.
// See it as a BLOB url;
repeated uint64 files_id = 4;
optional string hash = 5;
string igid = 6;
string mark = 7;
optional string ip = 8;
repeated uint64 comments_id = 9;
}
// Several post info
repeated Message posts = 1;
Status status = 1;
repeated Message posts = 2;
optional string failed_message = 3;
}

View File

@ -1,7 +0,0 @@
syntax = "proto3";
message DataMessage {
string content = 1;
optional int64 ref = 2;
repeated optional bytes files = 3;
}

View File

@ -13,21 +13,21 @@ _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\"u\n\x0cPostResponse\x12\x1b\n\x06status\x18\x01 \x01(\x0e\x32\x0b.PostStatus\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\"\x8a\x01\n\rFetchResponse\x12%\n\x05posts\x18\x01 \x03(\x0b\x32\x16.FetchResponse.Message\x1aR\n\x07Message\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\x12\x10\n\x03ref\x18\x03 \x01(\x04H\x00\x88\x01\x01\x12\x10\n\x08\x66iles_id\x18\x04 \x03(\x04\x42\x06\n\x04_ref*%\n\nPostStatus\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\"\xb9\x02\n\rFetchResponse\x12\x17\n\x06status\x18\x01 \x01(\x0e\x32\x07.Status\x12%\n\x05posts\x18\x02 \x03(\x0b\x32\x16.FetchResponse.Message\x12\x1b\n\x0e\x66\x61iled_message\x18\x03 \x01(\tH\x00\x88\x01\x01\x1a\xb7\x01\n\x07Message\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\x12\x10\n\x03ref\x18\x03 \x01(\x04H\x00\x88\x01\x01\x12\x10\n\x08\x66iles_id\x18\x04 \x03(\x04\x12\x11\n\x04hash\x18\x05 \x01(\tH\x01\x88\x01\x01\x12\x0c\n\x04igid\x18\x06 \x01(\t\x12\x0c\n\x04mark\x18\x07 \x01(\t\x12\x0f\n\x02ip\x18\x08 \x01(\tH\x02\x88\x01\x01\x12\x13\n\x0b\x63omments_id\x18\t \x03(\x04\x42\x06\n\x04_refB\x07\n\x05_hashB\x05\n\x03_ipB\x11\n\x0f_failed_message*!\n\x06Status\x12\n\n\x06\x46\x61iled\x10\x00\x12\x0b\n\x07Success\x10\x01\x62\x06proto3')
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'niming_pb2', globals())
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
_POSTSTATUS._serialized_start=342
_POSTSTATUS._serialized_end=379
_STATUS._serialized_start=513
_STATUS._serialized_end=546
_POST._serialized_start=16
_POST._serialized_end=80
_POSTRESPONSE._serialized_start=82
_POSTRESPONSE._serialized_end=199
_FETCHRESPONSE._serialized_start=202
_FETCHRESPONSE._serialized_end=340
_FETCHRESPONSE_MESSAGE._serialized_start=258
_FETCHRESPONSE_MESSAGE._serialized_end=340
_POSTRESPONSE._serialized_end=195
_FETCHRESPONSE._serialized_start=198
_FETCHRESPONSE._serialized_end=511
_FETCHRESPONSE_MESSAGE._serialized_start=309
_FETCHRESPONSE_MESSAGE._serialized_end=492
# @@protoc_insertion_point(module_scope)

View File

@ -1,36 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: niming.proto
# Protobuf Python Version: 5.28.3
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
5,
28,
3,
'',
'niming.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cniming.proto\"6\n\x0b\x44\x61taMessage\x12\x0b\n\x03\x63tx\x18\x01 \x01(\t\x12\x0b\n\x03ref\x18\x02 \x01(\x03\x12\r\n\x05\x66iles\x18\x03 \x03(\x0c\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'niming_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_DATAMESSAGE']._serialized_start=16
_globals['_DATAMESSAGE']._serialized_end=70
# @@protoc_insertion_point(module_scope)

View File

@ -1,11 +1,11 @@
from typing import Tuple, Dict, List
from flask import make_response, Response, jsonify
from sqlalchemy.orm import sessionmaker
from sqlalchemy import desc
from sqlalchemy.orm import sessionmaker, aliased
from sqlalchemy import desc, func, literal, and_
from utils import pgclass
from utils.misc import error
from utils.misc import error, error_proto
from protobuf_files import niming_pb2
class db:
@ -20,104 +20,118 @@ class db:
Session = sessionmaker(bind=cls._engine)
return Session()
# role (general) (owner) (admin)
# 獲取單一文章
def solo_article_fetcher(role:str, key) -> Tuple[Dict,int]: # admin, owner, general
table = pgclass.SQLarticle
ftab = pgclass.SQLfile
resfn = {}
def solo_article_fetcher(role:str, key) -> Tuple[Dict | bytes, int]: # admin, owner, general
table = pgclass.SQLarticle # main
table2 = aliased(table) # comment
with db.getsession() as session:
# query
res = session.query(table.id,
table.content,
table.reference,
table.file_list,
table.hash,
table.igid,
table.mark,
table.ip,
func.coalesce(func.array_agg(table2.id), literal([])).label("comments"))
if role == "owner":
res = session.query(table).filter(table.hash == key).first()
res = res.join(table2, table2.reference == table.id, isouter=True) \
.filter(table.hash == key[0], table.id == key[1])
elif role == "admin":
res = session.query(table).filter(table.id == key).first()
res = res.join(table2, table2.reference == table.id, isouter=True) \
.filter(table.id == key)
elif role == "general":
res = session.query(table).filter(table.id == key, table.mark == "visible").first()
if res is None: return {"error":"Post not found"}, 404
res = res.join(table2, and_(table2.reference == table.id, table2.mark == "visible"), isouter=True) \
.filter(table.id == key, table.mark == "visible")
res = res.group_by(table.id, table.content, table.reference, table.file_list,
table.hash, table.igid, table.mark, table.ip).first()
if res is None:
return error_proto("fetch", "Post not found"), 404
# mapping
resfn.update({"id": res.id, "content": res.content, "igid": res.igid, "mark": res.mark, "reference": res.reference})
if role == "admin": resfn["ip"] = res.ip
elif role == "owner": resfn["hash"] = res.hash
one = {
"id": res[0],
"content":res[1],
"igid":res[5],
"mark":res[6],
"reference":res[2],
"files_id":res[3],
"comments":res[8]
}
# comment
if role == "admin":
one["ip"] = res[7]
if role == "owner" or role == "admin":
resfn["comment"] = [ c[0] for c in session.query(table.id).filter(table.reference == int(res.id)).all() ]
elif role == "general":
resfn["comment"] = [ c[0] for c in session.query(table.id).filter(table.reference == int(res.id), table.mark == "visible").all() ]
one["hash"] = res[4]
# file
resfn["files"] = [ f[0] for f in session.query(ftab.id).filter(ftab.reference == res.hash).all() ]
return one, 200
return resfn, 200
# 獲取文章列表
def multi_article_fetcher(role:str, page:str, count:int) -> Tuple[List, int]: # general, admin
def multi_article_fetcher(role:str, page:str, count:int) -> Tuple[bytes, int]: # general, admin
# checker
if page is None or not page.isdigit():
return error("Arguments error"), 400
page = int(page)*30
return error_proto("fetch", "Arguments error"), 400
page = int(page)*count
table = pgclass.SQLarticle
ftab = pgclass.SQLfile
resfn = []
resfn = niming_pb2.FetchResponse(
status = niming_pb2.Status.Success
)
with db.getsession() as session:
# query
res = session.query(table)
if role == "general":
res = session.query(table).filter(table.mark == "visible", table.reference == None)
res = res.filter(table.mark == "visible", table.reference == None)
elif role == "admin":
res = session.query(table).filter(table.reference == None)
res = res.filter(table.reference == None)
res = res.order_by(desc(table.id)).offset(page).limit(count).all()
# mapping
for r in res:
rup = {"id":r.id, "content":r.content, "igid":r.igid, "created_at":r.created_at, "mark":r.mark, "reference":r.reference}
if role == "admin": rup["ip"] = r.ip # 如果是管理員 多給ip
rup["files"] = [ f[0] for f in session.query(ftab.id).filter(ftab.reference == r.hash).all() ] # 檔案
resfn.append(rup)
one = niming_pb2.FetchResponse.Message(
id = r.id,
content = r.content,
files_id = r.file_list,
igid = r.igid,
mark = r.mark,
ref = r.reference
)
if role == "admin": # 如果是管理員 多給ip 跟 hash
one.hash = r.hash
one.ip = r.ip
resfn.posts.append(one)
return resfn.SerializeToString(), 200
return resfn, 200
# 刪除文章
def solo_article_remover(role:str, hash:str=None, id:int=None) -> Tuple[Dict, int]: # admin, general
def solo_article_remover(role:str, hash:str=None, id:int=None) -> Tuple[Dict | bytes, int]: # admin, owner
key = None
if role == "admin": key = id
elif role == "general": key = hash
elif role == "owner": key = (hash, id)
table = pgclass.SQLarticle
ftab = pgclass.SQLfile
rcl = []
with db.getsession() as session:
# 獲取本體
if role == "admin":
res = session.query(table).filter(table.id == key).first()
elif role == "general":
res = session.query(table).filter(table.hash == key).first()
elif role == "owner":
res = session.query(table).filter(table.hash == key[0], table.id == key[1]).first()
if res is None: # 檢查本體是否存在
return {"error":"Post not found"}, 404
# 刪除本體檔案
session.query(ftab).filter(ftab.reference == res.hash).delete()
# 刪留言
resc = session.query(table).filter(table.reference == res.id).all() # 留言
for c in resc:
rcl.append(c.id)
# 刪留言的檔案
session.query(ftab).filter(ftab.reference == c.hash).delete()
# 刪留言
session.delete(c)
return error_proto("fetch", "Post not found!"), 404
# 刪本體
session.delete(res)
session.commit()
return {"id":res.id, "mark":res.mark, "rcl":rcl}, 200
return {"id":res.id, "mark":res.mark}, 200
# 獲取檔案
def solo_file_fetcher(role:str, id:int) -> Tuple[Response, int]: # general, admin

View File

@ -6,11 +6,14 @@ def error(message:str) -> Response:
return jsonify({"error":message})
def error_proto(message:str) -> Response:
def error_proto(type:str, message:str) -> Response:
if type == "post":
proto = niming_pb2.PostResponse()
proto.status = niming_pb2.PostStatus.Failed
proto.hash = ""
proto.id = 0
elif type == "fetch":
proto = niming_pb2.FetchResponse()
proto.status = niming_pb2.Status.Failed
proto.failed_message = message
return proto.SerializeToString()
@ -19,13 +22,22 @@ def internal_json2protobuf(original:list|dict) -> bytes:
if isinstance(original, dict):
original = [original]
res = niming_pb2.FetchResponse()
res = niming_pb2.FetchResponse(status = niming_pb2.Status.Success)
for o in original:
ob = niming_pb2.FetchResponse.Message()
ob.id = o["id"]
ob.content = o["content"]
ob = niming_pb2.FetchResponse.Message(
id = o["id"],
content = o["content"],
igid = o["igid"],
mark = o["mark"],
files_id = o["files_id"]
)
if None not in o["comments"]:
ob.comments_id.extend(o["comments"])
if o["reference"]:
ob.ref = o["reference"]
ob.files_id.extend(o["files"])
if "ip" in o:
ob.ip = o["ip"]
if "hash" in o:
ob.hash = o["hash"]
res.posts.append(ob)
return res.SerializeToString()

View File

@ -14,6 +14,7 @@ class SQLarticle(Base):
mark = Column(String)
ip = Column(String)
reference = Column(BIGINT)
file_list = Column(ARRAY(BIGINT))
def __repr__(self):
return f"<article(id={self.id}, hash={self.hash}, content={self.content}, igid={self.igid}, mark={self.mark}, created_at={self.created_at}, ip={self.ip}, reference={self.reference})>"