feat: Enhance hook management and session handling
- Update hook model to include last_triggered_at field. - Modify API endpoints to support updating hooks with new fields. - Implement session management UI improvements with toggle functionality. - Add new JavaScript functions for better session detail visibility. - Refactor hook storage logic to handle last triggered timestamps. - Introduce new favicon and logo for branding. - Update styles for improved layout and user experience. - Enhance tests to cover new functionality and ensure reliability.
This commit is contained in:
120
app/storage.py
120
app/storage.py
@@ -22,6 +22,16 @@ class HookStore:
|
||||
self.hook_id_length = hook_id_length
|
||||
self._lock = threading.Lock()
|
||||
self._initialize()
|
||||
|
||||
def _deserialize_hook(self, item: dict) -> HookRead:
|
||||
last_triggered = item.get("last_triggered_at")
|
||||
return HookRead(
|
||||
hook_id=item["hook_id"],
|
||||
chat_id=item["chat_id"],
|
||||
message=item["message"],
|
||||
created_at=datetime.fromisoformat(item["created_at"]),
|
||||
last_triggered_at=datetime.fromisoformat(last_triggered) if last_triggered else None,
|
||||
)
|
||||
|
||||
def _initialize(self) -> None:
|
||||
if not self.storage_path.exists():
|
||||
@@ -58,15 +68,7 @@ class HookStore:
|
||||
|
||||
def list_hooks(self) -> List[HookRead]:
|
||||
raw_hooks = self._load_raw()
|
||||
hooks = [
|
||||
HookRead(
|
||||
hook_id=item["hook_id"],
|
||||
chat_id=item["chat_id"],
|
||||
message=item["message"],
|
||||
created_at=datetime.fromisoformat(item["created_at"]),
|
||||
)
|
||||
for item in raw_hooks
|
||||
]
|
||||
hooks = [self._deserialize_hook(item) for item in raw_hooks]
|
||||
hooks.sort(key=lambda h: h.created_at, reverse=True)
|
||||
return hooks
|
||||
|
||||
@@ -82,6 +84,7 @@ class HookStore:
|
||||
"chat_id": payload.chat_id,
|
||||
"message": payload.message,
|
||||
"created_at": created_at,
|
||||
"last_triggered_at": None,
|
||||
}
|
||||
)
|
||||
self._save_raw(raw_hooks)
|
||||
@@ -90,18 +93,14 @@ class HookStore:
|
||||
chat_id=payload.chat_id,
|
||||
message=payload.message,
|
||||
created_at=datetime.fromisoformat(created_at),
|
||||
last_triggered_at=None,
|
||||
)
|
||||
|
||||
def get_hook(self, hook_id: str) -> Optional[HookRead]:
|
||||
raw_hooks = self._load_raw()
|
||||
for item in raw_hooks:
|
||||
if item.get("hook_id") == hook_id:
|
||||
return HookRead(
|
||||
hook_id=item["hook_id"],
|
||||
chat_id=item["chat_id"],
|
||||
message=item["message"],
|
||||
created_at=datetime.fromisoformat(item["created_at"]),
|
||||
)
|
||||
return self._deserialize_hook(item)
|
||||
return None
|
||||
|
||||
def delete_hook(self, hook_id: str) -> bool:
|
||||
@@ -113,34 +112,61 @@ class HookStore:
|
||||
self._save_raw(new_hooks)
|
||||
return True
|
||||
|
||||
def update_hook_id(self, current_id: str, new_id: str) -> HookRead:
|
||||
normalized_new_id = new_id.strip()
|
||||
if not normalized_new_id:
|
||||
raise ValueError("Hook ID cannot be empty")
|
||||
if not HOOK_ID_PATTERN.fullmatch(normalized_new_id):
|
||||
raise ValueError("Hook ID must be 3-64 characters of letters, numbers, '_' or '-' only")
|
||||
def update_hook(
|
||||
self,
|
||||
current_id: str,
|
||||
*,
|
||||
new_hook_id: Optional[str] = None,
|
||||
chat_id: Optional[str] = None,
|
||||
message: Optional[str] = None,
|
||||
) -> HookRead:
|
||||
if new_hook_id is None and chat_id is None and message is None:
|
||||
raise ValueError("No updates provided")
|
||||
|
||||
normalized_id = new_hook_id.strip() if new_hook_id is not None else None
|
||||
normalized_chat = chat_id.strip() if chat_id is not None else None
|
||||
normalized_message = message.strip() if message is not None else None
|
||||
|
||||
if normalized_id is not None:
|
||||
if not normalized_id:
|
||||
raise ValueError("Hook ID cannot be empty")
|
||||
if not HOOK_ID_PATTERN.fullmatch(normalized_id):
|
||||
raise ValueError("Hook ID must be 3-64 characters of letters, numbers, '_' or '-' only")
|
||||
if normalized_chat is not None and not normalized_chat:
|
||||
raise ValueError("Chat ID cannot be empty")
|
||||
if normalized_message is not None and not normalized_message:
|
||||
raise ValueError("Message cannot be empty")
|
||||
|
||||
with self._lock:
|
||||
raw_hooks = self._load_raw()
|
||||
exists = next((item for item in raw_hooks if item.get("hook_id") == current_id), None)
|
||||
if not exists:
|
||||
raise KeyError("Hook not found")
|
||||
if normalized_new_id == current_id:
|
||||
return HookRead(
|
||||
hook_id=exists["hook_id"],
|
||||
chat_id=exists["chat_id"],
|
||||
message=exists["message"],
|
||||
created_at=datetime.fromisoformat(exists["created_at"]),
|
||||
)
|
||||
if any(item.get("hook_id") == normalized_new_id for item in raw_hooks):
|
||||
raise ValueError("Hook ID already exists")
|
||||
exists["hook_id"] = normalized_new_id
|
||||
|
||||
if normalized_id is not None and normalized_id != current_id:
|
||||
if any(item.get("hook_id") == normalized_id for item in raw_hooks):
|
||||
raise ValueError("Hook ID already exists")
|
||||
exists["hook_id"] = normalized_id
|
||||
|
||||
if normalized_chat is not None:
|
||||
exists["chat_id"] = normalized_chat
|
||||
|
||||
if normalized_message is not None:
|
||||
exists["message"] = normalized_message
|
||||
|
||||
self._save_raw(raw_hooks)
|
||||
return HookRead(
|
||||
hook_id=normalized_new_id,
|
||||
chat_id=exists["chat_id"],
|
||||
message=exists["message"],
|
||||
created_at=datetime.fromisoformat(exists["created_at"]),
|
||||
)
|
||||
return self._deserialize_hook(exists)
|
||||
|
||||
def mark_hook_triggered(self, hook_id: str) -> HookRead:
|
||||
timestamp = datetime.now(UTC).replace(microsecond=0).isoformat()
|
||||
with self._lock:
|
||||
raw_hooks = self._load_raw()
|
||||
exists = next((item for item in raw_hooks if item.get("hook_id") == hook_id), None)
|
||||
if not exists:
|
||||
raise KeyError("Hook not found")
|
||||
exists["last_triggered_at"] = timestamp
|
||||
self._save_raw(raw_hooks)
|
||||
return self._deserialize_hook(exists)
|
||||
|
||||
|
||||
settings = get_settings()
|
||||
@@ -163,5 +189,21 @@ async def delete_hook_async(hook_id: str) -> bool:
|
||||
return await run_in_threadpool(store.delete_hook, hook_id)
|
||||
|
||||
|
||||
async def update_hook_id_async(current_hook_id: str, new_hook_id: str) -> HookRead:
|
||||
return await run_in_threadpool(store.update_hook_id, current_hook_id, new_hook_id)
|
||||
async def update_hook_async(
|
||||
current_hook_id: str,
|
||||
*,
|
||||
new_hook_id: Optional[str] = None,
|
||||
chat_id: Optional[str] = None,
|
||||
message: Optional[str] = None,
|
||||
) -> HookRead:
|
||||
return await run_in_threadpool(
|
||||
store.update_hook,
|
||||
current_hook_id,
|
||||
new_hook_id=new_hook_id,
|
||||
chat_id=chat_id,
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
async def record_hook_trigger_async(hook_id: str) -> HookRead:
|
||||
return await run_in_threadpool(store.mark_hook_triggered, hook_id)
|
||||
|
||||
Reference in New Issue
Block a user