Add FCM device registration and Gitea bug reporter

This commit is contained in:
2026-09-15 01:01:08 +02:00
parent b2af866cf0
commit b96b1890d2
33 changed files with 1232 additions and 18 deletions
+121
View File
@@ -0,0 +1,121 @@
"""The only component allowed to contact Gitea. Never logs payloads or credentials."""
from dataclasses import dataclass, field
import logging
import os
import re
from urllib.parse import quote, urlsplit
import httpx
logger = logging.getLogger(__name__)
class GiteaError(Exception):
def __init__(self, kind, uncertain=False):
self.kind = kind
self.uncertain = uncertain
super().__init__(kind)
def redact(text, secrets=()):
for secret in secrets:
if secret and len(secret) >= 8:
text = text.replace(secret, '[REDACTED]')
text = re.sub(r'-----BEGIN [^-]*PRIVATE KEY-----.*?-----END [^-]*PRIVATE KEY-----',
'[REDACTED PRIVATE KEY]', text, flags=re.S)
text = re.sub(r'(?im)((?:password|passwd|authorization|cookie|[\w]*(?:token|secret|credential|api_key)[\w]*)\s*[:=]\s*)[^\r\n]+',
r'\1[REDACTED]', text)
text = re.sub(r'[A-Za-z0-9_-]{20,}:[A-Za-z0-9_-]{80,}', '[REDACTED FCM TOKEN]', text)
text = re.sub(r'(https?://)[^/\s:@]+:[^/\s@]+@', r'\1[REDACTED]@', text)
return text.replace('\x00', '')
@dataclass(frozen=True)
class GiteaConfig:
url: str
token: str = field(repr=False)
owner: str
repo: str
@classmethod
def from_env(cls):
return cls(*(os.environ.get(key, '').strip() for key in
('GITEA_URL', 'GITEA_TOKEN', 'GITEA_OWNER', 'GITEA_REPO')))
def validate(self):
try:
parsed = urlsplit(self.url)
except ValueError:
raise GiteaError('configuration') from None
if (not all((self.url, self.token, self.owner, self.repo)) or
parsed.scheme not in ('http', 'https') or not parsed.netloc or
parsed.username or parsed.password or parsed.query or parsed.fragment or
not re.fullmatch(r'[A-Za-z0-9_.-]+', self.owner) or
not re.fullmatch(r'[A-Za-z0-9_.-]+', self.repo)):
raise GiteaError('configuration')
class GiteaService:
def __init__(self, config=None, transport=None):
self.config = config or GiteaConfig.from_env()
self.transport = transport
def create_issue(self, title, body, category):
self.config.validate()
repository = '/repos/' + quote(self.config.owner, safe='') + '/' + quote(self.config.repo, safe='')
wanted = {'reported-from-metalcircle', 'bug'}
wanted.update({'android': {'android'}, 'web': {'web', 'frontend'}, 'push': {'push'}}.get(category, set()))
attempted = False
try:
with httpx.Client(base_url=self.config.url.rstrip('/') + '/api/v1/',
headers={'Authorization': 'token ' + self.config.token, 'Accept': 'application/json'},
timeout=httpx.Timeout(15.0, connect=5.0), follow_redirects=False,
transport=self.transport, trust_env=False) as client:
# Fail closed if a personal account token is accidentally configured.
identity = client.get('user')
identity.raise_for_status()
if identity.json().get('login') != 'metalcircle-bot':
raise GiteaError('wrong_account')
labels = []
try:
for page in range(1, 11):
result = client.get(repository.lstrip('/') + '/labels', params={'limit': 50, 'page': page})
result.raise_for_status()
items = result.json()
if not isinstance(items, list):
break
labels.extend(item['id'] for item in items if isinstance(item, dict) and
isinstance(item.get('name'), str) and
item.get('name', '').lower() in wanted and type(item.get('id')) is int)
if len(items) < 50:
break
except (httpx.HTTPError, ValueError, TypeError, KeyError):
# Labels are optional; permissions/missing labels must not block a report.
labels = []
attempted = True
result = client.post(repository.lstrip('/') + '/issues', json={
'title': redact('[MetalCircle Bug] ' + title, (self.config.token,)),
'body': redact(body, (self.config.token,)), 'labels': sorted(set(labels)),
})
result.raise_for_status()
issue = result.json()
if (not isinstance(issue, dict) or type(issue.get('number')) is not int or
issue['number'] <= 0 or not isinstance(issue.get('user'), dict) or
issue['user'].get('login') != 'metalcircle-bot'):
raise GiteaError('invalid_response', uncertain=True)
return issue['number']
except GiteaError as error:
logger.warning('Gitea issue submission failed: %s', error.kind)
raise
except httpx.HTTPStatusError as error:
code = error.response.status_code
logger.warning('Gitea issue submission failed: HTTP %d', code)
raise GiteaError('http_' + str(code), uncertain=attempted and code >= 500) from None
except (httpx.TimeoutException, httpx.NetworkError, httpx.RemoteProtocolError):
logger.warning('Gitea issue submission failed: network_or_timeout')
raise GiteaError('network_or_timeout', uncertain=attempted) from None
except (ValueError, TypeError, KeyError, AttributeError, httpx.HTTPError):
logger.warning('Gitea issue submission failed: invalid_response')
raise GiteaError('invalid_response', uncertain=attempted) from None