2025-08-25 16:06:41 +08:00
|
|
|
import json
|
|
|
|
import threading
|
|
|
|
import time
|
|
|
|
import urllib.parse
|
2025-08-20 16:47:07 +08:00
|
|
|
import uuid
|
2025-09-14 15:38:19 +08:00
|
|
|
from datetime import datetime, timedelta
|
2025-08-20 16:47:07 +08:00
|
|
|
|
2025-08-25 16:06:41 +08:00
|
|
|
import requests
|
|
|
|
from pydantic import BaseModel, field_serializer
|
2025-08-20 16:47:07 +08:00
|
|
|
import sqlalchemy as db
|
2025-09-19 20:55:47 +02:00
|
|
|
from .ConnectionString import ConnectionString, DEFAULT_DB
|
2025-09-13 08:23:54 +08:00
|
|
|
from flask import current_app
|
2025-08-20 16:47:07 +08:00
|
|
|
|
|
|
|
WebHookActions = ['peer_created', 'peer_deleted', 'peer_updated']
|
|
|
|
class WebHook(BaseModel):
|
2025-08-22 18:26:31 +08:00
|
|
|
WebHookID: str = ''
|
2025-08-20 16:47:07 +08:00
|
|
|
PayloadURL: str = ''
|
2025-08-22 18:26:31 +08:00
|
|
|
ContentType: str = 'application/json'
|
|
|
|
Headers: dict[str, dict[str, str]] = {}
|
2025-08-20 16:47:07 +08:00
|
|
|
VerifySSL: bool = True
|
|
|
|
SubscribedActions: list[str] = WebHookActions
|
|
|
|
IsActive: bool = True
|
2025-08-22 18:26:31 +08:00
|
|
|
CreationDate: datetime = ''
|
2025-08-20 16:47:07 +08:00
|
|
|
Notes: str = ''
|
|
|
|
|
2025-08-25 16:06:41 +08:00
|
|
|
class WebHookSessionLog(BaseModel):
|
|
|
|
LogTime: datetime
|
|
|
|
Status: int
|
|
|
|
Message: str = ''
|
|
|
|
|
|
|
|
@field_serializer('LogTime')
|
|
|
|
def logTimeSerializer(self, LogTime: datetime):
|
|
|
|
return LogTime.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
|
|
|
class WebHookSessionLogs(BaseModel):
|
|
|
|
Logs: list[WebHookSessionLog] = []
|
|
|
|
|
|
|
|
def addLog(self, status: int, message: str):
|
|
|
|
self.Logs.append(WebHookSessionLog(LogTime=datetime.now(), Status=status, Message=message))
|
|
|
|
|
2025-08-20 16:47:07 +08:00
|
|
|
class DashboardWebHooks:
|
|
|
|
def __init__(self, DashboardConfig):
|
2025-09-19 20:55:47 +02:00
|
|
|
self.engine = db.create_engine(ConnectionString(DEFAULT_DB))
|
2025-08-20 16:47:07 +08:00
|
|
|
self.metadata = db.MetaData()
|
|
|
|
self.webHooksTable = db.Table(
|
|
|
|
'DashboardWebHooks', self.metadata,
|
|
|
|
db.Column('WebHookID', db.String(255), nullable=False, primary_key=True),
|
|
|
|
db.Column('PayloadURL', db.Text, nullable=False),
|
|
|
|
db.Column('ContentType', db.String(255), nullable=False),
|
2025-08-22 18:26:31 +08:00
|
|
|
db.Column('Headers', db.JSON),
|
2025-08-20 16:47:07 +08:00
|
|
|
db.Column('VerifySSL', db.Boolean, nullable=False),
|
2025-08-22 18:26:31 +08:00
|
|
|
db.Column('SubscribedActions', db.JSON),
|
2025-08-20 16:47:07 +08:00
|
|
|
db.Column('IsActive', db.Boolean, nullable=False),
|
2025-08-22 18:26:31 +08:00
|
|
|
db.Column('CreationDate',
|
|
|
|
(db.DATETIME if DashboardConfig.GetConfig("Database", "type")[1] == 'sqlite' else db.TIMESTAMP),
|
|
|
|
server_default=db.func.now(),
|
|
|
|
nullable=False),
|
2025-08-20 16:47:07 +08:00
|
|
|
db.Column('Notes', db.Text),
|
|
|
|
extend_existing=True
|
|
|
|
)
|
2025-08-25 16:06:41 +08:00
|
|
|
self.webHookSessionsTable = db.Table(
|
|
|
|
'DashboardWebHookSessions', self.metadata,
|
|
|
|
db.Column('WebHookSessionID', db.String(255), nullable=False, primary_key=True),
|
|
|
|
db.Column('WebHookID', db.String(255), nullable=False),
|
|
|
|
db.Column('StartDate',
|
|
|
|
(db.DATETIME if DashboardConfig.GetConfig("Database", "type")[1] == 'sqlite' else db.TIMESTAMP),
|
|
|
|
server_default=db.func.now(),
|
|
|
|
nullable=False
|
|
|
|
),
|
|
|
|
db.Column('EndDate',
|
|
|
|
(db.DATETIME if DashboardConfig.GetConfig("Database", "type")[1] == 'sqlite' else db.TIMESTAMP),
|
|
|
|
),
|
2025-08-26 00:41:37 +08:00
|
|
|
db.Column('Data', db.JSON),
|
2025-08-25 16:06:41 +08:00
|
|
|
db.Column('Status', db.INTEGER),
|
|
|
|
db.Column('Logs', db.JSON)
|
|
|
|
)
|
|
|
|
|
2025-08-20 16:47:07 +08:00
|
|
|
self.metadata.create_all(self.engine)
|
|
|
|
self.WebHooks: list[WebHook] = []
|
2025-09-14 15:38:19 +08:00
|
|
|
|
|
|
|
with self.engine.begin() as conn:
|
|
|
|
conn.execute(
|
|
|
|
self.webHookSessionsTable.update().values({
|
|
|
|
"EndDate": datetime.now(),
|
|
|
|
"Status": 2
|
|
|
|
}).where(
|
|
|
|
self.webHookSessionsTable.c.Status == -1
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2025-08-20 16:47:07 +08:00
|
|
|
self.__getWebHooks()
|
|
|
|
|
|
|
|
def __getWebHooks(self):
|
|
|
|
with self.engine.connect() as conn:
|
|
|
|
webhooks = conn.execute(
|
2025-08-22 18:26:31 +08:00
|
|
|
self.webHooksTable.select().order_by(
|
|
|
|
self.webHooksTable.c.CreationDate
|
|
|
|
)
|
2025-08-20 16:47:07 +08:00
|
|
|
).mappings().fetchall()
|
|
|
|
self.WebHooks.clear()
|
|
|
|
self.WebHooks = [WebHook(**webhook) for webhook in webhooks]
|
2025-08-22 18:26:31 +08:00
|
|
|
|
|
|
|
def GetWebHooks(self):
|
|
|
|
self.__getWebHooks()
|
|
|
|
return list(map(lambda x : x.model_dump(), self.WebHooks))
|
2025-08-20 16:47:07 +08:00
|
|
|
|
2025-08-26 00:41:37 +08:00
|
|
|
def GetWebHookSessions(self, webHook: WebHook):
|
|
|
|
with self.engine.connect() as conn:
|
|
|
|
sessions = conn.execute(
|
|
|
|
self.webHookSessionsTable.select().where(
|
|
|
|
self.webHookSessionsTable.c.WebHookID == webHook.WebHookID
|
|
|
|
).order_by(
|
|
|
|
db.desc(self.webHookSessionsTable.c.StartDate)
|
|
|
|
)
|
|
|
|
).mappings().fetchall()
|
|
|
|
return sessions
|
|
|
|
|
2025-08-22 18:26:31 +08:00
|
|
|
def CreateWebHook(self) -> WebHook:
|
|
|
|
return WebHook(WebHookID=str(uuid.uuid4()))
|
2025-08-20 16:47:07 +08:00
|
|
|
|
|
|
|
def SearchWebHook(self, webHook: WebHook) -> WebHook | None:
|
|
|
|
try:
|
|
|
|
first = next(filter(lambda x : x.WebHookID == webHook.WebHookID, self.WebHooks))
|
|
|
|
except StopIteration:
|
|
|
|
return None
|
|
|
|
return first
|
|
|
|
|
2025-08-26 00:41:37 +08:00
|
|
|
def SearchWebHookByID(self, webHookID: str) -> WebHook | None:
|
|
|
|
try:
|
|
|
|
first = next(filter(lambda x : x.WebHookID == webHookID, self.WebHooks))
|
|
|
|
except StopIteration:
|
|
|
|
return None
|
|
|
|
return first
|
|
|
|
|
2025-08-20 16:47:07 +08:00
|
|
|
def UpdateWebHook(self, webHook: dict[str, str]) -> tuple[bool, str] | tuple[bool, None]:
|
|
|
|
try:
|
2025-08-22 18:26:31 +08:00
|
|
|
webHook = WebHook(**webHook)
|
|
|
|
|
|
|
|
if len(webHook.PayloadURL) == 0:
|
|
|
|
return False, "Payload URL cannot be empty"
|
|
|
|
|
2025-08-25 16:06:41 +08:00
|
|
|
if len(webHook.ContentType) == 0 or webHook.ContentType not in [
|
|
|
|
'application/json', 'application/x-www-form-urlencoded'
|
|
|
|
]:
|
2025-08-22 18:26:31 +08:00
|
|
|
return False, "Content Type is invalid"
|
|
|
|
|
|
|
|
|
2025-08-20 16:47:07 +08:00
|
|
|
with self.engine.begin() as conn:
|
|
|
|
if self.SearchWebHook(webHook):
|
|
|
|
conn.execute(
|
|
|
|
self.webHooksTable.update().values(
|
|
|
|
webHook.model_dump(exclude={'WebHookID'})
|
|
|
|
).where(
|
|
|
|
self.webHooksTable.c.WebHookID == webHook.WebHookID
|
|
|
|
)
|
|
|
|
)
|
|
|
|
else:
|
2025-08-22 18:26:31 +08:00
|
|
|
webHook.CreationDate = datetime.now()
|
2025-08-20 16:47:07 +08:00
|
|
|
conn.execute(
|
|
|
|
self.webHooksTable.insert().values(
|
|
|
|
webHook.model_dump()
|
|
|
|
)
|
|
|
|
)
|
|
|
|
self.__getWebHooks()
|
|
|
|
except Exception as e:
|
|
|
|
return False, str(e)
|
|
|
|
return True, None
|
|
|
|
|
|
|
|
def DeleteWebHook(self, webHook) -> tuple[bool, str] | tuple[bool, None]:
|
|
|
|
try:
|
2025-08-25 16:06:41 +08:00
|
|
|
webHook = WebHook(**webHook)
|
2025-08-20 16:47:07 +08:00
|
|
|
with self.engine.begin() as conn:
|
|
|
|
conn.execute(
|
|
|
|
self.webHooksTable.delete().where(
|
|
|
|
self.webHooksTable.c.WebHookID == webHook.WebHookID
|
|
|
|
)
|
|
|
|
)
|
2025-08-25 16:06:41 +08:00
|
|
|
self.__getWebHooks()
|
2025-08-20 16:47:07 +08:00
|
|
|
except Exception as e:
|
|
|
|
return False, str(e)
|
2025-08-25 16:06:41 +08:00
|
|
|
return True, None
|
|
|
|
|
2025-08-28 16:11:01 +08:00
|
|
|
def RunWebHook(self, action: str, data):
|
|
|
|
try:
|
|
|
|
if action not in WebHookActions:
|
|
|
|
return False
|
|
|
|
self.__getWebHooks()
|
2025-09-13 08:23:54 +08:00
|
|
|
subscribedWebHooks = filter(lambda webhook: action in webhook.SubscribedActions and webhook.IsActive,
|
|
|
|
self.WebHooks)
|
2025-08-28 16:11:01 +08:00
|
|
|
data['action'] = action
|
|
|
|
for i in subscribedWebHooks:
|
|
|
|
try:
|
|
|
|
ws = WebHookSession(i, data)
|
|
|
|
t = threading.Thread(target=ws.Execute, daemon=True)
|
|
|
|
t.start()
|
2025-09-13 08:23:54 +08:00
|
|
|
current_app.logger.info(f"Requesting {i.PayloadURL}")
|
2025-08-28 16:11:01 +08:00
|
|
|
except Exception as e:
|
2025-09-13 08:23:54 +08:00
|
|
|
current_app.logger.error(f"Requesting {i.PayloadURL} error", e)
|
2025-08-28 16:11:01 +08:00
|
|
|
except Exception as e:
|
2025-09-13 08:23:54 +08:00
|
|
|
current_app.logger.error("Error when running WebHook")
|
2025-08-25 16:06:41 +08:00
|
|
|
|
|
|
|
class WebHookSession:
|
|
|
|
def __init__(self, webHook: WebHook, data: dict[str, str]):
|
2025-09-19 20:55:47 +02:00
|
|
|
self.engine = db.create_engine(ConnectionString(DEFAULT_DB))
|
2025-08-25 16:06:41 +08:00
|
|
|
self.metadata = db.MetaData()
|
|
|
|
self.webHookSessionsTable = db.Table('DashboardWebHookSessions', self.metadata, autoload_with=self.engine)
|
|
|
|
self.webHook = webHook
|
|
|
|
self.sessionID = str(uuid.uuid4())
|
|
|
|
self.webHookSessionLogs: WebHookSessionLogs = WebHookSessionLogs()
|
2025-08-26 00:41:37 +08:00
|
|
|
self.time = datetime.now()
|
|
|
|
data['time'] = self.time.strftime("%Y-%m-%d %H:%M:%S")
|
2025-08-25 16:06:41 +08:00
|
|
|
data['webhook_id'] = webHook.WebHookID
|
|
|
|
data['webhook_session'] = self.sessionID
|
2025-08-26 00:41:37 +08:00
|
|
|
self.data = data
|
2025-08-25 16:06:41 +08:00
|
|
|
self.Prepare()
|
|
|
|
|
|
|
|
def Prepare(self):
|
|
|
|
with self.engine.begin() as conn:
|
|
|
|
conn.execute(
|
|
|
|
self.webHookSessionsTable.insert().values({
|
|
|
|
"WebHookSessionID": self.sessionID,
|
|
|
|
"WebHookID": self.webHook.WebHookID,
|
2025-08-26 00:41:37 +08:00
|
|
|
"Data": self.data,
|
|
|
|
"StartDate": self.time,
|
2025-08-25 16:06:41 +08:00
|
|
|
"Status": -1,
|
|
|
|
"Logs": self.webHookSessionLogs.model_dump()
|
|
|
|
})
|
|
|
|
)
|
|
|
|
self.UpdateSessionLog(-1, "Preparing webhook session")
|
|
|
|
|
|
|
|
def UpdateSessionLog(self, status, message):
|
|
|
|
self.webHookSessionLogs.addLog(status, message)
|
|
|
|
with self.engine.begin() as conn:
|
|
|
|
conn.execute(
|
|
|
|
self.webHookSessionsTable.update().values({
|
|
|
|
"Logs": self.webHookSessionLogs.model_dump()
|
|
|
|
}).where(
|
|
|
|
self.webHookSessionsTable.c.WebHookSessionID == self.sessionID
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
def UpdateStatus(self, status: int):
|
|
|
|
with self.engine.begin() as conn:
|
|
|
|
conn.execute(
|
|
|
|
self.webHookSessionsTable.update().values({
|
|
|
|
"Status": status,
|
|
|
|
"EndDate": datetime.now()
|
|
|
|
}).where(
|
|
|
|
self.webHookSessionsTable.c.WebHookSessionID == self.sessionID
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2025-08-28 16:11:01 +08:00
|
|
|
def Execute(self):
|
2025-08-25 16:06:41 +08:00
|
|
|
success = False
|
|
|
|
|
|
|
|
for i in range(5):
|
|
|
|
headerDictionary = {
|
|
|
|
'Content-Type': self.webHook.ContentType
|
|
|
|
}
|
|
|
|
for header in self.webHook.Headers.values():
|
|
|
|
if header['key'] not in ['Content-Type']:
|
|
|
|
headerDictionary[header['key']] = header['value']
|
|
|
|
|
|
|
|
if self.webHook.ContentType == "application/json":
|
2025-08-28 16:11:01 +08:00
|
|
|
reqData = json.dumps(self.data)
|
2025-08-25 16:06:41 +08:00
|
|
|
else:
|
2025-08-28 16:11:01 +08:00
|
|
|
for (key, val) in self.data.items():
|
|
|
|
if type(self.data[key]) not in [str, int]:
|
|
|
|
self.data[key] = json.dumps(self.data[key])
|
|
|
|
reqData = urllib.parse.urlencode(self.data)
|
2025-08-25 16:06:41 +08:00
|
|
|
try:
|
|
|
|
req = requests.post(
|
2025-08-28 16:11:01 +08:00
|
|
|
self.webHook.PayloadURL, headers=headerDictionary, timeout=10, data=reqData, verify=self.webHook.VerifySSL
|
2025-08-25 16:06:41 +08:00
|
|
|
)
|
|
|
|
req.raise_for_status()
|
|
|
|
success = True
|
|
|
|
self.UpdateSessionLog(0, "Webhook request finished")
|
|
|
|
self.UpdateSessionLog(0, json.dumps({"returned_data": req.text}))
|
|
|
|
self.UpdateStatus(0)
|
|
|
|
break
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
self.UpdateSessionLog(1, f"Attempt #{i + 1}/5. Request errored. Reason: " + str(e))
|
2025-08-26 00:41:37 +08:00
|
|
|
time.sleep(10)
|
2025-08-25 16:06:41 +08:00
|
|
|
|
|
|
|
if not success:
|
|
|
|
self.UpdateSessionLog(1, "Webhook request failed & terminated.")
|
2025-08-26 00:41:37 +08:00
|
|
|
self.UpdateStatus(1)
|