import json import requests from django.db import models from django.core.validators import MinValueValidator from django.utils import timezone from pxy_langchain.models import AIAssistant # LangChain assistant # Telegram bot + simple conversation log class TelegramBot(models.Model): name = models.CharField(max_length=50, unique=True, help_text="Bot name (e.g., 'SupportBot').") username = models.CharField(max_length=50, unique=True, help_text="Bot username (e.g., 'SupportBot').") token = models.CharField(max_length=200, unique=True, help_text="Telegram bot token.") is_active = models.BooleanField(default=True, help_text="If off, webhook can be refused.") assistant = models.ForeignKey( AIAssistant, on_delete=models.CASCADE, related_name="telegram_bots", help_text="LangChain AI assistant associated with this bot.", ) created_at = models.DateTimeField(default=timezone.now, editable=False) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ["name"] def __str__(self): return f"{self.name} (@{self.username})" @staticmethod def get_bot_token(bot_name: str) -> str: try: bot = TelegramBot.objects.get(name=bot_name, is_active=True) return bot.token except TelegramBot.DoesNotExist: raise ValueError(f"Bot with name '{bot_name}' not found or inactive.") def set_webhook(self, base_url: str) -> dict: if not self.is_active: raise ValueError(f"Bot '{self.name}' is inactive. Activate it before setting the webhook.") webhook_url = f"{base_url.rstrip('/')}/bots/webhook/{self.name}/" resp = requests.post( f"https://api.telegram.org/bot{self.token}/setWebhook", data={"url": webhook_url}, timeout=5, ) resp.raise_for_status() return resp.json() class TelegramConversation(models.Model): bot = models.ForeignKey(TelegramBot, on_delete=models.CASCADE, related_name='conversations') user_id = models.CharField(max_length=64) started_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["-started_at"] def __str__(self): return f"{self.user_id} @ {self.started_at:%Y-%m-%d %H:%M}" class TelegramMessage(models.Model): IN = 'in' OUT = 'out' DIRECTION_CHOICES = [(IN, 'In'), (OUT, 'Out')] conversation = models.ForeignKey(TelegramConversation, on_delete=models.CASCADE, related_name='messages') direction = models.CharField(max_length=4, choices=DIRECTION_CHOICES) content = models.TextField() timestamp = models.DateTimeField(auto_now_add=True) response_time_ms = models.IntegerField(null=True, blank=True) class Meta: ordering = ["timestamp", "id"] def __str__(self): return f"[{self.direction}] {self.content[:30]}…" # Configurable routing (Admin-driven) class Connection(models.Model): AUTH_NONE = "none" AUTH_BEARER = "bearer" AUTH_API_KEY = "api_key" AUTH_BASIC = "basic" AUTH_CHOICES = [ (AUTH_NONE, "None"), (AUTH_BEARER, "Bearer token"), (AUTH_API_KEY, "API key (in header)"), (AUTH_BASIC, "Basic user:pass"), ] name = models.CharField(max_length=120, unique=True) base_url = models.CharField(max_length=500, help_text="e.g. https://api.example.com") path_default = models.CharField(max_length=300, blank=True, default="", help_text="Optional default path, e.g. /bots/route") auth_type = models.CharField(max_length=20, choices=AUTH_CHOICES, default=AUTH_NONE) auth_value = models.CharField(max_length=500, blank=True, default="", help_text="token | key | user:pass") headers_json = models.TextField(blank=True, default="", help_text='Extra headers as JSON object') timeout_s = models.PositiveIntegerField(default=4, validators=[MinValueValidator(1)]) allowed_hosts = models.CharField( max_length=800, blank=True, default="127.0.0.1,localhost,app.polisplexity.tech", help_text="Comma-separated host allowlist for safety." ) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(default=timezone.now, editable=False) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ["name"] def __str__(self): return self.name def allowed_host_set(self): return {h.strip().lower() for h in (self.allowed_hosts or "").split(",") if h.strip()} def extra_headers(self): if not self.headers_json: return {} try: obj = json.loads(self.headers_json) return obj if isinstance(obj, dict) else {} except Exception: return {} def auth_headers(self): h = {} if self.auth_type == self.AUTH_BEARER and self.auth_value: h["Authorization"] = f"Bearer {self.auth_value}" elif self.auth_type == self.AUTH_API_KEY and self.auth_value: h["X-API-Key"] = self.auth_value # convention; adjust if needed elif self.auth_type == self.AUTH_BASIC and self.auth_value: h["Authorization"] = f"Basic {self.auth_value}" # store user:pass return h class CommandRoute(models.Model): TRIG_MESSAGE = "message" TRIG_TEXTCMD = "text_command" TRIG_CALLBACK = "callback" TRIGGER_CHOICES = [ (TRIG_MESSAGE, "Message (no command)"), (TRIG_TEXTCMD, "Text command (/cmd)"), (TRIG_CALLBACK, "Callback"), ] bot = models.ForeignKey("pxy_bots.TelegramBot", on_delete=models.CASCADE, related_name="routes") trigger = models.CharField(max_length=20, choices=TRIGGER_CHOICES, default=TRIG_MESSAGE) command_name = models.CharField( max_length=80, blank=True, null=True, help_text="Without leading '/'. Leave blank for default of that trigger." ) connection = models.ForeignKey(Connection, on_delete=models.PROTECT, related_name="routes") path = models.CharField(max_length=300, blank=True, default="", help_text="Overrides connection.path_default if set") enabled = models.BooleanField(default=True) priority = models.PositiveIntegerField(default=100, help_text="Lower runs first") note = models.CharField(max_length=240, blank=True, default="") created_at = models.DateTimeField(default=timezone.now, editable=False) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ["priority", "id"] indexes = [ models.Index(fields=["bot", "trigger", "command_name", "enabled", "priority"]), ] def __str__(self): cmd = self.command_name or "(default)" return f"{self.bot.name} · {self.trigger} · {cmd} → {self.connection.name}" def clean(self): if self.command_name: self.command_name = self.command_name.strip().lstrip("/").lower() # --- Admin-configurable reply templates -------------------------------------- from django.core.validators import MinValueValidator from django.utils import timezone import json class BotReplyTemplate(models.Model): PARSE_NONE = "none" PARSE_MD = "markdown" PARSE_HTML = "html" PARSE_CHOICES = [ (PARSE_NONE, "None"), (PARSE_MD, "Markdown"), (PARSE_HTML, "HTML"), ] TRIG_MESSAGE = "message" TRIG_TEXTCMD = "text_command" TRIG_CALLBACK = "callback" TRIGGER_CHOICES = [ (TRIG_MESSAGE, "Message (no command)"), (TRIG_TEXTCMD, "Text command (/cmd)"), (TRIG_CALLBACK, "Callback"), ] bot = models.ForeignKey("pxy_bots.TelegramBot", on_delete=models.CASCADE, related_name="reply_templates") trigger = models.CharField(max_length=20, choices=TRIGGER_CHOICES, default=TRIG_MESSAGE) command_name = models.CharField(max_length=80, blank=True, null=True, help_text="Without leading '/'. Blank = default for this trigger.") # Content text_template = models.TextField(blank=True, default="", help_text="Use ${args}, ${user_id}, ${lat}, ${lon}, etc.") parse_mode = models.CharField(max_length=20, choices=PARSE_CHOICES, default=PARSE_MD) media_url = models.CharField(max_length=600, blank=True, default="", help_text="Optional image/video URL") buttons_json = models.TextField(blank=True, default="", help_text='Optional JSON: [{"label":"Open","kind":"open_url","url":"https://..."}]') enabled = models.BooleanField(default=True) priority = models.PositiveIntegerField(default=100, validators=[MinValueValidator(1)], help_text="Lower runs first") note = models.CharField(max_length=240, blank=True, default="") created_at = models.DateTimeField(default=timezone.now, editable=False) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ["priority", "id"] indexes = [models.Index(fields=["bot", "trigger", "command_name", "enabled", "priority"])] def __str__(self): cmd = self.command_name or "(default)" return f"{self.bot.name} · {self.trigger} · {cmd}" def clean(self): if self.command_name: self.command_name = self.command_name.strip().lstrip("/").lower() # helpers def buttons(self): if not self.buttons_json: return None try: obj = json.loads(self.buttons_json) return obj if isinstance(obj, list) else None except Exception: return None