mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
An expired token was indistinguishable from a working one. set_token()
stamped token_expiry = now + 30 days every time a stored token was loaded,
so the expiry reset on every request and is_authenticated could never
return False. /cloud/status answered "connected" for as long as any token
existed, while every cloud call 401'd — and the user was shown Bambu's own
{"error": "Please login."} verbatim.
Bambu is now the authority: /cloud/status validates the token upstream
(cached 5m), and any 401 from any authenticated call durably records the
credential as dead via users.cloud_token_invalid_at, so MakerWorld, cloud
profiles, slicer presets and firmware checks all agree at once. An
unreachable Bambu is treated as unknown, never as expired, so an outage
cannot sign a working session out.
The user-facing message now names the Profiles page, where the Bambu Cloud
sign-in actually lives; the old text pointed at a Settings page that does
not exist. Same stale path corrected in the wiki.
142 lines
4.6 KiB
Python
142 lines
4.6 KiB
Python
from typing import Literal
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
Region = Literal["global", "china"]
|
|
|
|
|
|
class CloudLoginRequest(BaseModel):
|
|
"""Request to initiate cloud login."""
|
|
|
|
email: str = Field(..., description="Bambu Lab account email")
|
|
password: str = Field(..., description="Account password")
|
|
region: Region = Field(default="global", description="Region: 'global' or 'china'")
|
|
|
|
|
|
class CloudVerifyRequest(BaseModel):
|
|
"""Request to verify login with 2FA code (email or TOTP)."""
|
|
|
|
email: str = Field(..., description="Bambu Lab account email")
|
|
code: str = Field(..., description="6-digit verification code")
|
|
tfa_key: str | None = Field(None, description="TFA key for TOTP verification (from login response)")
|
|
region: Region = Field(default="global", description="Region: 'global' or 'china'")
|
|
|
|
|
|
class CloudLoginResponse(BaseModel):
|
|
"""Response from login attempt."""
|
|
|
|
success: bool
|
|
needs_verification: bool = False
|
|
message: str
|
|
verification_type: str | None = None # "email" or "totp"
|
|
tfa_key: str | None = None # Key needed for TOTP verification
|
|
|
|
|
|
class CloudAuthStatus(BaseModel):
|
|
"""Current authentication status."""
|
|
|
|
is_authenticated: bool
|
|
email: str | None = None
|
|
region: Region | None = None
|
|
# True when a token is stored but Bambu no longer accepts it. Both this and
|
|
# "never signed in" render the login form, but only this one warrants
|
|
# telling the user why it came back.
|
|
sign_in_expired: bool = False
|
|
|
|
|
|
class CloudTokenRequest(BaseModel):
|
|
"""Request to set access token directly."""
|
|
|
|
access_token: str = Field(..., description="Bambu Lab access token")
|
|
region: Region = Field(default="global", description="Region: 'global' or 'china'")
|
|
|
|
|
|
class SlicerSetting(BaseModel):
|
|
"""A slicer setting/preset."""
|
|
|
|
setting_id: str
|
|
name: str
|
|
type: str # filament, printer, process
|
|
version: str | None = None
|
|
user_id: str | None = None
|
|
updated_time: str | None = None
|
|
is_custom: bool = False
|
|
|
|
|
|
class SlicerSettingsResponse(BaseModel):
|
|
"""Response containing slicer settings."""
|
|
|
|
filament: list[SlicerSetting] = []
|
|
printer: list[SlicerSetting] = []
|
|
process: list[SlicerSetting] = []
|
|
|
|
|
|
class CloudDevice(BaseModel):
|
|
"""A bound printer device."""
|
|
|
|
dev_id: str
|
|
name: str
|
|
dev_model_name: str | None = None
|
|
dev_product_name: str | None = None
|
|
online: bool = False
|
|
|
|
|
|
class SlicerSettingCreate(BaseModel):
|
|
"""Request to create a new slicer preset."""
|
|
|
|
type: str = Field(..., description="Preset type: 'filament', 'print', or 'printer'")
|
|
name: str = Field(..., description="Display name for the preset")
|
|
base_id: str = Field(..., description="Base preset ID to inherit from")
|
|
version: str = Field(default="2.0.0.0", description="Version string for the preset")
|
|
setting: dict = Field(default_factory=dict, description="Setting key-value pairs (delta from base)")
|
|
|
|
|
|
class SlicerSettingUpdate(BaseModel):
|
|
"""Request to update an existing slicer preset."""
|
|
|
|
name: str | None = Field(None, description="New display name")
|
|
setting: dict | None = Field(None, description="Setting key-value pairs to update")
|
|
|
|
|
|
class SlicerSettingDetail(BaseModel):
|
|
"""Detailed slicer setting/preset response."""
|
|
|
|
message: str | None = None
|
|
code: str | None = None
|
|
error: str | None = None
|
|
public: bool = False
|
|
version: str | None = None
|
|
type: str
|
|
name: str
|
|
update_time: str | None = None
|
|
nickname: str | None = None
|
|
base_id: str | None = None
|
|
setting: dict = Field(default_factory=dict)
|
|
filament_id: str | None = None
|
|
setting_id: str | None = None # For response after create
|
|
|
|
|
|
class SlicerSettingDeleteResponse(BaseModel):
|
|
"""Response from deleting a preset."""
|
|
|
|
success: bool
|
|
message: str
|
|
|
|
|
|
class FirmwareUpdateInfo(BaseModel):
|
|
"""Firmware update information for a device."""
|
|
|
|
device_id: str = Field(..., description="Device ID")
|
|
device_name: str = Field(..., description="Device name")
|
|
current_version: str | None = Field(None, description="Currently installed firmware version")
|
|
latest_version: str | None = Field(None, description="Latest available firmware version")
|
|
update_available: bool = Field(False, description="Whether an update is available")
|
|
release_notes: str | None = Field(None, description="Release notes for the latest version")
|
|
|
|
|
|
class FirmwareUpdatesResponse(BaseModel):
|
|
"""Response containing firmware updates for all devices."""
|
|
|
|
updates: list[FirmwareUpdateInfo] = Field(default_factory=list)
|
|
updates_available: int = Field(0, description="Total number of devices with updates available")
|