import secrets from django.conf import settings from django.db import models def generate_api_key(): """`dbk_` prefix (recognizable as a AgentFeed key) + 40 hex chars.""" return "dbk_" + secrets.token_hex(20) class Artifact(models.Model): HTML_FRAGMENT = "html_fragment" HTML_FULL = "html_full" MARKDOWN = "markdown" DECK = "deck" IMAGE = "image" PDF = "pdf" BINARY = "binary" JSON = "json" CSV = "csv" AGENT = "agent" PERSON = "person" SYSTEM = "system" KIND_CHOICES = [ (HTML_FRAGMENT, "HTML fragment"), (HTML_FULL, "Full HTML"), (MARKDOWN, "Markdown"), (DECK, "Deck"), (IMAGE, "Image"), (PDF, "PDF"), (BINARY, "Binary"), (JSON, "JSON"), (CSV, "CSV"), ] SOURCE_TYPE_CHOICES = [ (AGENT, "Agent"), (PERSON, "Person"), (SYSTEM, "System"), ] id = models.CharField(max_length=8, primary_key=True) owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="artifacts") source = models.CharField(max_length=80, db_index=True) title = models.CharField(max_length=200) kind = models.CharField(max_length=20, choices=KIND_CHOICES) theme = models.CharField(max_length=40, blank=True) day = models.DateField(db_index=True) created_at = models.DateTimeField(auto_now_add=True) path = models.CharField(max_length=500) original_kind = models.CharField(max_length=20) tags = models.JSONField(default=list, blank=True) source_prompt = models.TextField(blank=True) source_type = models.CharField(max_length=20, choices=SOURCE_TYPE_CHOICES, default=AGENT) source_name = models.CharField(max_length=120, blank=True) source_id = models.CharField(max_length=120, blank=True) metadata = models.JSONField(default=dict, blank=True) read_at = models.DateTimeField(null=True, blank=True) is_kept = models.BooleanField(default=False) # Soft-delete: non-null = in Trash (hidden from feed/sources, off-quota, # recoverable via undelete, purged by Empty Trash). NULL = live. deleted_at = models.DateTimeField(null=True, blank=True, db_index=True) size_bytes = models.BigIntegerField(default=0) # NULL = not shared. Many rows share NULL (SQL treats each NULL as # distinct, so `unique=True` still holds); a real value must be unique # since it's the sole credential for the unauthenticated /s/ route. share_token = models.CharField(max_length=64, null=True, blank=True, unique=True, default=None) @property def is_unread(self): return self.read_at is None @property def is_shared(self): return bool(self.share_token) @property def is_deleted(self): return self.deleted_at is not None class Meta: ordering = ["-created_at"] def __str__(self): return f"{self.title} ({self.source})" class TagColor(models.Model): name = models.CharField(max_length=40, primary_key=True) color = models.CharField(max_length=20) def __str__(self): return f"{self.name} ({self.color})" class UserProfile(models.Model): DEFAULT_QUOTA_BYTES = 1024 * 1024 * 1024 # 1 GB user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="profile") quota_bytes = models.BigIntegerField(default=DEFAULT_QUOTA_BYTES) def __str__(self): return f"{self.user} quota={self.quota_bytes}" class ApiKey(models.Model): """A named, per-user API key for ingest. A user can hold several — one per agent or script — and revoke them individually. Stored and shown in plaintext: this is a self-hosted, single-operator app and keys are viewable on demand by design. """ user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="api_keys" ) name = models.CharField(max_length=100) key = models.CharField(max_length=64, unique=True, default=generate_api_key, db_index=True) created_at = models.DateTimeField(auto_now_add=True) last_used_at = models.DateTimeField(null=True, blank=True) class Meta: ordering = ["created_at"] def __str__(self): return f"{self.name} ({self.user})" class Asset(models.Model): """An image uploaded for embedding inside an artifact's content. Uploaded before the artifact exists, so `artifact` is null until the artifact referencing its URL is created. Unattached assets are swept after ASSET_ORPHAN_GRACE; attached ones die with their artifact. """ id = models.CharField(max_length=12, primary_key=True) owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="assets") artifact = models.ForeignKey( Artifact, on_delete=models.CASCADE, related_name="assets", null=True, blank=True ) path = models.CharField(max_length=500) content_type = models.CharField(max_length=100) size_bytes = models.BigIntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["-created_at"] def __str__(self): return f"{self.id} ({self.content_type})"