943acbc573
Durcit la convention de nommage des projets (dérive constatée : 'Sliding Automation', 'code_versioning'... au lieu des formes canoniques). - trilium_api.py : projets_canoniques() lit le référentiel = valeurs du label projet sur les notes de type=projet (source unique, pas de constante en dur). Note-projet CodeVersioning créée (manquait). - mcp_server.py : _valider_projet() branché dans les 6 tools de création (add_decision/history/backlog, new_conversation, create_entite, add_skill). Refuse un projet non canonique (suggestion si faute) ou inconnu (renvoi au processus de création de projet). Ne verrouille pas si référentiel illisible. - lint_audit.py : VAL-nommage aligné sur le référentiel (attrape casse, espace ET snake_case ; l'ancien 'contient un espace' ratait code_versioning). - Données : 79 notes ré-étiquetées vers les 3 formes canoniques. Quality by design : l'erreur de nommage devient impossible à l'écriture, le Lint n'est plus que le filet de sécurité.
161 lines
5.2 KiB
Python
161 lines
5.2 KiB
Python
import re
|
|
import sys
|
|
from typing import Optional
|
|
|
|
import requests
|
|
from loguru import logger
|
|
|
|
from .version import __version__
|
|
|
|
|
|
class WEBAPI:
|
|
__version__ = __version__
|
|
|
|
def __init__(self, server_url: str, sid: Optional[str] = None, _csrf: Optional[str] = None,
|
|
csrf_token: Optional[str] = None):
|
|
if sys.version_info < (3, 9):
|
|
print(
|
|
(
|
|
f'You are using Python {sys.version_info.major}.{sys.version_info.minor}'
|
|
', 3.9+ is required.'
|
|
),
|
|
file=sys.stderr,
|
|
)
|
|
|
|
self.server_url = server_url
|
|
self.sid: str = sid
|
|
self._csrf = _csrf
|
|
self.csrf_token = csrf_token
|
|
|
|
def get_cookie(self) -> dict:
|
|
return {
|
|
'_csrf': self._csrf,
|
|
'trilium.sid': self.sid,
|
|
'trilium-device': 'desktop',
|
|
# Trilium next uses this cookie
|
|
'trilium-csrf': self.csrf_token,
|
|
}
|
|
|
|
def get_headers(self) -> dict:
|
|
return {
|
|
'x-csrf-token': self.csrf_token,
|
|
}
|
|
|
|
def refresh_csrf_token(self) -> str:
|
|
# Classic Trilium 0.63.7, the csrfToken is from `/` endpoint
|
|
url = f'{self.server_url}/'
|
|
res = requests.get(url, cookies=self.get_cookie())
|
|
csrf_token_match = re.search(r"csrfToken:\s*'([^']+)'", res.text)
|
|
|
|
if csrf_token_match:
|
|
csrf_token = csrf_token_match.group(1)
|
|
logger.info(f"Extracted csrfToken: {csrf_token}")
|
|
self.csrf_token = csrf_token
|
|
return csrf_token
|
|
else:
|
|
logger.info("csrfToken not found.")
|
|
# Trilium next, the csrfToken is from `/bootstrap` endpoint
|
|
logger.info("Trying to extract csrfToken from /bootstrap endpoint...")
|
|
url = f'{self.server_url}/bootstrap'
|
|
res = requests.get(url, cookies=self.get_cookie())
|
|
csrf_token = res.json()['csrfToken']
|
|
logger.info(f"Extracted csrfToken: {csrf_token}")
|
|
self.csrf_token = csrf_token
|
|
return csrf_token
|
|
|
|
def login(self, password: str) -> Optional[str]:
|
|
"""
|
|
mimic web login
|
|
"""
|
|
url = f'{self.server_url}/login'
|
|
|
|
data = {'password': password}
|
|
|
|
# login process is in 2-step
|
|
# 1. 302 set-cookie trilium.sid
|
|
# 2. 200 set-cookie _csrf
|
|
# single requests will not work, need to use session
|
|
session = requests.Session()
|
|
res = session.post(url, data=data, allow_redirects=True)
|
|
for cookie in session.cookies:
|
|
logger.info(f"{cookie.name}: {cookie.value}")
|
|
|
|
if res.status_code == 200:
|
|
self.sid = session.cookies.get('trilium.sid')
|
|
self._csrf = session.cookies.get('_csrf')
|
|
self.refresh_csrf_token()
|
|
return self.sid
|
|
else:
|
|
logger.info(res.text)
|
|
return ''
|
|
|
|
def logout(self, sid: Optional[str] = None) -> bool:
|
|
"""
|
|
mimic web logout
|
|
"""
|
|
|
|
if not sid:
|
|
sid = self.sid
|
|
|
|
if not sid:
|
|
return False
|
|
|
|
url = f'{self.server_url}/logout'
|
|
res = requests.post(url, headers=self.get_headers(), cookies=self.get_cookie())
|
|
|
|
if res.status_code == 200:
|
|
logger.info('logout successfully')
|
|
return True
|
|
return False
|
|
|
|
def get_note_content(self, note_id):
|
|
url = f'{self.server_url}/api/notes/{note_id}/blob'
|
|
res = requests.get(url, cookies=self.get_cookie())
|
|
return res.json()['content']
|
|
|
|
def update_note_content(self, note_id, content):
|
|
url = f'{self.server_url}/api/notes/{note_id}/data'
|
|
data = {'content': content}
|
|
res = requests.put(url, headers=self.get_headers(), cookies=self.get_cookie(), json=data)
|
|
if res.status_code == 204:
|
|
return True
|
|
return False
|
|
|
|
def share_note(self, note_id: str):
|
|
url = f'{self.server_url}/api/notes/{note_id}/clone-to-note/_share'
|
|
|
|
res = requests.put(url, headers=self.get_headers(), cookies=self.get_cookie())
|
|
logger.info(res.json())
|
|
if res.json():
|
|
return True
|
|
return False
|
|
|
|
def cancel_share_note(self, note_id: str):
|
|
url = f'{self.server_url}/api/branches/_share_{note_id}?taskId=no-progress-reporting'
|
|
|
|
res = requests.delete(url, headers=self.get_headers(), cookies=self.get_cookie())
|
|
logger.info(res.json())
|
|
|
|
if res.status_code == 200: # DELETE 请求通常返回 200 表示成功
|
|
return True
|
|
return False
|
|
|
|
def enter_protected_session(self, password: str):
|
|
url = f'{self.server_url}/api/login/protected'
|
|
data = {'password': password}
|
|
res = requests.post(url, headers=self.get_headers(), cookies=self.get_cookie(), json=data)
|
|
try:
|
|
if res.json()['success'] == True:
|
|
return True
|
|
else:
|
|
return False
|
|
except:
|
|
return False
|
|
|
|
def leave_protected_session(self):
|
|
url = f'{self.server_url}/api/logout/protected'
|
|
res = requests.post(url, headers=self.get_headers(), cookies=self.get_cookie())
|
|
if res.status_code == 204:
|
|
logger.info('leave protected session successfully')
|
|
return True
|
|
return False |