mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-07 23:01:06 +02:00
124 lines
4.9 KiB
Python
124 lines
4.9 KiB
Python
"""MakerWorld model provider.
|
|
|
|
Static descriptor + per-request service factory for makerworld.com. The
|
|
``MakerWorldProvider`` instance is what gets registered in the shared
|
|
:class:`ModelProviderRegistry`; the actual API work lives in ``service.py``
|
|
(the per-request :class:`ProviderService`) and ``url.py`` (URL parsing and
|
|
canonicalisation). Credential handling is centralised here so route layers
|
|
never touch MakerWorld specifics.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
import httpx
|
|
|
|
from backend.app.core.auth import is_auth_enabled
|
|
from backend.app.core.permissions import Permission
|
|
from backend.app.services.model_providers.base import (
|
|
ModelProvider,
|
|
ProviderAuthConfig,
|
|
ProviderAuthType,
|
|
ProviderResourceRef,
|
|
ProviderService,
|
|
)
|
|
from backend.app.services.model_providers.makerworld import url as mw_url
|
|
from backend.app.services.model_providers.makerworld.auth import (
|
|
get_stored_token,
|
|
mark_cloud_token_invalid,
|
|
)
|
|
from backend.app.services.model_providers.makerworld.http import MAKERWORLD_CDN_HOSTS
|
|
from backend.app.services.model_providers.makerworld.service import MakerWorldService
|
|
|
|
if TYPE_CHECKING:
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from backend.app.models.user import User
|
|
|
|
|
|
class MakerWorldProvider(ModelProvider):
|
|
"""MakerWorld descriptor: identity, URL routing, auth requirements, and the
|
|
factory that builds a per-request :class:`MakerWorldService` seeded with the
|
|
caller's stored Bambu Cloud bearer token.
|
|
"""
|
|
|
|
source_type = "makerworld"
|
|
display_name = "MakerWorld"
|
|
host_patterns = ("makerworld.com",)
|
|
auth = ProviderAuthConfig(
|
|
auth_type=ProviderAuthType.BAMBU_CLOUD_BEARER,
|
|
display_label="Bambu Cloud sign-in",
|
|
description=(
|
|
"MakerWorld downloads reuse the Bambu Cloud account already stored in Bambuddy — "
|
|
"there is no separate MakerWorld sign-in."
|
|
),
|
|
setup_hint="Open the Profiles page and sign in to Bambu Cloud.",
|
|
)
|
|
default_folder_name = "MakerWorld"
|
|
view_permission = Permission.MAKERWORLD_VIEW
|
|
import_permission = Permission.MAKERWORLD_IMPORT
|
|
|
|
async def build_service(
|
|
self,
|
|
*,
|
|
db: AsyncSession,
|
|
user: User | None,
|
|
api_key_owner: User | None = None,
|
|
client: httpx.AsyncClient | None = None,
|
|
) -> ProviderService:
|
|
"""Build a per-request service seeded with the caller's stored Bambu
|
|
Cloud bearer, mirroring ``cloud.build_authenticated_cloud``.
|
|
|
|
``api_key_owner`` is the API key's owning user for API-keyed calls
|
|
(see ``resolve_api_key_cloud_owner``); MakerWorld uses it as the
|
|
fallback identity when ``user`` is None. Like the cloud integration, a
|
|
rejected token is recorded so the whole app agrees the sign-in is dead
|
|
rather than each feature failing on its own — including auth-disabled
|
|
single-user installs, where ``user_id=None`` records the *global*
|
|
flag those installs read back on the status endpoints.
|
|
"""
|
|
identity = user if user is not None else api_key_owner
|
|
# Without an identity, the stored sign-in is the auth-off install's.
|
|
# With auth on (an API key without Allow Cloud Access) there is none.
|
|
if identity is None and await is_auth_enabled(db):
|
|
token = None
|
|
else:
|
|
token, _email, _region = await get_stored_token(db, identity)
|
|
user_id = identity.id if identity is not None else None
|
|
return MakerWorldService(
|
|
client=client,
|
|
auth_token=token,
|
|
user=identity,
|
|
on_auth_failure=lambda: mark_cloud_token_invalid(user_id),
|
|
# The SSRF allowlists are the provider's declared seams — the
|
|
# service must not hardcode its own copies (symmetric pair,
|
|
# ``fetch_thumbnail`` / ``download``).
|
|
thumbnail_hosts=self.thumbnail_hosts(),
|
|
download_hosts=self.download_hosts(),
|
|
)
|
|
|
|
def parse_url(self, url: str) -> ProviderResourceRef:
|
|
return mw_url.parse_url(url)
|
|
|
|
def canonical_url(self, ref: ProviderResourceRef) -> str:
|
|
return mw_url.canonical_url(ref)
|
|
|
|
def source_url_filter(self, column, external_id: str):
|
|
"""Whole-model key plus every per-plate key — MakerWorld's canonical
|
|
shape appends ``#profileId-{n}`` for plate-level dedupe (see
|
|
``url.canonical_url``), so the already-imported detection must match
|
|
both. The ``#profileId-`` fragment lives here with the descriptor
|
|
because it is part of this provider's URL contract."""
|
|
prefix = mw_url.canonical_url(ProviderResourceRef(source_type=self.source_type, external_id=external_id))
|
|
return (column == prefix) | (column.like(f"{prefix}#profileId-%"))
|
|
|
|
def thumbnail_hosts(self) -> tuple[str, ...]:
|
|
return MAKERWORLD_CDN_HOSTS
|
|
|
|
def download_hosts(self) -> tuple[str, ...]:
|
|
return MAKERWORLD_CDN_HOSTS
|
|
|
|
|
|
makerworld_provider = MakerWorldProvider()
|