mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-09-22 23:08:08 +03:00
147 lines
4.7 KiB
Python
147 lines
4.7 KiB
Python
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||
#
|
||
# This file is part of Furious.
|
||
#
|
||
# This program is free software: you can redistribute it and/or modify
|
||
# it under the terms of the GNU General Public License as published by
|
||
# the Free Software Foundation, either version 3 of the License, or
|
||
# (at your option) any later version.
|
||
#
|
||
# This program is distributed in the hope that it will be useful,
|
||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
# GNU General Public License for more details.
|
||
#
|
||
# You should have received a copy of the GNU General Public License
|
||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
|
||
"""Convert decoded subscription payloads into metadata-separated profiles."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from Furious.Models import ServerProfile, profileConnectionFingerprint
|
||
from Furious.Plugins import getPluginRegistry, profileFromAny
|
||
|
||
from dataclasses import dataclass, field
|
||
from datetime import datetime, timezone
|
||
from typing import Optional, Tuple
|
||
|
||
__all__ = [
|
||
'SubscriptionImportResult',
|
||
'SubscriptionImportService',
|
||
'SubscriptionSource',
|
||
'SubscriptionWorkerUnsafe',
|
||
]
|
||
|
||
|
||
class SubscriptionWorkerUnsafe(RuntimeError):
|
||
"""Signal that one plugin capability must remain on the GUI thread."""
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class SubscriptionSource:
|
||
"""Describe where a subscription payload came from."""
|
||
|
||
id: str
|
||
location: str = ''
|
||
displayName: str = ''
|
||
decoderId: Optional[str] = None
|
||
updatedAt: str = field(
|
||
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
||
)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class SubscriptionImportResult:
|
||
"""Return profiles generated from one decoded subscription payload."""
|
||
|
||
decoderId: str
|
||
profiles: Tuple[ServerProfile, ...]
|
||
rejectedItems: int = 0
|
||
|
||
|
||
class SubscriptionImportService:
|
||
"""Separate payload decoding from protocol profile construction."""
|
||
|
||
def __init__(self, registry=None):
|
||
"""Use the supplied capability registry or the application registry."""
|
||
self.registry = registry or getPluginRegistry()
|
||
|
||
def importPayload(
|
||
self,
|
||
data: bytes,
|
||
source: SubscriptionSource,
|
||
*,
|
||
requireWorkerSafe: bool = False,
|
||
isCancelled=None,
|
||
):
|
||
"""Decode *data* and construct supported profiles for *source*."""
|
||
if requireWorkerSafe and not self.registry.subscriptionDecoderWorkerSafe(
|
||
source.decoderId
|
||
):
|
||
raise SubscriptionWorkerUnsafe(
|
||
'subscription capabilities did not opt in to worker execution'
|
||
)
|
||
|
||
if callable(isCancelled) and isCancelled():
|
||
return None
|
||
|
||
result = self.registry.decodeSubscription(data, source.decoderId)
|
||
|
||
if result is None:
|
||
return None
|
||
|
||
profiles = []
|
||
rejected = 0
|
||
identityOccurrences = {}
|
||
|
||
for item in result.items:
|
||
if callable(isCancelled) and isCancelled():
|
||
return None
|
||
|
||
if requireWorkerSafe and not self.registry.subscriptionItemWorkerSafe(item):
|
||
raise SubscriptionWorkerUnsafe(
|
||
'subscription protocol capability did not opt in to worker execution'
|
||
)
|
||
|
||
value = item.configuration if item.configuration is not None else item.uri
|
||
metadata = {
|
||
**dict(item.metadata),
|
||
'subscriptionSource': source.id,
|
||
'subscriptionManaged': True,
|
||
'updatedAt': source.updatedAt,
|
||
}
|
||
|
||
if item.name:
|
||
metadata['displayName'] = item.name
|
||
|
||
profile = profileFromAny(
|
||
value,
|
||
registry=self.registry,
|
||
**metadata,
|
||
)
|
||
handler = self.registry.handlerForConfig(profile)
|
||
|
||
if handler is not None and not handler.descriptor.subscriptionImportable:
|
||
rejected += 1
|
||
|
||
continue
|
||
|
||
baseIdentity = (
|
||
f'upstream:{item.upstreamId}'
|
||
if item.upstreamId
|
||
else f'config:{profileConnectionFingerprint(profile)}'
|
||
)
|
||
occurrence = identityOccurrences.get(baseIdentity, 0)
|
||
identityOccurrences[baseIdentity] = occurrence + 1
|
||
profile.metadata.subscriptionProfileKey = (
|
||
baseIdentity if occurrence == 0 else f'{baseIdentity}#{occurrence + 1}'
|
||
)
|
||
|
||
if not self.registry.validateConfig(profile):
|
||
profiles.append(profile)
|
||
else:
|
||
rejected += 1
|
||
|
||
return SubscriptionImportResult(result.decoderId, tuple(profiles), rejected)
|