diff --git a/api/__init__.py b/api/__init__.py index 2d98b92..b1423f0 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -41,20 +41,19 @@ def get_jwks(): def get_public_key_for_kid(kid): global _public_key_cache - if kid in _public_key_cache: - return _public_key_cache[kid] - jwks = get_jwks() - res = [] - for key_data in jwks["keys"]: - if key_data["kid"] == kid and key_data["use"] == "sig" and key_data["alg"] == "RS256" and key_data["kty"] == "RSA": - x5c = key_data["x5c"][0] - pem_public_key = x5c_to_public_key(x5c) - _public_key_cache[kid] = pem_public_key - res.append(pem_public_key) - if len(res) > 0: - print(len(res)) - return res[0] - + with _lock: + if kid in _public_key_cache: + return _public_key_cache[kid] + jwks = get_jwks() + res = [] + for key_data in jwks["keys"]: + if key_data["kid"] == kid and key_data["use"] == "sig" and key_data["alg"] == "RS256" and key_data["kty"] == "RSA": + x5c = key_data["x5c"][0] + pem_public_key = x5c_to_public_key(x5c) + _public_key_cache[kid] = pem_public_key + res.append(pem_public_key) + if len(res) > 0: + return res[0] return None diff --git a/api/log.py b/api/log.py index 39afb25..166078f 100644 --- a/api/log.py +++ b/api/log.py @@ -41,3 +41,4 @@ def create_log(): extra = data.get('extra', None) log_entry = Log(level=level, message=message, application=application, extra=extra) insert_log(log_entry) + return jsonify({"message": "log created"}), 201 diff --git a/api/markdown.py b/api/markdown.py index 0607e52..fe6fe1b 100644 --- a/api/markdown.py +++ b/api/markdown.py @@ -1,12 +1,13 @@ from flask import Blueprint, request, jsonify from sqlalchemy import or_ -import api +from api import limiter from api import require_auth, etag_response from contexts.RequestContext import RequestContext from db import get_db from db.models.Markdown import Markdown +from events import markdown_created, markdown_updated, markdown_deleted +import api import logging -from api import limiter logger = logging.getLogger(__name__) markdown_bp = Blueprint('markdown', __name__, url_prefix='/api/markdown') @@ -79,6 +80,7 @@ def create_markdown(): return jsonify({"error": "duplicate shortcut"}), 400 session.add(new_markdown) session.commit() + markdown_created.send(None, new_markdown.to_dict()) return jsonify(new_markdown.to_dict()), 201 except Exception as e: logger.error(f"failed to create markdown: {e}") @@ -118,6 +120,7 @@ def update_markdown(markdown_id): if 'shortcut' in data: markdown.shortcut = data.get('shortcut') session.commit() + markdown_updated.send(None, markdown.to_dict()) return jsonify(markdown.to_dict()), 200 @markdown_bp.route('/', methods=['DELETE']) @@ -130,8 +133,10 @@ def delete_markdown(markdown_id): logger.error(f"failed to delete markdown: {markdown_id}") errno = RequestContext.get_error_id() return jsonify({"error": f"file not found - {errno}"}), 404 + md = markdown.to_dict() session.delete(markdown) session.commit() + markdown_deleted.send(None, md) return jsonify({"message": "deleted"}), 200 @@ -150,6 +155,7 @@ def move_forward(markdown_id): previous_markdown = siblings[current_index - 1] markdown.order, previous_markdown.order = previous_markdown.order, markdown.order session.commit() + markdown_updated.send(None, markdown.to_dict()) return jsonify(markdown.to_dict()), 200 @@ -167,8 +173,9 @@ def move_backward(markdown_id): return jsonify({"error": "already at the last position"}), 400 next_markdown = siblings[current_index + 1] - markdown.order, next_markdown.order = next_markdown.order, next_markdown.order + markdown.order, next_markdown.order = next_markdown.order, markdown.order session.commit() + markdown_updated.send(None, markdown.to_dict()) return jsonify(markdown.to_dict()), 200 @markdown_bp.route('/search/', methods=['GET']) diff --git a/api/path.py b/api/path.py index a639017..741ba7a 100644 --- a/api/path.py +++ b/api/path.py @@ -7,6 +7,9 @@ from db.models.Markdown import Markdown from db.models.Path import Path from api import limiter import logging + +from events import path_created, path_updated, path_deleted + logger = logging.getLogger(__name__) path_bp = Blueprint('path', __name__, url_prefix='/api/path') @@ -52,6 +55,7 @@ def create_path(): new_path = Path(name=data['name'], parent_id=data['parent_id']) session.add(new_path) session.commit() + path_created.send(None, new_path.to_dict()) return jsonify(new_path.to_dict()), 201 @path_bp.route('/', methods=['PUT']) @@ -70,6 +74,7 @@ def update_path(path_id): path.name = data['name'] path.parent_id = data['parent_id'] session.commit() + path_updated.send(None, path.to_dict()) return jsonify(path.to_dict()), 200 @path_bp.route('/', methods=['PATCH']) @@ -91,6 +96,7 @@ def patch_path(path_id): path.name = updated_name path.parent_id = updated_parent_id session.commit() + path_updated.send(None, path.to_dict()) return jsonify(path.to_dict()), 200 @@ -106,8 +112,10 @@ def delete_path(path_id): return jsonify({"error": "can not delete non empty path"}), 409 if session.query(Markdown).filter_by(path_id=path_id).first(): return jsonify({"error": "can not delete non empty path"}), 409 + pth = path.to_dict() session.delete(path) session.commit() + path_deleted.send(None, pth) return jsonify({"message": "path deleted"}), 200 @@ -126,6 +134,7 @@ def move_forward(path_id): previous_path = siblings[current_index - 1] path.order, previous_path.order = previous_path.order, path.order session.commit() + path_updated.send(None, path.to_dict()) return jsonify(path.to_dict()), 200 @@ -145,5 +154,6 @@ def move_backward(path_id): next_path = siblings[current_index + 1] path.order, next_path.order = next_path.order, path.order session.commit() + path_updated.send(None, path.to_dict()) return jsonify(path.to_dict()), 200 diff --git a/api/webhook.py b/api/webhook.py new file mode 100644 index 0000000..63885d3 --- /dev/null +++ b/api/webhook.py @@ -0,0 +1,152 @@ +from flask import Blueprint, jsonify, request +from api import require_auth, limiter +from db import get_db +from db.models.Webhook import Webhook +from db.models.WebhookSetting import WebhookSetting + +webhook_bp = Blueprint('webhook', __name__, url_prefix='/api/webhook') + + +@webhook_bp.route('/', methods=['GET']) +@require_auth(roles=['admin']) +def list_webhooks(): + with get_db() as session: + hooks = session.query(Webhook).all() + return jsonify([h.to_dict() for h in hooks]), 200 + + +@webhook_bp.route('/', methods=['POST']) +@require_auth(['admin']) +def create_webhook(): + data = request.json + hook_url = data.get('hook_url') + if not hook_url: + return jsonify({'error': 'hook_url required'}), 400 + + with get_db() as session: + existing = session.query(Webhook).filter_by(hook_url=hook_url).first() + if existing: + return jsonify({'error': 'Webhook URL already exists'}), 409 + webhook = Webhook(hook_url=hook_url) + session.add(webhook) + session.commit() + return jsonify(webhook.to_dict()), 201 + + + + + +@webhook_bp.route('/', methods=['PUT', 'PATCH']) +@require_auth(roles=['admin']) +def update_webhook(webhook_id): + data = request.json + if 'hook_url' not in data: + return jsonify({'error': 'hook_url is required'}), 400 + + with get_db() as session: + webhook = session.query(Webhook).get(webhook_id) + if not webhook: + return jsonify({'error': 'Webhook not found'}), 404 + + existing = session.query(Webhook).filter_by(hook_url=data['hook_url']).filter(Webhook.id != webhook_id).first() + if existing: + return jsonify({'error': 'Webhook URL already exists'}), 409 + + webhook.hook_url = data['hook_url'] + session.commit() + return jsonify(webhook.to_dict()), 200 + + +@webhook_bp.route('/', methods=['DELETE']) +@require_auth(roles=['admin']) +def delete_webhook(webhook_id): + with get_db() as session: + webhook = session.query(Webhook).get(webhook_id) + if not webhook: + return jsonify({'error': 'Webhook not found'}), 404 + + session.delete(webhook) + session.commit() + return jsonify({'message': 'Webhook deleted'}), 200 + + + +@webhook_bp.route('/setting/', methods=['GET']) +@require_auth(roles=['admin']) +def list_webhook_settings(): + with get_db() as session: + settings = session.query(WebhookSetting).all() + return jsonify([s.to_dict() for s in settings]), 200 + +@webhook_bp.route('/setting/', methods=['GET']) +@require_auth(roles=['admin']) +def webhook_setting(setting_id): + with get_db() as session: + setting = session.query(WebhookSetting).filter(WebhookSetting.id == setting_id).first() + if not setting: + return jsonify({'info': 'Webhook setting not found'}), 204 + return jsonify(setting.to_dict()), 200 + + +@webhook_bp.route('/setting/path/', methods=['GET']) +@require_auth(roles=['admin']) +def webhook_setting_by_path(path_id): + with get_db() as session: + setting = session.query(WebhookSetting).filter(WebhookSetting.path_id == path_id).first() + if not setting: + return jsonify({'info': 'Webhook setting not found'}), 204 + return jsonify(setting.to_dict()), 200 + +@webhook_bp.route('/setting/', methods=['POST']) +@require_auth(roles=['admin']) +def create_webhook_setting(): + data = request.json + print(data) + required = ['path_id', 'webhook_id'] + for key in required: + if key not in data: + return jsonify({'error': 'Required field missing'}), 400 + with get_db() as session: + setting = WebhookSetting( + path_id=data.get('path_id'), + webhook_id=data.get('webhook_id'), + recursive=data.get('recursive', False), + additional_header=data.get('additional_header', ''), + enabled=data.get('enabled', True), + on_events=data.get('on_events', 1), + ) + session.add(setting) + session.commit() + return jsonify(setting.to_dict()), 201 +@webhook_bp.route('/setting/', methods=['PUT', 'PATCH']) +@require_auth(roles=['admin']) +def update_webhook_setting(setting_id): + data = request.json + with get_db() as session: + setting = session.query(WebhookSetting).get(setting_id) + if not setting: + return jsonify({'error': 'Webhook setting not found'}), 404 + + if 'recursive' in data: + setting.recursive = data['recursive'] + if 'additional_header' in data: + setting.additional_header = data['additional_header'] + if 'enabled' in data: + setting.enabled = data['enabled'] + if 'on_events' in data: + setting.on_events = data['on_events'] + + session.commit() + return jsonify(setting.to_dict()), 200 + +@webhook_bp.route('/setting/', methods=['DELETE']) +@require_auth(roles=['admin']) +def delete_webhook_setting(setting_id): + with get_db() as session: + setting = session.query(WebhookSetting).get(setting_id) + if not setting: + return jsonify({'error': 'Webhook setting not found'}), 404 + + session.delete(setting) + session.commit() + return jsonify({'message': 'Webhook setting deleted'}), 200 diff --git a/db/__init__.py b/db/__init__.py index d47eb99..4883edb 100644 --- a/db/__init__.py +++ b/db/__init__.py @@ -1,5 +1,3 @@ -import os -import subprocess from contextlib import contextmanager from sqlalchemy.schema import CreateTable @@ -21,15 +19,6 @@ def get_db(): finally: db.close() -# def dump_db(): -# try: -# os.environ['MYSQL_PWD'] = DB_PASSWORD -# dump_cmd = f"mysqldump --no-tablespaces -h {DB_HOST} -P {DB_PORT} -u {DB_USER} {DB_NAME} > /app/dump/db_dump.sql" -# subprocess.run(dump_cmd, shell=True, check=True) -# except subprocess.CalledProcessError as e: -# print(f"Failed to dump database: {e}") -# raise e - def clear_db(): with engine.connect() as conn: conn.execute(text("SET FOREIGN_KEY_CHECKS = 0;")) @@ -60,9 +49,9 @@ def init_payload(): for model in table_models: print(str(CreateTable(model.__table__))) - print(f"MODEL -- {model}, {hasattr(model, '__pay_load__')}") - if hasattr(model, "__pay_load__"): - payload = model.__pay_load__[ENVIRONMENT] + print(f"MODEL -- {model}, {hasattr(model, '__payload__')}") + if hasattr(model, "__payload__"): + payload = model.__payload__[ENVIRONMENT] print(f"- - [ - ] hasattr, {ENVIRONMENT} - {payload}") stmt = insert(model.__table__).values(payload).prefix_with("IGNORE") print(f"- - [ - ] {stmt}\n") @@ -73,11 +62,6 @@ def init_payload(): def setup_db(): if DB_SCHEMA_UPDATED: - # try: - # dump_db() - # print("[ x ] db dumped") - # except Exception as e: - # print(f"[ x ] Failed to dump database: {e}") clear_db() print("[ x ] db cleared") create_all() diff --git a/db/models/Markdown.py b/db/models/Markdown.py index 35ac1e7..f13337c 100644 --- a/db/models/Markdown.py +++ b/db/models/Markdown.py @@ -9,7 +9,7 @@ class Markdown(Base): id = Column(Integer, primary_key=True) title = Column(String(255), nullable=False) content = Column(Text, nullable=False) - created_at = Column(DateTime, default=datetime.datetime.utcnow) + created_at = Column(DateTime, default=datetime.datetime.now(datetime.UTC)) path_id = Column(Integer, ForeignKey('path.id'), nullable=False) order = Column(String(36), default=lambda: str(uuid.uuid4())) shortcut = Column(String(36), default="") @@ -23,7 +23,7 @@ class Markdown(Base): 'order': self.order, 'shortcut': self.shortcut, } - __pay_load__ = { + __payload__ = { 'dev': [ {'id': 1, 'title': 'index', 'content': ' ', 'created_at': datetime.datetime.utcnow, 'path_id': 1 }, ], diff --git a/db/models/Path.py b/db/models/Path.py index 7dd17bb..f56d368 100644 --- a/db/models/Path.py +++ b/db/models/Path.py @@ -19,7 +19,7 @@ class Path(Base): "order": self.order, } - __pay_load__ = { + __payload__ = { 'dev': [ {'id': 1, 'name': '', 'parent_id': None}, {'id': 2, 'name': 'test', 'parent_id': 1}, diff --git a/db/models/Webhook.py b/db/models/Webhook.py new file mode 100644 index 0000000..2e129d4 --- /dev/null +++ b/db/models/Webhook.py @@ -0,0 +1,16 @@ + +from sqlalchemy import Column, String, Integer, ForeignKey, UniqueConstraint, text, Boolean +from db.models import Base + + +class Webhook(Base): + __tablename__ = 'webhook' + id = Column(Integer, primary_key=True, autoincrement=True) + hook_url = Column(String(100), nullable=False) + __table_args__ = (UniqueConstraint('hook_url'),) + + def to_dict(self): + return { + "id": self.id, + "hook_url": self.hook_url + } \ No newline at end of file diff --git a/db/models/WebhookSetting.py b/db/models/WebhookSetting.py new file mode 100644 index 0000000..6974fa5 --- /dev/null +++ b/db/models/WebhookSetting.py @@ -0,0 +1,26 @@ +from sqlalchemy import Column, String, Integer, ForeignKey, UniqueConstraint, Boolean +from db.models import Base + + +class WebhookSetting(Base): + __tablename__ = 'webhook_setting' + id = Column(Integer, primary_key=True, autoincrement=True) + path_id = Column(Integer, ForeignKey('path.id'), nullable=False) + webhook_id = Column(Integer, ForeignKey('webhook.id'), nullable=False) + recursive = Column(Boolean, default=False) + additional_header = Column(String(500), nullable=True) + enabled = Column(Boolean, default=True) + on_events = Column(Integer, default=0) + + __table_args__ = (UniqueConstraint('webhook_id', 'path_id', name='_webhook_path_uc'),) + + def to_dict(self): + return { + "id": self.id, + "path_id": self.path_id, + "webhook_id": self.webhook_id, + "recursive": self.recursive, + "additional_header": self.additional_header, + "enabled": self.enabled, + "on_events": self.on_events, + } diff --git a/db/models/__init__.py b/db/models/__init__.py index f5ed4e5..02b6ea2 100644 --- a/db/models/__init__.py +++ b/db/models/__init__.py @@ -1,8 +1,6 @@ -#db/models/__init__.py - import pkgutil import importlib -from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import declarative_base Base = declarative_base() package_name = "db.models" diff --git a/db/utils.py b/db/utils.py index 5284e55..fe89ae1 100644 --- a/db/utils.py +++ b/db/utils.py @@ -1,4 +1,3 @@ -#db/utils.py from db import get_db def insert_log(log_entry): diff --git a/events/WebhookEventHandlers/MarkdownWebhookEventHandlers/MarkdownCreatedWebhookEventHandler.py b/events/WebhookEventHandlers/MarkdownWebhookEventHandlers/MarkdownCreatedWebhookEventHandler.py new file mode 100644 index 0000000..0c807e4 --- /dev/null +++ b/events/WebhookEventHandlers/MarkdownWebhookEventHandlers/MarkdownCreatedWebhookEventHandler.py @@ -0,0 +1,20 @@ +from events import MARKDOWN_CREATED_EVENT, markdown_created +from events.WebhookEventHandlers import auto_instantiate +from events.WebhookEventHandlers.MarkdownWebhookEventHandlers import MarkdownWebhookEventHandler +from misc import Singleton + + +@auto_instantiate +class MarkdownCreatedWebhookEventHandler(MarkdownWebhookEventHandler, Singleton): + def __init__(self): + if getattr(self, "_initialized", False): + return + super().__init__(MARKDOWN_CREATED_EVENT) + markdown_created.connect(self) + self._initialized = True + + + + + + diff --git a/events/WebhookEventHandlers/MarkdownWebhookEventHandlers/MarkdownDeletedWebhookEventHandler.py b/events/WebhookEventHandlers/MarkdownWebhookEventHandlers/MarkdownDeletedWebhookEventHandler.py new file mode 100644 index 0000000..da71d4b --- /dev/null +++ b/events/WebhookEventHandlers/MarkdownWebhookEventHandlers/MarkdownDeletedWebhookEventHandler.py @@ -0,0 +1,15 @@ +from events import MARKDOWN_DELETED_EVENT, markdown_deleted +from events.WebhookEventHandlers import auto_instantiate +from events.WebhookEventHandlers.MarkdownWebhookEventHandlers import MarkdownWebhookEventHandler +from misc import Singleton + + +@auto_instantiate +class MarkdownDeletedWebhookEventHandler(MarkdownWebhookEventHandler, Singleton): + def __init__(self): + if getattr(self, "_initialized", False): + return + super().__init__(MARKDOWN_DELETED_EVENT) + markdown_deleted.connect(self) + self._initialized = True + diff --git a/events/WebhookEventHandlers/MarkdownWebhookEventHandlers/MarkdownUpdatedWebhookEventHandler.py b/events/WebhookEventHandlers/MarkdownWebhookEventHandlers/MarkdownUpdatedWebhookEventHandler.py new file mode 100644 index 0000000..9037dae --- /dev/null +++ b/events/WebhookEventHandlers/MarkdownWebhookEventHandlers/MarkdownUpdatedWebhookEventHandler.py @@ -0,0 +1,20 @@ +from events import MARKDOWN_UPDATED_EVENT, markdown_updated +from events.WebhookEventHandlers import auto_instantiate +from events.WebhookEventHandlers.MarkdownWebhookEventHandlers import MarkdownWebhookEventHandler +from misc import Singleton + + +@auto_instantiate +class MarkdownUpdatedWebhookEventHandler(MarkdownWebhookEventHandler, Singleton): + def __init__(self): + if getattr(self, "_initialized", False): + return + super().__init__(MARKDOWN_UPDATED_EVENT) + markdown_updated.connect(self) + self._initialized = True + + + + + + diff --git a/events/WebhookEventHandlers/MarkdownWebhookEventHandlers/__init__.py b/events/WebhookEventHandlers/MarkdownWebhookEventHandlers/__init__.py new file mode 100644 index 0000000..2158ff7 --- /dev/null +++ b/events/WebhookEventHandlers/MarkdownWebhookEventHandlers/__init__.py @@ -0,0 +1,9 @@ +from events.WebhookEventHandlers import WebhookEventHandler + + +class MarkdownWebhookEventHandler(WebhookEventHandler): + def __init__(self, event_type): + super().__init__(event_type) + + def get_path_id(self, payload): + return payload["path_id"] \ No newline at end of file diff --git a/events/WebhookEventHandlers/PathWebhookEventHandlers/PathCreatedWebhookEventHandler.py b/events/WebhookEventHandlers/PathWebhookEventHandlers/PathCreatedWebhookEventHandler.py new file mode 100644 index 0000000..d592c7a --- /dev/null +++ b/events/WebhookEventHandlers/PathWebhookEventHandlers/PathCreatedWebhookEventHandler.py @@ -0,0 +1,26 @@ +from events import PATH_CREATED_EVENT, path_created +from events.WebhookEventHandlers import auto_instantiate +from events.WebhookEventHandlers.PathWebhookEventHandlers import PathWebhookEventHandler +from misc import Singleton + + +@auto_instantiate +class PathCreatedWebhookEventHandler(PathWebhookEventHandler,Singleton): + + _instance = None + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super(PathCreatedWebhookEventHandler, cls).__new__(cls) + return cls._instance + def __init__(self): + if getattr(self, "_initialized", False): + return + super().__init__(PATH_CREATED_EVENT) + path_created.connect(self) + self._initialized = True + + + + + + diff --git a/events/WebhookEventHandlers/PathWebhookEventHandlers/PathDeletedWebhookEventHandler.py b/events/WebhookEventHandlers/PathWebhookEventHandlers/PathDeletedWebhookEventHandler.py new file mode 100644 index 0000000..3874d92 --- /dev/null +++ b/events/WebhookEventHandlers/PathWebhookEventHandlers/PathDeletedWebhookEventHandler.py @@ -0,0 +1,20 @@ +from events import PATH_DELETED_EVENT, path_deleted +from events.WebhookEventHandlers import auto_instantiate +from events.WebhookEventHandlers.PathWebhookEventHandlers import PathWebhookEventHandler +from misc import Singleton + + +@auto_instantiate +class PathDeletedWebhookEventHandler(PathWebhookEventHandler, Singleton): + _instance = None + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super(PathDeletedWebhookEventHandler, cls).__new__(cls) + return cls._instance + def __init__(self): + if getattr(self, "_initialized", False): + return + super().__init__(PATH_DELETED_EVENT) + path_deleted.connect(self) + self._initialized = True + diff --git a/events/WebhookEventHandlers/PathWebhookEventHandlers/PathUpdatedWebhookEventHandler.py b/events/WebhookEventHandlers/PathWebhookEventHandlers/PathUpdatedWebhookEventHandler.py new file mode 100644 index 0000000..05c0c11 --- /dev/null +++ b/events/WebhookEventHandlers/PathWebhookEventHandlers/PathUpdatedWebhookEventHandler.py @@ -0,0 +1,26 @@ +from events import PATH_UPDATED_EVENT, path_updated +from events.WebhookEventHandlers import auto_instantiate +from events.WebhookEventHandlers.PathWebhookEventHandlers import PathWebhookEventHandler +from misc import Singleton + + +@auto_instantiate +class PathUpdatedWebhookEventHandler(PathWebhookEventHandler, Singleton): + + _instance = None + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super(PathUpdatedWebhookEventHandler, cls).__new__(cls) + return cls._instance + def __init__(self): + if getattr(self, "_initialized", False): + return + super().__init__(PATH_UPDATED_EVENT) + path_updated.connect(self) + self._initialized = True + + + + + + diff --git a/events/WebhookEventHandlers/PathWebhookEventHandlers/__init__.py b/events/WebhookEventHandlers/PathWebhookEventHandlers/__init__.py new file mode 100644 index 0000000..9a5202b --- /dev/null +++ b/events/WebhookEventHandlers/PathWebhookEventHandlers/__init__.py @@ -0,0 +1,9 @@ +from events.WebhookEventHandlers import WebhookEventHandler + + +class PathWebhookEventHandler(WebhookEventHandler): + def __init__(self, event_type): + super().__init__(event_type) + + def get_path_id(self, payload): + return payload["id"] diff --git a/events/WebhookEventHandlers/__init__.py b/events/WebhookEventHandlers/__init__.py new file mode 100644 index 0000000..21ef414 --- /dev/null +++ b/events/WebhookEventHandlers/__init__.py @@ -0,0 +1,69 @@ +from sqlalchemy.orm import Session +from db.models.Path import Path +from db.models.Webhook import Webhook +from db.models.WebhookSetting import WebhookSetting +import abc +import importlib +import json +import pkgutil +import requests +import db + +class WebhookEventHandler(abc.ABC): + def __init__(self, event_type=0): + self.event_type = event_type + + @abc.abstractmethod + def get_path_id(self, payload): + pass + + def __call__(self, payload): + path_id = self.get_path_id(payload) + with db.get_db() as session: + setting = self.get_setting(session, path_id) + if setting is None: + return + headers = {'Content-Type': 'application/json'} + if setting["additional_headers"] is not None: + headers.update(json.loads(setting["additional_headers"])) + try: + response = requests.post(setting["webhook_url"], json=payload, headers=headers, timeout=5) + response.raise_for_status() + except requests.RequestException as e: + print(e) + + def get_setting(self, session: Session, path_id): + + path = session.query(Path).filter(Path.id == path_id).first() + if path is None: + return None + p = path.to_dict() + webhook_setting = session.query(WebhookSetting).filter(WebhookSetting.path_id == path_id).first() + + if webhook_setting is None and p["parent_id"] != 1: + return self.get_setting(session, p["parent_id"]) + + setting = webhook_setting.to_dict() + if not setting["enabled"] or setting["on_events"] & self.event_type == 0: + return None + webhook = session.query(Webhook).filter(Webhook.id == webhook_setting.webhook_id).first() + if webhook is None: + return None + setting["webhook_url"] = webhook.to_dict()["hook_url"] + return setting + +_auto_instantiate_classes = set() + +def auto_instantiate(cls): + _auto_instantiate_classes.add(cls) + return cls + +def register_all_webhook_event_handlers(): + package = __name__ + package_path = __path__ + + for finder, name, ispkg in pkgutil.walk_packages(package_path, package+"."): + importlib.import_module(name) + + for cls in _auto_instantiate_classes: + cls() diff --git a/events/__init__.py b/events/__init__.py new file mode 100644 index 0000000..9148a57 --- /dev/null +++ b/events/__init__.py @@ -0,0 +1,17 @@ +from blinker import Namespace + +signals = Namespace() + +MARKDOWN_CREATED_EVENT = 1 +MARKDOWN_UPDATED_EVENT = 2 +MARKDOWN_DELETED_EVENT = 4 +PATH_CREATED_EVENT = 8 +PATH_UPDATED_EVENT = 16 +PATH_DELETED_EVENT = 32 + +markdown_created = signals.signal('markdown_created') +markdown_updated = signals.signal('markdown_updated') +markdown_deleted = signals.signal('markdown_deleted') +path_created = signals.signal('path_created') +path_updated = signals.signal('path_updated') +path_deleted = signals.signal('path_deleted') diff --git a/misc/__init__.py b/misc/__init__.py new file mode 100644 index 0000000..a25d82f --- /dev/null +++ b/misc/__init__.py @@ -0,0 +1,7 @@ +class Singleton: + _instances = {} + + def __new__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__new__(cls) + return cls._instances[cls]