|
# retoor <retoor@molodetz.nl>
|
|
|
|
import asyncio
|
|
import base64
|
|
import json
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
from io import BytesIO
|
|
|
|
from devplacepy import stealth
|
|
from devplacepy.attachments import link_attachments, store_attachment
|
|
from devplacepy.awards.images import enforce_rgba_png, resize_award_png
|
|
from devplacepy.config import (
|
|
AWARD_GENERATION_TIMEOUT_SECONDS,
|
|
AWARD_IMAGE_MODEL_DEFAULT,
|
|
AWARD_IMAGE_PROMPT_DEFAULT,
|
|
AWARD_IMAGE_SIZE_DEFAULT,
|
|
INTERNAL_BASE_URL,
|
|
)
|
|
from devplacepy.database import add_award_usage, get_award_usage, get_setting, get_table
|
|
from devplacepy.database.awards import recompute_user_award_stats
|
|
from devplacepy.services.base import ConfigField
|
|
from devplacepy.services.correction import new_usage_totals
|
|
from devplacepy.services.jobs.base import JobService
|
|
from devplacepy.services.openai_gateway.usage import accumulate_usage, usage_metric_cards
|
|
from devplacepy.utils import create_notification
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AwardService(JobService):
|
|
kind = "award"
|
|
title = "Awards"
|
|
description = (
|
|
"Generates award images off the request path, stores PNG attachments, and "
|
|
"notifies receivers when ready."
|
|
)
|
|
|
|
def __init__(self):
|
|
super().__init__(name="award", interval_seconds=2)
|
|
self.config_fields = list(self.config_fields) + [
|
|
ConfigField(
|
|
"award_give_cooldown_hours",
|
|
"Give cooldown (hours)",
|
|
type="int",
|
|
default=24,
|
|
minimum=1,
|
|
maximum=168,
|
|
group="Awards",
|
|
),
|
|
ConfigField(
|
|
"award_receive_cooldown_hours",
|
|
"Receive cooldown (hours)",
|
|
type="int",
|
|
default=24,
|
|
minimum=1,
|
|
maximum=168,
|
|
group="Awards",
|
|
),
|
|
ConfigField(
|
|
"award_display_hours",
|
|
"Prominent display (hours)",
|
|
type="int",
|
|
default=24,
|
|
minimum=1,
|
|
maximum=168,
|
|
group="Awards",
|
|
),
|
|
ConfigField(
|
|
"award_image_model",
|
|
"Image model",
|
|
type="str",
|
|
default=AWARD_IMAGE_MODEL_DEFAULT,
|
|
group="Awards",
|
|
),
|
|
ConfigField(
|
|
"award_image_prompt",
|
|
"Image prompt",
|
|
type="text",
|
|
default=AWARD_IMAGE_PROMPT_DEFAULT,
|
|
group="Awards",
|
|
),
|
|
]
|
|
|
|
async def process(self, job: dict) -> dict:
|
|
totals = new_usage_totals()
|
|
try:
|
|
result = await asyncio.to_thread(self._generate, job, totals)
|
|
except Exception as exc:
|
|
self._audit_failed(job, str(exc))
|
|
raise
|
|
if totals["calls"]:
|
|
add_award_usage(totals)
|
|
return result
|
|
|
|
def _generate(self, job: dict, totals: dict) -> dict:
|
|
from devplacepy.services.audit import record as audit
|
|
|
|
payload = job.get("payload") or {}
|
|
award_uid = str(payload.get("award_uid", ""))
|
|
giver_uid = str(payload.get("giver_uid", ""))
|
|
receiver_uid = str(payload.get("receiver_uid", ""))
|
|
description = str(payload.get("description", ""))
|
|
api_key = str(payload.get("api_key", ""))
|
|
awards = get_table("awards")
|
|
row = awards.find_one(uid=award_uid)
|
|
if not row or row.get("deleted_at") or row.get("generated_at"):
|
|
return {"award_uid": award_uid, "skipped": True}
|
|
source_png = self._fetch_image(api_key, description, totals)
|
|
source_png = enforce_rgba_png(source_png)
|
|
uid512 = store_attachment(
|
|
resize_award_png(source_png, 512), "award-512.png", receiver_uid
|
|
)["uid"]
|
|
uid256 = store_attachment(
|
|
resize_award_png(source_png, 256), "award-256.png", giver_uid
|
|
)["uid"]
|
|
uid64 = store_attachment(
|
|
resize_award_png(source_png, 64), "award-64.png", giver_uid
|
|
)["uid"]
|
|
link_attachments([uid512, uid256, uid64], "award", award_uid)
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
awards.update(
|
|
{
|
|
"uid": award_uid,
|
|
"attachment_uid_512": uid512,
|
|
"attachment_uid_256": uid256,
|
|
"attachment_uid_64": uid64,
|
|
"generated_at": now,
|
|
},
|
|
["uid"],
|
|
)
|
|
recompute_user_award_stats(receiver_uid)
|
|
users = get_table("users")
|
|
giver = users.find_one(uid=giver_uid) or {}
|
|
receiver = users.find_one(uid=receiver_uid) or {}
|
|
slug = row.get("slug", "")
|
|
create_notification(
|
|
receiver_uid,
|
|
"award",
|
|
f"@{giver.get('username', 'someone')} gave you an award",
|
|
giver_uid,
|
|
f"/profile/{receiver.get('username', '')}?tab=awards#award-{slug}",
|
|
)
|
|
audit.record_system(
|
|
"award.complete",
|
|
actor_kind="service",
|
|
target_type="award",
|
|
target_uid=award_uid,
|
|
target_label=description[:80],
|
|
summary=f"Award ready for {receiver.get('username', receiver_uid)}",
|
|
links=[
|
|
audit.target("award", award_uid, slug),
|
|
audit.target("user", receiver_uid, receiver.get("username")),
|
|
audit.target("user", giver_uid, giver.get("username")),
|
|
audit.job(job.get("uid", "")),
|
|
],
|
|
)
|
|
return {"award_uid": award_uid, "slug": slug}
|
|
|
|
def _fetch_image(self, api_key: str, description: str, totals: dict) -> bytes:
|
|
prompt = get_setting("award_image_prompt", AWARD_IMAGE_PROMPT_DEFAULT)
|
|
model = get_setting("award_image_model", AWARD_IMAGE_MODEL_DEFAULT)
|
|
size = AWARD_IMAGE_SIZE_DEFAULT
|
|
body = {
|
|
"model": model,
|
|
"prompt": f"{prompt}\n\n{description}".strip(),
|
|
"size": size,
|
|
"background": "transparent",
|
|
"output_format": "png",
|
|
}
|
|
url = f"{INTERNAL_BASE_URL}/openai/v1/images/generations"
|
|
headers = {
|
|
"Content-Type": "application/json",
|
|
"X-App-Reference": "devplace-awards-v-1-0-0",
|
|
}
|
|
if api_key:
|
|
headers["Authorization"] = f"Bearer {api_key}"
|
|
with stealth.stealth_sync_client(timeout=AWARD_GENERATION_TIMEOUT_SECONDS) as client:
|
|
response = client.post(url, json=body, headers=headers)
|
|
accumulate_usage(totals, response)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
items = data.get("data") or []
|
|
if not items:
|
|
raise RuntimeError("image API returned no data")
|
|
encoded = items[0].get("b64_json") or ""
|
|
if not encoded:
|
|
raise RuntimeError("image API returned no b64_json")
|
|
return base64.b64decode(encoded)
|
|
|
|
def _audit_failed(self, job: dict, error: str) -> None:
|
|
from devplacepy.services.audit import record as audit
|
|
|
|
payload = job.get("payload") or {}
|
|
award_uid = str(payload.get("award_uid", ""))
|
|
audit.record_system(
|
|
"award.failed",
|
|
actor_kind="service",
|
|
target_type="award",
|
|
target_uid=award_uid,
|
|
summary=f"Award generation failed: {error[:80]}",
|
|
metadata={"error": error[:500], "award_uid": award_uid},
|
|
result="failure",
|
|
links=[audit.job(job.get("uid", "")), audit.target("award", award_uid)],
|
|
)
|
|
|
|
def collect_metrics(self) -> dict:
|
|
base = super().collect_metrics()
|
|
base["stats"] = base.get("stats", []) + usage_metric_cards(get_award_usage())
|
|
return base |