This commit is contained in:
parent
1a80d6be24
commit
7e135e92ba
@ -67,3 +67,14 @@ class TelegramMessageAdmin(admin.ModelAdmin):
|
||||
list_filter = ("direction",)
|
||||
search_fields = ("content",)
|
||||
def short(self, obj): return (obj.content or "")[:60]
|
||||
|
||||
|
||||
from django.contrib import admin
|
||||
from .models import BotReplyTemplate
|
||||
|
||||
@admin.register(BotReplyTemplate)
|
||||
class BotReplyTemplateAdmin(admin.ModelAdmin):
|
||||
list_display = ("bot", "trigger", "command_name", "enabled", "priority")
|
||||
list_filter = ("bot", "trigger", "enabled", "parse_mode")
|
||||
search_fields = ("command_name", "text_template", "note")
|
||||
ordering = ("bot", "priority", "id")
|
||||
|
@ -1,7 +1,9 @@
|
||||
# pxy_bots/api/urls.py
|
||||
from django.urls import path
|
||||
from .views import echo_render, health # health if you already have it
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("bots/echo_render", echo_render, name="pxy_bots_echo_render"),
|
||||
path("bots/health/", health, name="pxy_bots_health"), # optional
|
||||
path("bots/health/", views.health, name="pxy_bots_health"), # (if you have it)
|
||||
path("bots/echo_render", views.echo_render, name="pxy_bots_echo_render"), # (you already had)
|
||||
path("bots/template_reply", views.template_reply, name="pxy_bots_template_reply"), # <-- NEW
|
||||
]
|
||||
|
@ -25,3 +25,93 @@ def echo_render(request):
|
||||
],
|
||||
}
|
||||
return JsonResponse(spec)
|
||||
|
||||
# pxy_bots/api/views.py
|
||||
import json, string
|
||||
from django.http import JsonResponse, HttpResponseBadRequest
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from pxy_bots.models import TelegramBot, BotReplyTemplate
|
||||
|
||||
def _ctx_from_req(req):
|
||||
inp = (req.get("input") or {})
|
||||
loc = inp.get("location") or {}
|
||||
args_raw = (inp.get("args_raw") or "") or (inp.get("text") or "") or ""
|
||||
# Strip /cmd if present for ${args}
|
||||
if args_raw.startswith("/"):
|
||||
parts = args_raw.split(" ", 1)
|
||||
args_raw = parts[1] if len(parts) > 1 else ""
|
||||
return {
|
||||
"user_id": ((req.get("user") or {}).get("id")),
|
||||
"chat_id": ((req.get("chat") or {}).get("id")),
|
||||
"cmd": ((req.get("command") or {}).get("name")),
|
||||
"trigger": ((req.get("command") or {}).get("trigger")),
|
||||
"text": (inp.get("text") or ""),
|
||||
"caption": (inp.get("caption") or ""),
|
||||
"args": args_raw,
|
||||
"lat": loc.get("lat"),
|
||||
"lon": loc.get("lon"),
|
||||
}
|
||||
|
||||
def _choose_template(bot, trigger, cmd):
|
||||
qs = (BotReplyTemplate.objects
|
||||
.filter(bot=bot, enabled=True, trigger=trigger)
|
||||
.order_by("priority", "id"))
|
||||
t = (qs.filter(command_name=(cmd or None)).first()
|
||||
or qs.filter(command_name__isnull=True).first()
|
||||
or qs.filter(command_name="").first())
|
||||
return t
|
||||
|
||||
@csrf_exempt
|
||||
def template_reply(request):
|
||||
if request.method != "POST":
|
||||
return HttpResponseBadRequest("POST only")
|
||||
try:
|
||||
req = json.loads(request.body.decode("utf-8") or "{}")
|
||||
except Exception:
|
||||
return HttpResponseBadRequest("invalid json")
|
||||
|
||||
bot_name = ((req.get("bot") or {}).get("username"))
|
||||
if not bot_name:
|
||||
return HttpResponseBadRequest("missing bot.username")
|
||||
|
||||
bot = TelegramBot.objects.filter(name=bot_name, is_active=True).first() \
|
||||
or TelegramBot.objects.filter(username=bot_name, is_active=True).first()
|
||||
if not bot:
|
||||
return HttpResponseBadRequest("bot not found")
|
||||
|
||||
trigger = ((req.get("command") or {}).get("trigger")) or "message"
|
||||
cmd = ((req.get("command") or {}).get("name") or "")
|
||||
cmd = cmd.strip().lstrip("/").lower() or None
|
||||
|
||||
tpl = _choose_template(bot, trigger, cmd)
|
||||
if not tpl:
|
||||
# Soft fallback to help you see wiring issues
|
||||
return JsonResponse({
|
||||
"schema_version": "render.v1",
|
||||
"messages": [{"type": "text",
|
||||
"text": f"(no template) bot={bot_name} trigger={trigger} cmd={cmd or '(default)'}"}]
|
||||
})
|
||||
|
||||
ctx = _ctx_from_req(req)
|
||||
text = string.Template(tpl.text_template or "").safe_substitute(**{k:("" if v is None else v) for k,v in ctx.items()})
|
||||
|
||||
msg_list = []
|
||||
if tpl.media_url:
|
||||
# If media present, put text in caption; otherwise plain text message
|
||||
if text.strip():
|
||||
msg_list.append({"type": "photo", "media_url": tpl.media_url,
|
||||
"caption": text, "parse_mode": tpl.parse_mode.upper() if tpl.parse_mode!="none" else None})
|
||||
else:
|
||||
msg_list.append({"type": "photo", "media_url": tpl.media_url})
|
||||
if (not tpl.media_url) and text.strip():
|
||||
msg = {"type": "text", "text": text}
|
||||
if tpl.parse_mode != BotReplyTemplate.PARSE_NONE:
|
||||
msg["parse_mode"] = tpl.parse_mode.upper()
|
||||
msg_list.append(msg)
|
||||
|
||||
spec = {"schema_version": "render.v1", "messages": msg_list}
|
||||
btns = tpl.buttons()
|
||||
if btns:
|
||||
spec["buttons"] = btns
|
||||
return JsonResponse(spec)
|
||||
|
||||
|
38
pxy_bots/migrations/0008_botreplytemplate.py
Normal file
38
pxy_bots/migrations/0008_botreplytemplate.py
Normal file
@ -0,0 +1,38 @@
|
||||
# Generated by Django 5.0.3 on 2025-09-17 06:20
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pxy_bots', '0007_connection_alter_telegrambot_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BotReplyTemplate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('trigger', models.CharField(choices=[('message', 'Message (no command)'), ('text_command', 'Text command (/cmd)'), ('callback', 'Callback')], default='message', max_length=20)),
|
||||
('command_name', models.CharField(blank=True, help_text="Without leading '/'. Blank = default for this trigger.", max_length=80, null=True)),
|
||||
('text_template', models.TextField(blank=True, default='', help_text='Use ${args}, ${user_id}, ${lat}, ${lon}, etc.')),
|
||||
('parse_mode', models.CharField(choices=[('none', 'None'), ('markdown', 'Markdown'), ('html', 'HTML')], default='markdown', max_length=20)),
|
||||
('media_url', models.CharField(blank=True, default='', help_text='Optional image/video URL', max_length=600)),
|
||||
('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, help_text='Lower runs first', validators=[django.core.validators.MinValueValidator(1)])),
|
||||
('note', models.CharField(blank=True, default='', max_length=240)),
|
||||
('created_at', models.DateTimeField(default=django.utils.timezone.now, editable=False)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('bot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reply_templates', to='pxy_bots.telegrambot')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['priority', 'id'],
|
||||
'indexes': [models.Index(fields=['bot', 'trigger', 'command_name', 'enabled', 'priority'], name='pxy_bots_bo_bot_id_19f2c3_idx')],
|
||||
},
|
||||
),
|
||||
]
|
@ -181,3 +181,67 @@ class CommandRoute(models.Model):
|
||||
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
|
||||
|
@ -95,6 +95,12 @@ EXEMPT_URLS += [ re.compile(r"^api/bots/health/?$") ]
|
||||
|
||||
EXEMPT_URLS += [ re.compile(r"^api/bots/echo_render$") ]
|
||||
|
||||
# pxy_dashboard/middleware.py (append to EXEMPT_URLS)
|
||||
EXEMPT_URLS += [
|
||||
re.compile(r"^api/bots/template_reply$"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
class LoginRequiredMiddleware(MiddlewareMixin):
|
||||
def process_request(self, request):
|
||||
|
Loading…
x
Reference in New Issue
Block a user