diff --git a/app.py b/app.py index e0e6b23..fcdb4b3 100644 --- a/app.py +++ b/app.py @@ -12,11 +12,7 @@ from blueprints.log import log from blueprints.admin import admin # env -PG_HOST = os.getenv("PG_HOST", None) -PG_PORT = os.getenv("PG_PORT", None) -PG_NAME = os.getenv("PG_NAME", None) -PG_USER = os.getenv("PG_USER", None) -PG_PASS = os.getenv("PG_PASS", None) +PG_HOST = os.getenv("PG_HOST", None).strip() JWT_KEY = os.getenv("JWT_KEY", None) PLATFORM_ROOT_PASSWORD = os.getenv("PLATFORM_ROOT_PASSWORD", None) @@ -39,7 +35,7 @@ for s in settings: # Postgresql print("[*] Connecting to Database") -dbhelper.db = dbhelper.DB(create_engine('postgresql+psycopg2://%s:%s@%s:%s/%s'%(PG_USER, PG_PASS, PG_HOST, PG_PORT, PG_NAME))) +dbhelper.db = dbhelper.DB(create_engine(PG_HOST)) Base.metadata.create_all(dbhelper.db._engine) # root checker diff --git a/blueprints/admin.py b/blueprints/admin.py index 2df8336..3f5ea0f 100644 --- a/blueprints/admin.py +++ b/blueprints/admin.py @@ -1,7 +1,3 @@ -""" -not done -""" - import os import time import math @@ -12,7 +8,7 @@ from flask import Blueprint, request, jsonify, make_response, g, abort from bcrypt import hashpw, gensalt, checkpw from functools import wraps -from utils import pgclass, setting_loader, logger, dbhelper +from utils import pgclass, setting_loader, logger, dbhelper, ighelper from utils.misc import error, internal_json2protobuf from utils.platform_consts import PLIST, PLIST_ROOT from protobuf_files import niming_pb2 @@ -276,6 +272,10 @@ def article_pend(type:str, key:str): session.commit() # run IG Post + if type == 'a': + result, err = ighelper.request_upload(tg["id"]) + if err or result["result"] == "Canceled delete post request": + return abort(500) return "OK", 200 else: @@ -303,3 +303,32 @@ def setting_edit(): logger.logger("setting.modify", "User:%s modified settings: %s"%(opuser, json.dumps(request.json))) return jsonify(d), 200 + +#################### +# IGAPI Area # +#################### +@admin.route("/ig/accinfo", methods=["GET"]) +@role_required(["ig.accinfo"]) +def ig_accinfo(): + result, err = ighelper.request_account_info() + if err: + return jsonify(result), 500 + else: + return jsonify(result), 200 + + +@admin.route("/ig/login", methods=["GET"]) +@role_required(["ig.login"]) +def ig_login(): + result, err = ighelper.request_login() + if err: + return jsonify(result), 500 + else: + return jsonify(result), 200 + + +@admin.route("/ig/queue", methods=["GET"]) +@role_required(["ig.queue"]) +def ig_queue(): + result = ighelper.request_queue() + return jsonify(result), 200 diff --git a/protobuf_files/igapi.proto b/protobuf_files/igapi.proto new file mode 100644 index 0000000..eeff066 --- /dev/null +++ b/protobuf_files/igapi.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +service IGAPI { + rpc login (Request) returns (Reply) {} + + rpc account_info (Request) returns (Reply) {} + + rpc upload (Request) returns (Reply) {} + + rpc delete (Request) returns (Reply) {} + + rpc setting (Request) returns (Reply) {} + + rpc queue (Request) returns (Reply) {} +} + +message Request { + int64 code = 1; + repeated string args = 2; +} + +message Reply { + int64 err = 1; + map result = 2; +} \ No newline at end of file diff --git a/protobuf_files/igapi_pb2.py b/protobuf_files/igapi_pb2.py new file mode 100644 index 0000000..9c290bc --- /dev/null +++ b/protobuf_files/igapi_pb2.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: igapi.proto +# Protobuf Python Version: 5.28.1 +"""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, + 1, + '', + 'igapi.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0bigapi.proto\"%\n\x07Request\x12\x0c\n\x04\x63ode\x18\x01 \x01(\x03\x12\x0c\n\x04\x61rgs\x18\x02 \x03(\t\"g\n\x05Reply\x12\x0b\n\x03\x65rr\x18\x01 \x01(\x03\x12\"\n\x06result\x18\x02 \x03(\x0b\x32\x12.Reply.ResultEntry\x1a-\n\x0bResultEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x32\xc0\x01\n\x05IGAPI\x12\x1b\n\x05login\x12\x08.Request\x1a\x06.Reply\"\x00\x12\"\n\x0c\x61\x63\x63ount_info\x12\x08.Request\x1a\x06.Reply\"\x00\x12\x1c\n\x06upload\x12\x08.Request\x1a\x06.Reply\"\x00\x12\x1c\n\x06\x64\x65lete\x12\x08.Request\x1a\x06.Reply\"\x00\x12\x1d\n\x07setting\x12\x08.Request\x1a\x06.Reply\"\x00\x12\x1b\n\x05queue\x12\x08.Request\x1a\x06.Reply\"\x00\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'igapi_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_REPLY_RESULTENTRY']._loaded_options = None + _globals['_REPLY_RESULTENTRY']._serialized_options = b'8\001' + _globals['_REQUEST']._serialized_start=15 + _globals['_REQUEST']._serialized_end=52 + _globals['_REPLY']._serialized_start=54 + _globals['_REPLY']._serialized_end=157 + _globals['_REPLY_RESULTENTRY']._serialized_start=112 + _globals['_REPLY_RESULTENTRY']._serialized_end=157 + _globals['_IGAPI']._serialized_start=160 + _globals['_IGAPI']._serialized_end=352 +# @@protoc_insertion_point(module_scope) diff --git a/protobuf_files/igapi_pb2_grpc.py b/protobuf_files/igapi_pb2_grpc.py new file mode 100644 index 0000000..0964154 --- /dev/null +++ b/protobuf_files/igapi_pb2_grpc.py @@ -0,0 +1,312 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + +from protobuf_files import igapi_pb2 as igapi__pb2 + +GRPC_GENERATED_VERSION = '1.68.0' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in igapi_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class IGAPIStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.login = channel.unary_unary( + '/IGAPI/login', + request_serializer=igapi__pb2.Request.SerializeToString, + response_deserializer=igapi__pb2.Reply.FromString, + _registered_method=True) + self.account_info = channel.unary_unary( + '/IGAPI/account_info', + request_serializer=igapi__pb2.Request.SerializeToString, + response_deserializer=igapi__pb2.Reply.FromString, + _registered_method=True) + self.upload = channel.unary_unary( + '/IGAPI/upload', + request_serializer=igapi__pb2.Request.SerializeToString, + response_deserializer=igapi__pb2.Reply.FromString, + _registered_method=True) + self.delete = channel.unary_unary( + '/IGAPI/delete', + request_serializer=igapi__pb2.Request.SerializeToString, + response_deserializer=igapi__pb2.Reply.FromString, + _registered_method=True) + self.setting = channel.unary_unary( + '/IGAPI/setting', + request_serializer=igapi__pb2.Request.SerializeToString, + response_deserializer=igapi__pb2.Reply.FromString, + _registered_method=True) + self.queue = channel.unary_unary( + '/IGAPI/queue', + request_serializer=igapi__pb2.Request.SerializeToString, + response_deserializer=igapi__pb2.Reply.FromString, + _registered_method=True) + + +class IGAPIServicer(object): + """Missing associated documentation comment in .proto file.""" + + def login(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def account_info(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def upload(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def delete(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def setting(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def queue(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_IGAPIServicer_to_server(servicer, server): + rpc_method_handlers = { + 'login': grpc.unary_unary_rpc_method_handler( + servicer.login, + request_deserializer=igapi__pb2.Request.FromString, + response_serializer=igapi__pb2.Reply.SerializeToString, + ), + 'account_info': grpc.unary_unary_rpc_method_handler( + servicer.account_info, + request_deserializer=igapi__pb2.Request.FromString, + response_serializer=igapi__pb2.Reply.SerializeToString, + ), + 'upload': grpc.unary_unary_rpc_method_handler( + servicer.upload, + request_deserializer=igapi__pb2.Request.FromString, + response_serializer=igapi__pb2.Reply.SerializeToString, + ), + 'delete': grpc.unary_unary_rpc_method_handler( + servicer.delete, + request_deserializer=igapi__pb2.Request.FromString, + response_serializer=igapi__pb2.Reply.SerializeToString, + ), + 'setting': grpc.unary_unary_rpc_method_handler( + servicer.setting, + request_deserializer=igapi__pb2.Request.FromString, + response_serializer=igapi__pb2.Reply.SerializeToString, + ), + 'queue': grpc.unary_unary_rpc_method_handler( + servicer.queue, + request_deserializer=igapi__pb2.Request.FromString, + response_serializer=igapi__pb2.Reply.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'IGAPI', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('IGAPI', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class IGAPI(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def login(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/IGAPI/login', + igapi__pb2.Request.SerializeToString, + igapi__pb2.Reply.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def account_info(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/IGAPI/account_info', + igapi__pb2.Request.SerializeToString, + igapi__pb2.Reply.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def upload(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/IGAPI/upload', + igapi__pb2.Request.SerializeToString, + igapi__pb2.Reply.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def delete(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/IGAPI/delete', + igapi__pb2.Request.SerializeToString, + igapi__pb2.Reply.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def setting(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/IGAPI/setting', + igapi__pb2.Request.SerializeToString, + igapi__pb2.Reply.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def queue(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/IGAPI/queue', + igapi__pb2.Request.SerializeToString, + igapi__pb2.Reply.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/requirements.txt b/requirements.txt index 8ddb047..4ed249e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ python-magic bcrypt pytz sqlalchemy-utils -minio \ No newline at end of file +minio +grpcio \ No newline at end of file diff --git a/utils/dbhelper.py b/utils/dbhelper.py index d7472f6..f010880 100644 --- a/utils/dbhelper.py +++ b/utils/dbhelper.py @@ -10,7 +10,7 @@ from sqlalchemy.orm import sessionmaker from sqlalchemy import desc, update, Engine, text, delete import pytz -from utils import pgclass, setting_loader, s3helper, logger +from utils import pgclass, setting_loader, s3helper, logger, ighelper from utils.misc import error from protobuf_files import niming_pb2 @@ -43,12 +43,8 @@ def solo_article_uploader(content:str, file_list, fmimes:List[str]) -> Tuple[int # IP ip = request.remote_addr - # ig posting (only article) - if chk_before_post: - igid = None - # Go posting + # tmp igid igid = None - # Coming Soon... # mark if chk_before_post: mark = "pending" @@ -59,8 +55,8 @@ def solo_article_uploader(content:str, file_list, fmimes:List[str]) -> Tuple[int article_mark = pgclass.SQLmark article_metadata = pgclass.SQLmeta result_id = 0 - try: - with db.getsession() as session: + with db.getsession() as session: + try: # file processor fnlist, err = s3helper.multi_file_uploader(file_list, fmimes) if err: @@ -78,13 +74,19 @@ def solo_article_uploader(content:str, file_list, fmimes:List[str]) -> Tuple[int # commit session.commit() result_id = int(posta.id) + except: + return 0, "" - # logger - logger.logger("newpost", "New post (id=%d): %s"%(result_id, mark)) - - return result_id, hash - except: - return 0, "" + # 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 # 上傳單一留言 @@ -108,8 +110,8 @@ def solo_comment_uploader(content:str, ref:int) -> Tuple[int | str, str]: # posting article = pgclass.SQLarticle article_mark = pgclass.SQLmark - try: - with db.getsession() as session: + with db.getsession() as session: + try: # article processor cda = { "content":content, @@ -137,8 +139,8 @@ def solo_comment_uploader(content:str, ref:int) -> Tuple[int | str, str]: logger.logger("newcomment", "New comment %s points to %d: %s"%(sha1, ref, mark)) return sha1, hash - except Exception as e: - return 0, "" + except Exception as e: + return 0, "" # role (general) (owner) (admin) @@ -279,10 +281,12 @@ def multi_article_fetcher(role:str, page:str, count:int) -> Tuple[bytes, int]: # def solo_article_remover(role:str, hash:str=None, id:int=None, opuser:str=None) -> Tuple[Dict, int]: # admin, owner article = pgclass.SQLarticle article_mark = pgclass.SQLmark + article_meta = pgclass.SQLmeta with db.getsession() as session: # 獲取本體 - pres = session.query(article.id, article.hash, article_mark.mark, article.file_list) \ - .join(article_mark, article_mark.hash==article.hash) + 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) if role == "admin": pres = pres.filter(article.id == id).first() elif role == "owner": @@ -314,6 +318,14 @@ def solo_article_remover(role:str, hash:str=None, id:int=None, opuser:str=None) session.commit() + # 刪除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 + # logger logtype = "article.delete" if role == "admin" else "delpost" loguser = "User:%s "%opuser if role == "admin" else "" diff --git a/utils/ighelper.py b/utils/ighelper.py index c4971b2..6cb197e 100644 --- a/utils/ighelper.py +++ b/utils/ighelper.py @@ -1,2 +1,43 @@ -def ighelper(): - pass +from typing import Tuple +import os + +import grpc + +from protobuf_files import igapi_pb2_grpc +from protobuf_files.igapi_pb2 import Request, Reply + +IGAPI_HOST = os.getenv("IGAPI_HOST", None) + +def request_account_info() -> Tuple[dict, int]: + with grpc.insecure_channel(IGAPI_HOST) as channel: + stub = igapi_pb2_grpc.IGAPIStub(channel) + res = stub.account_info(Request()) + return dict(res.result.items()), res.err + + +def request_login() -> Tuple[dict, int]: + with grpc.insecure_channel(IGAPI_HOST) as channel: + stub = igapi_pb2_grpc.IGAPIStub(channel) + res = stub.login(Request()) + return dict(res.result.items()), res.err + + +def request_queue() -> dict: + with grpc.insecure_channel(IGAPI_HOST) as channel: + stub = igapi_pb2_grpc.IGAPIStub(channel) + res = stub.queue(Request()) + return dict(res.result.items()) + + +def request_upload(aid:int) -> Tuple[dict, int]: + with grpc.insecure_channel(IGAPI_HOST) as channel: + stub = igapi_pb2_grpc.IGAPIStub(channel) + res = stub.upload(Request(code=aid)) + return dict(res.result.items()), res.err + + +def request_delete(aid:int, code:str) -> Tuple[dict, int]: + with grpc.insecure_channel(IGAPI_HOST) as channel: + stub = igapi_pb2_grpc.IGAPIStub(channel) + res = stub.delete(Request(code=aid, args=[code])) + return dict(res.result.items()), res.err diff --git a/utils/platform_consts.py b/utils/platform_consts.py index 9bac186..8686af7 100644 --- a/utils/platform_consts.py +++ b/utils/platform_consts.py @@ -1,5 +1,6 @@ # Permission List -PLIST = ["article.read", "article.pend", "article.del", "setting.edit"] # no permission:usermgr except root +PLIST = ["article.read", "article.pend", "article.del", "setting.edit", + "ig.accinfo", "ig.login", "ig.queue"] # no permission:usermgr except root PLIST_ROOT = PLIST + ["usermgr"] # event type