Add CLI source code and update .gitignore
This commit is contained in:
21
cli_anything/codimd/core/__init__.py
Normal file
21
cli_anything/codimd/core/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
Core modules for CodiMD CLI.
|
||||
"""
|
||||
|
||||
from .config import Config
|
||||
from .session import Session
|
||||
from .client import CodiMDClient
|
||||
from .note import NoteManager
|
||||
from .user import UserManager
|
||||
from .export import ExportManager
|
||||
from .revision import RevisionManager
|
||||
|
||||
__all__ = [
|
||||
"Config",
|
||||
"Session",
|
||||
"CodiMDClient",
|
||||
"NoteManager",
|
||||
"UserManager",
|
||||
"ExportManager",
|
||||
"RevisionManager",
|
||||
]
|
||||
70
cli_anything/codimd/core/client.py
Normal file
70
cli_anything/codimd/core/client.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
HTTP client for CodiMD API.
|
||||
"""
|
||||
|
||||
import requests
|
||||
from typing import Optional, Dict, Any, Tuple
|
||||
from urllib.parse import urljoin
|
||||
|
||||
|
||||
class CodiMDClient:
|
||||
"""HTTP client for CodiMD server."""
|
||||
|
||||
def __init__(self, server: str, session_cookies: Dict[str, str] = None,
|
||||
timeout: int = 30, verify_ssl: bool = True):
|
||||
self.server = server.rstrip("/")
|
||||
self.timeout = timeout
|
||||
self.verify_ssl = verify_ssl
|
||||
self.session = requests.Session()
|
||||
if session_cookies:
|
||||
self.session.cookies.update(session_cookies)
|
||||
|
||||
def _url(self, path: str) -> str:
|
||||
"""Build full URL for path."""
|
||||
# Remove leading slash from path to make it relative
|
||||
path = path.lstrip("/")
|
||||
return urljoin(self.server + "/", path)
|
||||
|
||||
def get(self, path: str, **kwargs) -> requests.Response:
|
||||
"""Send GET request."""
|
||||
return self.session.get(
|
||||
self._url(path),
|
||||
timeout=self.timeout,
|
||||
verify=self.verify_ssl,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def post(self, path: str, **kwargs) -> requests.Response:
|
||||
"""Send POST request."""
|
||||
return self.session.post(
|
||||
self._url(path),
|
||||
timeout=self.timeout,
|
||||
verify=self.verify_ssl,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def put(self, path: str, **kwargs) -> requests.Response:
|
||||
"""Send PUT request."""
|
||||
return self.session.put(
|
||||
self._url(path),
|
||||
timeout=self.timeout,
|
||||
verify=self.verify_ssl,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def delete(self, path: str, **kwargs) -> requests.Response:
|
||||
"""Send DELETE request."""
|
||||
return self.session.delete(
|
||||
self._url(path),
|
||||
timeout=self.timeout,
|
||||
verify=self.verify_ssl,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def update_cookies(self, cookies: Dict[str, str]):
|
||||
"""Update session cookies."""
|
||||
self.session.cookies.update(cookies)
|
||||
|
||||
def get_cookies(self) -> Dict[str, str]:
|
||||
"""Get current cookies."""
|
||||
return dict(self.session.cookies)
|
||||
77
cli_anything/codimd/core/config.py
Normal file
77
cli_anything/codimd/core/config.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Configuration management for CodiMD CLI.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Config:
|
||||
"""Configuration manager for CodiMD CLI."""
|
||||
|
||||
DEFAULT_SERVER = "http://localhost:3000"
|
||||
CONFIG_DIR = Path.home() / ".config" / "cli-anything-codimd"
|
||||
|
||||
def __init__(self):
|
||||
self.config_dir = self.CONFIG_DIR
|
||||
self.config_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.config_file = self.config_dir / "config.json"
|
||||
self.session_file = self.config_dir / "session.json"
|
||||
self.cache_file = self.config_dir / "cache.json"
|
||||
|
||||
self._config = self._load_config()
|
||||
|
||||
def _load_config(self) -> dict:
|
||||
"""Load configuration from file or create default."""
|
||||
if self.config_file.exists():
|
||||
with open(self.config_file, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
default_config = {
|
||||
"server": os.environ.get("CODIMD_SERVER", self.DEFAULT_SERVER),
|
||||
"timeout": 30,
|
||||
"verify_ssl": True,
|
||||
}
|
||||
self.save_config(default_config)
|
||||
return default_config
|
||||
|
||||
def save_config(self, config: dict):
|
||||
"""Save configuration to file."""
|
||||
with open(self.config_file, "w") as f:
|
||||
json.dump(config, f, indent=2)
|
||||
self._config = config
|
||||
|
||||
@property
|
||||
def server(self) -> str:
|
||||
"""Get configured server URL."""
|
||||
return self._config.get("server", self.DEFAULT_SERVER)
|
||||
|
||||
@server.setter
|
||||
def server(self, value: str):
|
||||
"""Set server URL."""
|
||||
self._config["server"] = value
|
||||
self.save_config(self._config)
|
||||
|
||||
@property
|
||||
def timeout(self) -> int:
|
||||
"""Get request timeout."""
|
||||
return self._config.get("timeout", 30)
|
||||
|
||||
@timeout.setter
|
||||
def timeout(self, value: int):
|
||||
"""Set request timeout."""
|
||||
self._config["timeout"] = value
|
||||
self.save_config(self._config)
|
||||
|
||||
@property
|
||||
def verify_ssl(self) -> bool:
|
||||
"""Get SSL verification setting."""
|
||||
return self._config.get("verify_ssl", True)
|
||||
|
||||
@verify_ssl.setter
|
||||
def verify_ssl(self, value: bool):
|
||||
"""Set SSL verification setting."""
|
||||
self._config["verify_ssl"] = value
|
||||
self.save_config(self._config)
|
||||
97
cli_anything/codimd/core/export.py
Normal file
97
cli_anything/codimd/core/export.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
Export operations for CodiMD CLI.
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
|
||||
from .client import CodiMDClient
|
||||
|
||||
|
||||
class ExportManager:
|
||||
"""Manager for export operations."""
|
||||
|
||||
def __init__(self, client: CodiMDClient):
|
||||
self.client = client
|
||||
|
||||
def export_markdown(self, note_id: str, output_file: Optional[Path] = None) -> str:
|
||||
"""Export note as markdown."""
|
||||
response = self.client.get(f"/{note_id}/download")
|
||||
|
||||
if response.status_code == 404:
|
||||
raise FileNotFoundError(f"Note not found: {note_id}")
|
||||
if response.status_code == 403:
|
||||
raise PermissionError("No permission to export this note")
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to export markdown: {response.status_code}")
|
||||
|
||||
content = response.text
|
||||
|
||||
if output_file:
|
||||
output_file.write_text(content)
|
||||
return str(output_file)
|
||||
|
||||
return content
|
||||
|
||||
def export_pdf(self, note_id: str, output_file: Optional[Path] = None) -> bytes:
|
||||
"""Export note as PDF."""
|
||||
response = self.client.get(f"/{note_id}/pdf")
|
||||
|
||||
if response.status_code == 403:
|
||||
raise PermissionError("PDF export disabled or no permission")
|
||||
if response.status_code == 404:
|
||||
raise FileNotFoundError(f"Note not found: {note_id}")
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to export PDF: {response.status_code}")
|
||||
|
||||
content = response.content
|
||||
|
||||
if output_file:
|
||||
output_file.write_bytes(content)
|
||||
return str(output_file)
|
||||
|
||||
return content
|
||||
|
||||
def export_html(self, note_id: str, output_file: Optional[Path] = None) -> str:
|
||||
"""Export note as HTML (published view)."""
|
||||
# First publish the note
|
||||
publish_response = self.client.get(f"/{note_id}/publish")
|
||||
|
||||
if publish_response.status_code != 200:
|
||||
raise Exception(f"Failed to publish note: {publish_response.status_code}")
|
||||
|
||||
# Extract the short ID from the response
|
||||
url = publish_response.url
|
||||
short_id = url.rstrip("/").split("/")[-1]
|
||||
|
||||
# Get the published view
|
||||
response = self.client.get(f"/s/{short_id}")
|
||||
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to export HTML: {response.status_code}")
|
||||
|
||||
content = response.text
|
||||
|
||||
if output_file:
|
||||
output_file.write_text(content)
|
||||
return str(output_file)
|
||||
|
||||
return content
|
||||
|
||||
def export_slide(self, note_id: str, output_file: Optional[Path] = None) -> str:
|
||||
"""Export note as reveal.js slides."""
|
||||
response = self.client.get(f"/{note_id}/slide")
|
||||
|
||||
if response.status_code == 404:
|
||||
raise FileNotFoundError(f"Note not found: {note_id}")
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to export slides: {response.status_code}")
|
||||
|
||||
content = response.text
|
||||
|
||||
if output_file:
|
||||
output_file.write_text(content)
|
||||
return str(output_file)
|
||||
|
||||
return content
|
||||
183
cli_anything/codimd/core/note.py
Normal file
183
cli_anything/codimd/core/note.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""
|
||||
Note operations for CodiMD CLI.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import base64
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
from .client import CodiMDClient
|
||||
|
||||
|
||||
class NoteManager:
|
||||
"""Manager for note operations."""
|
||||
|
||||
def __init__(self, client: CodiMDClient):
|
||||
self.client = client
|
||||
|
||||
@staticmethod
|
||||
def encode_note_id(note_id: str) -> str:
|
||||
"""Encode UUID note ID to base64url format."""
|
||||
# Remove dashes and convert to bytes
|
||||
hex_str = note_id.replace("-", "")
|
||||
raw_bytes = bytes.fromhex(hex_str)
|
||||
# Encode to base64url
|
||||
return base64.urlsafe_b64encode(raw_bytes).decode("utf-8").rstrip("=")
|
||||
|
||||
@staticmethod
|
||||
def decode_note_id(encoded_id: str) -> str:
|
||||
"""Decode base64url note ID to UUID format."""
|
||||
# Add padding if needed
|
||||
padding = 4 - len(encoded_id) % 4
|
||||
if padding != 4:
|
||||
encoded_id += "=" * padding
|
||||
# Decode from base64url
|
||||
raw_bytes = base64.urlsafe_b64decode(encoded_id)
|
||||
hex_str = raw_bytes.hex()
|
||||
# Format as UUID
|
||||
parts = [
|
||||
hex_str[0:8], hex_str[8:12], hex_str[12:16],
|
||||
hex_str[16:20], hex_str[20:32]
|
||||
]
|
||||
return "-".join(parts)
|
||||
|
||||
def list_my_notes(self) -> List[Dict[str, Any]]:
|
||||
"""List notes owned by current user."""
|
||||
response = self.client.get("/api/notes/myNotes")
|
||||
if response.status_code == 403:
|
||||
raise PermissionError("Not authenticated or not authorized")
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to list notes: {response.status_code}")
|
||||
|
||||
data = response.json()
|
||||
return data.get("myNotes", [])
|
||||
|
||||
def get_note(self, note_id: str) -> Dict[str, Any]:
|
||||
"""Get note content and metadata."""
|
||||
response = self.client.get(f"/{note_id}")
|
||||
if response.status_code == 404:
|
||||
raise FileNotFoundError(f"Note not found: {note_id}")
|
||||
if response.status_code == 403:
|
||||
raise PermissionError("No permission to view this note")
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to get note: {response.status_code}")
|
||||
|
||||
# Parse response for note data
|
||||
# The note page contains the data in HTML, we need to extract it
|
||||
# For now, return a basic structure
|
||||
return {
|
||||
"id": note_id,
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
def create_note(self, content: str = "", alias: str = None) -> Dict[str, Any]:
|
||||
"""Create a new note."""
|
||||
if content:
|
||||
response = self.client.post(
|
||||
"/new",
|
||||
data=content.encode("utf-8"),
|
||||
headers={"Content-Type": "text/markdown"}
|
||||
)
|
||||
else:
|
||||
response = self.client.get("/new")
|
||||
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to create note: {response.status_code}")
|
||||
|
||||
# Extract note ID from response or redirect
|
||||
# The response might be a redirect to the new note
|
||||
if response.history:
|
||||
# Get the final URL
|
||||
final_url = response.url
|
||||
note_id = final_url.rstrip("/").split("/")[-1]
|
||||
else:
|
||||
# Parse response to find note ID
|
||||
note_id = self._extract_note_id_from_response(response)
|
||||
|
||||
return {
|
||||
"id": note_id,
|
||||
"url": f"{self.client.server}/{note_id}",
|
||||
"status": "created"
|
||||
}
|
||||
|
||||
def _extract_note_id_from_response(self, response) -> str:
|
||||
"""Extract note ID from response."""
|
||||
# Try to extract from JSON response
|
||||
try:
|
||||
data = response.json()
|
||||
return data.get("id") or data.get("noteId")
|
||||
except:
|
||||
pass
|
||||
|
||||
# Extract from HTML or use a default
|
||||
match = re.search(r'/([a-zA-Z0-9_-]+)', response.url or "")
|
||||
if match:
|
||||
return match.group(1)
|
||||
|
||||
return "unknown"
|
||||
|
||||
def update_note(self, note_id: str, content: str) -> Dict[str, Any]:
|
||||
"""Update note content."""
|
||||
response = self.client.put(
|
||||
f"/api/notes/{note_id}",
|
||||
json={"content": content},
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
if response.status_code == 403:
|
||||
raise PermissionError("No permission to edit this note")
|
||||
if response.status_code == 404:
|
||||
raise FileNotFoundError(f"Note not found: {note_id}")
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to update note: {response.status_code}")
|
||||
|
||||
return response.json()
|
||||
|
||||
def delete_note(self, note_id: str) -> bool:
|
||||
"""Delete a note."""
|
||||
response = self.client.delete(f"/api/notes/{note_id}")
|
||||
|
||||
if response.status_code == 403:
|
||||
raise PermissionError("No permission to delete this note")
|
||||
if response.status_code == 404:
|
||||
raise FileNotFoundError(f"Note not found: {note_id}")
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to delete note: {response.status_code}")
|
||||
|
||||
return True
|
||||
|
||||
def get_note_info(self, note_id: str) -> Dict[str, Any]:
|
||||
"""Get note metadata without content."""
|
||||
# Use the /:noteId/info endpoint
|
||||
response = self.client.get(f"/{noteId}/info")
|
||||
|
||||
if response.status_code == 404:
|
||||
raise FileNotFoundError(f"Note not found: {note_id}")
|
||||
if response.status_code == 403:
|
||||
raise PermissionError("No permission to view this note")
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to get note info: {response.status_code}")
|
||||
|
||||
try:
|
||||
return response.json()
|
||||
except:
|
||||
return {"id": note_id, "status": "success"}
|
||||
|
||||
def get_publish_link(self, note_id: str) -> Dict[str, Any]:
|
||||
"""Get or create publish link for note."""
|
||||
response = self.client.get(f"/{noteId}/publish")
|
||||
|
||||
if response.status_code == 404:
|
||||
raise FileNotFoundError(f"Note not found: {note_id}")
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to get publish link: {response.status_code}")
|
||||
|
||||
# Extract short ID from response
|
||||
return {
|
||||
"url": response.url,
|
||||
"note_id": note_id,
|
||||
"status": "published"
|
||||
}
|
||||
59
cli_anything/codimd/core/revision.py
Normal file
59
cli_anything/codimd/core/revision.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Revision operations for CodiMD CLI.
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import List, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
from .client import CodiMDClient
|
||||
|
||||
|
||||
class RevisionManager:
|
||||
"""Manager for revision operations."""
|
||||
|
||||
def __init__(self, client: CodiMDClient):
|
||||
self.client = client
|
||||
|
||||
def list_revisions(self, note_id: str) -> List[Dict[str, Any]]:
|
||||
"""List all revisions for a note."""
|
||||
response = self.client.get(f"/{note_id}/revision")
|
||||
|
||||
if response.status_code == 404:
|
||||
raise FileNotFoundError(f"Note not found: {note_id}")
|
||||
if response.status_code == 403:
|
||||
raise PermissionError("No permission to view revisions")
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to list revisions: {response.status_code}")
|
||||
|
||||
try:
|
||||
data = response.json()
|
||||
return data.get("revisions", [])
|
||||
except:
|
||||
return []
|
||||
|
||||
def get_revision_at_time(self, note_id: str, timestamp: int) -> Dict[str, Any]:
|
||||
"""Get note content at specific revision time."""
|
||||
response = self.client.get(
|
||||
f"/{note_id}/revision/{timestamp}",
|
||||
allow_redirects=True
|
||||
)
|
||||
|
||||
if response.status_code == 404:
|
||||
raise FileNotFoundError(f"Note or revision not found")
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to get revision: {response.status_code}")
|
||||
|
||||
try:
|
||||
return response.json()
|
||||
except:
|
||||
return {"note_id": note_id, "timestamp": timestamp, "content": response.text}
|
||||
|
||||
def get_latest_revision(self, note_id: str) -> Dict[str, Any]:
|
||||
"""Get the latest revision of a note."""
|
||||
revisions = self.list_revisions(note_id)
|
||||
if not revisions:
|
||||
raise FileNotFoundError(f"No revisions found for note: {note_id}")
|
||||
|
||||
latest = revisions[0]
|
||||
return self.get_revision_at_time(note_id, latest.get("time", 0))
|
||||
88
cli_anything/codimd/core/session.py
Normal file
88
cli_anything/codimd/core/session.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
Session management for CodiMD CLI.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
import hashlib
|
||||
|
||||
|
||||
class Session:
|
||||
"""Session manager for CodiMD authentication."""
|
||||
|
||||
def __init__(self, config_dir: Path):
|
||||
self.session_file = config_dir / "session.json"
|
||||
self._session = self._load_session()
|
||||
|
||||
def _load_session(self) -> dict:
|
||||
"""Load session from file."""
|
||||
if self.session_file.exists():
|
||||
with open(self.session_file, "r") as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
def save_session(self):
|
||||
"""Save session to file."""
|
||||
with open(self.session_file, "w") as f:
|
||||
json.dump(self._session, f, indent=2)
|
||||
|
||||
def is_authenticated(self) -> bool:
|
||||
"""Check if user is authenticated."""
|
||||
cookies = self._session.get("cookies", {})
|
||||
if not cookies:
|
||||
return False
|
||||
|
||||
# Check if session is expired (24 hours)
|
||||
expires_at = self._session.get("expires_at")
|
||||
if expires_at:
|
||||
expire_time = datetime.fromisoformat(expires_at)
|
||||
if datetime.now() > expire_time:
|
||||
self.clear()
|
||||
return False
|
||||
|
||||
return bool(cookies)
|
||||
|
||||
def set_cookies(self, cookies: Dict[str, str]):
|
||||
"""Set session cookies."""
|
||||
self._session["cookies"] = cookies
|
||||
# Set expiration to 24 hours from now
|
||||
self._session["expires_at"] = (
|
||||
datetime.now() + timedelta(hours=24)
|
||||
).isoformat()
|
||||
self.save_session()
|
||||
|
||||
def get_cookies(self) -> Dict[str, str]:
|
||||
"""Get session cookies."""
|
||||
return self._session.get("cookies", {})
|
||||
|
||||
def set_user(self, user_data: Dict[str, Any]):
|
||||
"""Set current user data."""
|
||||
self._session["user"] = user_data
|
||||
self.save_session()
|
||||
|
||||
def get_user(self) -> Optional[Dict[str, Any]]:
|
||||
"""Get current user data."""
|
||||
return self._session.get("user")
|
||||
|
||||
def clear(self):
|
||||
"""Clear session data."""
|
||||
self._session = {}
|
||||
self.save_session()
|
||||
|
||||
@property
|
||||
def user_id(self) -> Optional[str]:
|
||||
"""Get current user ID."""
|
||||
user = self.get_user()
|
||||
return user.get("id") if user else None
|
||||
|
||||
@property
|
||||
def username(self) -> Optional[str]:
|
||||
"""Get current username."""
|
||||
user = self.get_user()
|
||||
if not user:
|
||||
return None
|
||||
# CodiMD returns name directly, not nested in profile
|
||||
return user.get("name") or user.get("profile", {}).get("name")
|
||||
115
cli_anything/codimd/core/user.py
Normal file
115
cli_anything/codimd/core/user.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
User operations for CodiMD CLI.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from .client import CodiMDClient
|
||||
|
||||
|
||||
class UserManager:
|
||||
"""Manager for user operations."""
|
||||
|
||||
def __init__(self, client: CodiMDClient):
|
||||
self.client = client
|
||||
|
||||
def login(self, email: str, password: str) -> Dict[str, Any]:
|
||||
"""Login with email and password.
|
||||
|
||||
Returns the session cookies and user info.
|
||||
"""
|
||||
# First, get the login page to obtain CSRF token if needed
|
||||
login_page = self.client.get("/login")
|
||||
|
||||
# Try to extract CSRF token from the page
|
||||
csrf_token = None
|
||||
csrf_match = re.search(r'name="csrf".*?value="([^"]+)"', login_page.text)
|
||||
if csrf_match:
|
||||
csrf_token = csrf_match.group(1)
|
||||
|
||||
# Prepare login data
|
||||
login_data = {
|
||||
"email": email,
|
||||
"password": password,
|
||||
}
|
||||
if csrf_token:
|
||||
login_data["_csrf"] = csrf_token
|
||||
|
||||
# Perform login
|
||||
response = self.client.post(
|
||||
"/login",
|
||||
data=login_data,
|
||||
allow_redirects=True
|
||||
)
|
||||
|
||||
# Check if login was successful by trying to access /me
|
||||
me_response = self.client.get("/me")
|
||||
if me_response.status_code == 200:
|
||||
user_data = me_response.json()
|
||||
return {
|
||||
"success": True,
|
||||
"user": user_data,
|
||||
"cookies": self.client.get_cookies()
|
||||
}
|
||||
|
||||
# Login failed
|
||||
if response.status_code == 302:
|
||||
# Redirect might indicate failure or success
|
||||
location = response.headers.get("Location", "")
|
||||
if "login" in location.lower():
|
||||
raise PermissionError("Login failed: Invalid credentials")
|
||||
return {
|
||||
"success": True,
|
||||
"cookies": self.client.get_cookies()
|
||||
}
|
||||
|
||||
raise PermissionError(f"Login failed: {response.status_code}")
|
||||
|
||||
def get_me(self) -> Dict[str, Any]:
|
||||
"""Get current user information."""
|
||||
response = self.client.get("/me")
|
||||
|
||||
if response.status_code == 403:
|
||||
raise PermissionError("Not authenticated")
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to get user info: {response.status_code}")
|
||||
|
||||
return response.json()
|
||||
|
||||
def export_my_data(self) -> Dict[str, Any]:
|
||||
"""Export all user data."""
|
||||
response = self.client.post(
|
||||
"/me/export",
|
||||
data={},
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"}
|
||||
)
|
||||
|
||||
if response.status_code == 403:
|
||||
raise PermissionError("Not authenticated")
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to export data: {response.status_code}")
|
||||
|
||||
return response.json()
|
||||
|
||||
def delete_user(self, delete_token: Optional[str] = None) -> bool:
|
||||
"""Delete current user account."""
|
||||
url = f"/me/delete/{delete_token}" if delete_token else "/me/delete"
|
||||
response = self.client.get(url)
|
||||
|
||||
if response.status_code == 403:
|
||||
raise PermissionError("Not authenticated or invalid token")
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to delete user: {response.status_code}")
|
||||
|
||||
return True
|
||||
|
||||
def get_avatar(self, username: str) -> bytes:
|
||||
"""Get user avatar SVG."""
|
||||
response = self.client.get(f"/user/{username}/avatar.svg")
|
||||
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to get avatar: {response.status_code}")
|
||||
|
||||
return response.content
|
||||
Reference in New Issue
Block a user