steward functionality to bost reality
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
4d788c1acc
commit
4c56529c96
@ -65,6 +65,7 @@ INSTALLED_APPS = [
|
||||
'pxy_routing',
|
||||
'pxy_sites',
|
||||
"pxy_simulations",
|
||||
"pxy_stewards",
|
||||
|
||||
"rest_framework",
|
||||
"pxy_api",
|
||||
|
||||
@ -47,6 +47,7 @@ urlpatterns = [
|
||||
|
||||
path("api/", include("pxy_bots.api.urls")),
|
||||
path("api/langchain/", include("pxy_langchain.api.urls")),
|
||||
path("api/", include("pxy_stewards.api.urls")),
|
||||
|
||||
path("", include("pxy_openai.urls")),
|
||||
|
||||
|
||||
1
pxy_stewards/__init__.py
Normal file
1
pxy_stewards/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Stewardship app for NFC + bot-driven cell powers."""
|
||||
24
pxy_stewards/admin.py
Normal file
24
pxy_stewards/admin.py
Normal file
@ -0,0 +1,24 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Stewardship, StewardBlessing, StewardSignal
|
||||
|
||||
|
||||
@admin.register(Stewardship)
|
||||
class StewardshipAdmin(admin.ModelAdmin):
|
||||
list_display = ("platform", "user_id", "status", "geohash", "target_bot", "expires_at", "updated_at")
|
||||
list_filter = ("platform", "status", "target_bot")
|
||||
search_fields = ("user_id", "geohash", "target_bot")
|
||||
|
||||
|
||||
@admin.register(StewardBlessing)
|
||||
class StewardBlessingAdmin(admin.ModelAdmin):
|
||||
list_display = ("geohash", "target_bot", "multiplier", "starts_at", "ends_at")
|
||||
list_filter = ("target_bot",)
|
||||
search_fields = ("geohash",)
|
||||
|
||||
|
||||
@admin.register(StewardSignal)
|
||||
class StewardSignalAdmin(admin.ModelAdmin):
|
||||
list_display = ("geohash", "target_bot", "kind", "created_at")
|
||||
list_filter = ("target_bot", "kind")
|
||||
search_fields = ("geohash", "note")
|
||||
1
pxy_stewards/api/__init__.py
Normal file
1
pxy_stewards/api/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""API for steward workflows."""
|
||||
9
pxy_stewards/api/urls.py
Normal file
9
pxy_stewards/api/urls.py
Normal file
@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import steward_handle, blessings_active, steward_health
|
||||
|
||||
urlpatterns = [
|
||||
path("stewards/handle", steward_handle, name="stewards_handle"),
|
||||
path("stewards/blessings", blessings_active, name="stewards_blessings"),
|
||||
path("stewards/health", steward_health, name="stewards_health"),
|
||||
]
|
||||
328
pxy_stewards/api/views.py
Normal file
328
pxy_stewards/api/views.py
Normal file
@ -0,0 +1,328 @@
|
||||
import json
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from django.http import JsonResponse, HttpRequest
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_GET, require_http_methods
|
||||
|
||||
from pxy_stewards.models import Stewardship, StewardBlessing, StewardSignal
|
||||
from pxy_stewards.utils import encode_geohash
|
||||
|
||||
|
||||
STEWARD_TTL_HOURS = int(os.getenv("STEWARD_TTL_HOURS", "168"))
|
||||
STEWARD_SESSION_TTL_MIN = int(os.getenv("STEWARD_SESSION_TTL_MIN", "30"))
|
||||
STEWARD_GEOHASH_PRECISION = int(os.getenv("STEWARD_GEOHASH_PRECISION", "7"))
|
||||
STEWARD_ENERGY_MAX = int(os.getenv("STEWARD_ENERGY_MAX", "100"))
|
||||
STEWARD_BLESS_COST = int(os.getenv("STEWARD_BLESS_COST", "25"))
|
||||
STEWARD_BLESS_MINUTES = int(os.getenv("STEWARD_BLESS_MINUTES", "60"))
|
||||
STEWARD_BLESS_MULTIPLIER = float(os.getenv("STEWARD_BLESS_MULTIPLIER", "2.0"))
|
||||
|
||||
TARGET_BOTS = {
|
||||
"coins": {"label": "Citizens (Pepe Basurita)", "bot": "PepeBasuritaCoinsBot"},
|
||||
"motito": {"label": "Motito (On-demand)", "bot": "PepeMotitoBot"},
|
||||
"camion": {"label": "Camioncito (Route)", "bot": "PepeCamioncitoBot"},
|
||||
}
|
||||
|
||||
|
||||
def _render_text(text: str, buttons: Optional[list] = None) -> JsonResponse:
|
||||
spec = {
|
||||
"schema_version": "render.v1",
|
||||
"messages": [{"type": "text", "text": text}],
|
||||
}
|
||||
if buttons:
|
||||
spec["buttons"] = buttons
|
||||
return JsonResponse(spec)
|
||||
|
||||
|
||||
def _load_body(request: HttpRequest) -> Dict[str, Any]:
|
||||
try:
|
||||
raw = (request.body or b"").decode("utf-8")
|
||||
return json.loads(raw or "{}")
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _extract_args(body: Dict[str, Any]) -> str:
|
||||
args_raw = (body.get("input") or {}).get("args_raw") or ""
|
||||
if args_raw.startswith("/"):
|
||||
parts = args_raw.split(" ", 1)
|
||||
return parts[1].strip() if len(parts) > 1 else ""
|
||||
return args_raw.strip()
|
||||
|
||||
|
||||
def _parse_latlon(raw: str) -> Optional[tuple[float, float]]:
|
||||
parts = [p.strip() for p in (raw or "").split(",")]
|
||||
if len(parts) != 2:
|
||||
return None
|
||||
try:
|
||||
lat = float(parts[0])
|
||||
lon = float(parts[1])
|
||||
except Exception:
|
||||
return None
|
||||
return lat, lon
|
||||
|
||||
|
||||
def _platform(body: Dict[str, Any]) -> str:
|
||||
ctx = body.get("context") or {}
|
||||
return (ctx.get("platform") or "telegram").strip().lower()
|
||||
|
||||
|
||||
def _user_id(body: Dict[str, Any]) -> Optional[str]:
|
||||
user = body.get("user") or {}
|
||||
if user.get("id") is None:
|
||||
return None
|
||||
return str(user.get("id"))
|
||||
|
||||
|
||||
def _bot_name(body: Dict[str, Any]) -> str:
|
||||
bot = body.get("bot") or {}
|
||||
return (bot.get("username") or "unknown").strip()
|
||||
|
||||
|
||||
def _get_steward(platform: str, user_id: str) -> Stewardship:
|
||||
steward, _ = Stewardship.objects.get_or_create(
|
||||
platform=platform,
|
||||
user_id=user_id,
|
||||
defaults={"bot_name": "unknown", "energy": 0},
|
||||
)
|
||||
return steward
|
||||
|
||||
|
||||
def _expire_if_needed(steward: Stewardship) -> None:
|
||||
if steward.status == Stewardship.STATUS_ACTIVE and steward.expires_at:
|
||||
if steward.expires_at <= timezone.now():
|
||||
steward.status = Stewardship.STATUS_EXPIRED
|
||||
steward.energy = 0
|
||||
steward.save(update_fields=["status", "energy", "updated_at"])
|
||||
|
||||
|
||||
def _cell_taken(geohash: str, exclude_user_id: Optional[str] = None) -> bool:
|
||||
now = timezone.now()
|
||||
qs = Stewardship.objects.filter(
|
||||
geohash=geohash,
|
||||
status=Stewardship.STATUS_ACTIVE,
|
||||
expires_at__gt=now,
|
||||
)
|
||||
if exclude_user_id:
|
||||
qs = qs.exclude(user_id=exclude_user_id)
|
||||
return qs.exists()
|
||||
|
||||
|
||||
def _activation_buttons() -> list:
|
||||
buttons = []
|
||||
for key, meta in TARGET_BOTS.items():
|
||||
buttons.append({
|
||||
"label": meta["label"],
|
||||
"kind": "callback_api",
|
||||
"action": "choose",
|
||||
"params": {"b": key},
|
||||
})
|
||||
return buttons
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["POST"])
|
||||
def steward_handle(request: HttpRequest):
|
||||
body = _load_body(request)
|
||||
platform = _platform(body)
|
||||
user_id = _user_id(body)
|
||||
bot_name = _bot_name(body)
|
||||
if not user_id:
|
||||
return _render_text("Missing user context. Try again from the bot.")
|
||||
|
||||
cmd = ((body.get("command") or {}).get("name") or "").strip().lstrip("/").lower()
|
||||
trigger = ((body.get("command") or {}).get("trigger") or "").strip().lower()
|
||||
args = _extract_args(body)
|
||||
location = (body.get("input") or {}).get("location") or {}
|
||||
|
||||
steward = _get_steward(platform, user_id)
|
||||
_expire_if_needed(steward)
|
||||
if steward.bot_name != bot_name:
|
||||
steward.bot_name = bot_name
|
||||
steward.save(update_fields=["bot_name", "updated_at"])
|
||||
|
||||
if trigger == "callback":
|
||||
data_raw = ((body.get("callback") or {}).get("data") or "")
|
||||
try:
|
||||
data = json.loads(data_raw)
|
||||
except Exception:
|
||||
data = {}
|
||||
action = (data.get("a") or data.get("action") or "").lower()
|
||||
params = data.get("p") or data.get("params") or {}
|
||||
if action == "choose":
|
||||
key = (params.get("b") or params.get("bot") or "").lower()
|
||||
meta = TARGET_BOTS.get(key)
|
||||
if not meta:
|
||||
return _render_text("Unknown target bot. Try /steward again.")
|
||||
if steward.status != Stewardship.STATUS_AWAITING_TARGET:
|
||||
return _render_text("No pending activation. Use /steward to start.")
|
||||
if steward.geohash and _cell_taken(steward.geohash, exclude_user_id=steward.user_id):
|
||||
return _render_text("That cell already has a Steward. Choose a new location.")
|
||||
steward.target_bot = meta["bot"]
|
||||
steward.status = Stewardship.STATUS_ACTIVE
|
||||
steward.expires_at = timezone.now() + timedelta(hours=STEWARD_TTL_HOURS)
|
||||
steward.energy = STEWARD_ENERGY_MAX
|
||||
steward.pending_until = None
|
||||
steward.save()
|
||||
text = (
|
||||
f"Steward activated for cell {steward.geohash}.\n"
|
||||
f"Target bot: {meta['label']}.\n"
|
||||
f"Power window: {STEWARD_TTL_HOURS}h.\n"
|
||||
"Commands: /status /bless /signal"
|
||||
)
|
||||
return _render_text(text)
|
||||
return _render_text("Unknown action. Use /steward to start.")
|
||||
|
||||
if cmd in {"steward", "start"}:
|
||||
if steward.status == Stewardship.STATUS_ACTIVE and steward.expires_at and steward.expires_at > timezone.now():
|
||||
return _render_text(
|
||||
f"You are already Steward of {steward.geohash}.\n"
|
||||
"Use /status, /bless, or /signal."
|
||||
)
|
||||
|
||||
latlon = _parse_latlon(args)
|
||||
if latlon:
|
||||
location = {"lat": latlon[0], "lon": latlon[1]}
|
||||
|
||||
steward.status = Stewardship.STATUS_AWAITING_LOCATION
|
||||
steward.pending_until = timezone.now() + timedelta(minutes=STEWARD_SESSION_TTL_MIN)
|
||||
steward.save()
|
||||
|
||||
if location.get("lat") is not None and location.get("lon") is not None:
|
||||
cmd = "__location__"
|
||||
else:
|
||||
return _render_text(
|
||||
"Steward activation started.\n"
|
||||
"Send your location to anchor your cell.\n"
|
||||
"Your identity stays private."
|
||||
)
|
||||
|
||||
if cmd == "__location__":
|
||||
if steward.status != Stewardship.STATUS_AWAITING_LOCATION:
|
||||
return _render_text("Send /steward to start steward activation.")
|
||||
if location.get("lat") is None or location.get("lon") is None:
|
||||
return _render_text("Location missing. Send your location to continue.")
|
||||
lat = float(location.get("lat"))
|
||||
lon = float(location.get("lon"))
|
||||
geohash = encode_geohash(lat, lon, precision=STEWARD_GEOHASH_PRECISION)
|
||||
if _cell_taken(geohash, exclude_user_id=steward.user_id):
|
||||
return _render_text(
|
||||
"That cell already has a Steward.\n"
|
||||
"Send another location to try a different cell."
|
||||
)
|
||||
steward.lat = lat
|
||||
steward.lon = lon
|
||||
steward.geohash = geohash
|
||||
steward.status = Stewardship.STATUS_AWAITING_TARGET
|
||||
steward.pending_until = timezone.now() + timedelta(minutes=STEWARD_SESSION_TTL_MIN)
|
||||
steward.save()
|
||||
text = (
|
||||
f"Cell captured: {geohash}.\n"
|
||||
"Choose which bot to empower for this cell:"
|
||||
)
|
||||
return _render_text(text, buttons=_activation_buttons())
|
||||
|
||||
if cmd == "status":
|
||||
if steward.status != Stewardship.STATUS_ACTIVE or not steward.geohash:
|
||||
return _render_text("No active stewardship. Use /steward to begin.")
|
||||
active_bless = StewardBlessing.objects.filter(
|
||||
geohash=steward.geohash,
|
||||
ends_at__gt=timezone.now(),
|
||||
).count()
|
||||
remaining = int((steward.expires_at - timezone.now()).total_seconds() / 3600) if steward.expires_at else 0
|
||||
text = (
|
||||
f"Steward status\n"
|
||||
f"Cell: {steward.geohash}\n"
|
||||
f"Target bot: {steward.target_bot}\n"
|
||||
f"Energy: {steward.energy}/{STEWARD_ENERGY_MAX}\n"
|
||||
f"Blessings active: {active_bless}\n"
|
||||
f"Power window: {remaining}h"
|
||||
)
|
||||
return _render_text(text)
|
||||
|
||||
if cmd == "bless":
|
||||
if steward.status != Stewardship.STATUS_ACTIVE or not steward.geohash:
|
||||
return _render_text("No active stewardship. Use /steward to begin.")
|
||||
if steward.energy < STEWARD_BLESS_COST:
|
||||
return _render_text("Not enough energy to bless. Try later.")
|
||||
minutes = STEWARD_BLESS_MINUTES
|
||||
if args:
|
||||
try:
|
||||
minutes = max(10, min(240, int(args.split()[0])))
|
||||
except Exception:
|
||||
minutes = STEWARD_BLESS_MINUTES
|
||||
starts = timezone.now()
|
||||
ends = starts + timedelta(minutes=minutes)
|
||||
StewardBlessing.objects.create(
|
||||
steward=steward,
|
||||
geohash=steward.geohash,
|
||||
target_bot=steward.target_bot or "unknown",
|
||||
multiplier=STEWARD_BLESS_MULTIPLIER,
|
||||
starts_at=starts,
|
||||
ends_at=ends,
|
||||
)
|
||||
steward.energy = max(0, steward.energy - STEWARD_BLESS_COST)
|
||||
steward.save(update_fields=["energy", "updated_at"])
|
||||
text = (
|
||||
f"Blessing active for {minutes} min.\n"
|
||||
f"Cell: {steward.geohash}\n"
|
||||
f"Boost: x{STEWARD_BLESS_MULTIPLIER:.1f} Green Points"
|
||||
)
|
||||
return _render_text(text)
|
||||
|
||||
if cmd == "signal":
|
||||
if steward.status != Stewardship.STATUS_ACTIVE or not steward.geohash:
|
||||
return _render_text("No active stewardship. Use /steward to begin.")
|
||||
note = args or "cell_needs_attention"
|
||||
StewardSignal.objects.create(
|
||||
steward=steward,
|
||||
geohash=steward.geohash,
|
||||
target_bot=steward.target_bot or "unknown",
|
||||
kind="signal",
|
||||
note=note,
|
||||
)
|
||||
return _render_text(
|
||||
f"Signal recorded for {steward.geohash}.\n"
|
||||
"The city agents will adjust in your cell."
|
||||
)
|
||||
|
||||
return _render_text(
|
||||
"Steward commands:\n"
|
||||
"/steward - activate stewardship\n"
|
||||
"/status - view status\n"
|
||||
"/bless [minutes] - boost Green Points\n"
|
||||
"/signal <note> - log a cell signal"
|
||||
)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_GET
|
||||
def blessings_active(request: HttpRequest):
|
||||
geohash = (request.GET.get("geohash") or "").strip()
|
||||
target_bot = (request.GET.get("bot") or "").strip()
|
||||
if not geohash:
|
||||
return JsonResponse({"ok": False, "code": "missing_geohash"}, status=400)
|
||||
now = timezone.now()
|
||||
qs = StewardBlessing.objects.filter(geohash=geohash, ends_at__gt=now)
|
||||
if target_bot:
|
||||
qs = qs.filter(target_bot=target_bot)
|
||||
data = [
|
||||
{
|
||||
"geohash": b.geohash,
|
||||
"target_bot": b.target_bot,
|
||||
"multiplier": b.multiplier,
|
||||
"starts_at": b.starts_at.isoformat(),
|
||||
"ends_at": b.ends_at.isoformat(),
|
||||
}
|
||||
for b in qs
|
||||
]
|
||||
return JsonResponse({"ok": True, "blessings": data})
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_GET
|
||||
def steward_health(request: HttpRequest):
|
||||
return JsonResponse({"ok": True, "service": "pxy_stewards"})
|
||||
6
pxy_stewards/apps.py
Normal file
6
pxy_stewards/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PxyStewardsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "pxy_stewards"
|
||||
75
pxy_stewards/migrations/0001_initial.py
Normal file
75
pxy_stewards/migrations/0001_initial.py
Normal file
@ -0,0 +1,75 @@
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Stewardship",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("platform", models.CharField(default="telegram", max_length=20)),
|
||||
("user_id", models.CharField(max_length=64)),
|
||||
("bot_name", models.CharField(max_length=80)),
|
||||
("status", models.CharField(choices=[("awaiting_location", "Awaiting location"), ("awaiting_target", "Awaiting target bot"), ("active", "Active"), ("expired", "Expired")], default="awaiting_location", max_length=32)),
|
||||
("geohash", models.CharField(blank=True, max_length=12, null=True)),
|
||||
("lat", models.FloatField(blank=True, null=True)),
|
||||
("lon", models.FloatField(blank=True, null=True)),
|
||||
("target_bot", models.CharField(blank=True, max_length=80, null=True)),
|
||||
("energy", models.PositiveIntegerField(default=0)),
|
||||
("expires_at", models.DateTimeField(blank=True, null=True)),
|
||||
("pending_until", models.DateTimeField(blank=True, null=True)),
|
||||
("created_at", models.DateTimeField(default=django.utils.timezone.now, editable=False)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
"indexes": [
|
||||
models.Index(fields=["platform", "user_id"], name="pxy_stewar_platform_2a65e9_idx"),
|
||||
models.Index(fields=["geohash", "status", "expires_at"], name="pxy_stewar_geohash_626644_idx"),
|
||||
],
|
||||
"unique_together": {("platform", "user_id")},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="StewardBlessing",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("geohash", models.CharField(max_length=12)),
|
||||
("target_bot", models.CharField(max_length=80)),
|
||||
("multiplier", models.FloatField(default=2.0)),
|
||||
("starts_at", models.DateTimeField(default=django.utils.timezone.now)),
|
||||
("ends_at", models.DateTimeField()),
|
||||
("created_at", models.DateTimeField(default=django.utils.timezone.now, editable=False)),
|
||||
("steward", models.ForeignKey(on_delete=models.deletion.CASCADE, related_name="blessings", to="pxy_stewards.stewardship")),
|
||||
],
|
||||
options={
|
||||
"indexes": [
|
||||
models.Index(fields=["geohash", "ends_at"], name="pxy_stewar_geohash_245c81_idx"),
|
||||
models.Index(fields=["target_bot", "ends_at"], name="pxy_stewar_target__1c2e1f_idx"),
|
||||
],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="StewardSignal",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("geohash", models.CharField(max_length=12)),
|
||||
("target_bot", models.CharField(max_length=80)),
|
||||
("kind", models.CharField(default="signal", max_length=40)),
|
||||
("note", models.TextField(blank=True, default="")),
|
||||
("created_at", models.DateTimeField(default=django.utils.timezone.now, editable=False)),
|
||||
("steward", models.ForeignKey(on_delete=models.deletion.CASCADE, related_name="signals", to="pxy_stewards.stewardship")),
|
||||
],
|
||||
options={
|
||||
"indexes": [
|
||||
models.Index(fields=["geohash", "created_at"], name="pxy_stewar_geohash_62497e_idx"),
|
||||
models.Index(fields=["target_bot", "created_at"], name="pxy_stewar_target__73ec24_idx"),
|
||||
],
|
||||
},
|
||||
),
|
||||
]
|
||||
1
pxy_stewards/migrations/__init__.py
Normal file
1
pxy_stewards/migrations/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Migrations for pxy_stewards."""
|
||||
81
pxy_stewards/models.py
Normal file
81
pxy_stewards/models.py
Normal file
@ -0,0 +1,81 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class Stewardship(models.Model):
|
||||
STATUS_AWAITING_LOCATION = "awaiting_location"
|
||||
STATUS_AWAITING_TARGET = "awaiting_target"
|
||||
STATUS_ACTIVE = "active"
|
||||
STATUS_EXPIRED = "expired"
|
||||
|
||||
STATUS_CHOICES = [
|
||||
(STATUS_AWAITING_LOCATION, "Awaiting location"),
|
||||
(STATUS_AWAITING_TARGET, "Awaiting target bot"),
|
||||
(STATUS_ACTIVE, "Active"),
|
||||
(STATUS_EXPIRED, "Expired"),
|
||||
]
|
||||
|
||||
platform = models.CharField(max_length=20, default="telegram")
|
||||
user_id = models.CharField(max_length=64)
|
||||
bot_name = models.CharField(max_length=80)
|
||||
|
||||
status = models.CharField(max_length=32, choices=STATUS_CHOICES, default=STATUS_AWAITING_LOCATION)
|
||||
geohash = models.CharField(max_length=12, blank=True, null=True)
|
||||
lat = models.FloatField(null=True, blank=True)
|
||||
lon = models.FloatField(null=True, blank=True)
|
||||
target_bot = models.CharField(max_length=80, blank=True, null=True)
|
||||
|
||||
energy = models.PositiveIntegerField(default=0)
|
||||
expires_at = models.DateTimeField(null=True, blank=True)
|
||||
pending_until = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
created_at = models.DateTimeField(default=timezone.now, editable=False)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ("platform", "user_id")
|
||||
indexes = [
|
||||
models.Index(fields=["platform", "user_id"]),
|
||||
models.Index(fields=["geohash", "status", "expires_at"]),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
cell = self.geohash or "unset"
|
||||
return f"{self.platform}:{self.user_id} @ {cell} ({self.status})"
|
||||
|
||||
|
||||
class StewardBlessing(models.Model):
|
||||
steward = models.ForeignKey(Stewardship, on_delete=models.CASCADE, related_name="blessings")
|
||||
geohash = models.CharField(max_length=12)
|
||||
target_bot = models.CharField(max_length=80)
|
||||
multiplier = models.FloatField(default=2.0)
|
||||
starts_at = models.DateTimeField(default=timezone.now)
|
||||
ends_at = models.DateTimeField()
|
||||
created_at = models.DateTimeField(default=timezone.now, editable=False)
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=["geohash", "ends_at"]),
|
||||
models.Index(fields=["target_bot", "ends_at"]),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Blessing {self.geohash} -> {self.target_bot}"
|
||||
|
||||
|
||||
class StewardSignal(models.Model):
|
||||
steward = models.ForeignKey(Stewardship, on_delete=models.CASCADE, related_name="signals")
|
||||
geohash = models.CharField(max_length=12)
|
||||
target_bot = models.CharField(max_length=80)
|
||||
kind = models.CharField(max_length=40, default="signal")
|
||||
note = models.TextField(blank=True, default="")
|
||||
created_at = models.DateTimeField(default=timezone.now, editable=False)
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=["geohash", "created_at"]),
|
||||
models.Index(fields=["target_bot", "created_at"]),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Signal {self.geohash} -> {self.target_bot} ({self.kind})"
|
||||
35
pxy_stewards/utils.py
Normal file
35
pxy_stewards/utils.py
Normal file
@ -0,0 +1,35 @@
|
||||
_BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz"
|
||||
_BITS = [16, 8, 4, 2, 1]
|
||||
|
||||
|
||||
def encode_geohash(lat: float, lon: float, precision: int = 7) -> str:
|
||||
lat_interval = [-90.0, 90.0]
|
||||
lon_interval = [-180.0, 180.0]
|
||||
geohash = []
|
||||
bit = 0
|
||||
ch = 0
|
||||
even = True
|
||||
|
||||
while len(geohash) < precision:
|
||||
if even:
|
||||
mid = (lon_interval[0] + lon_interval[1]) / 2
|
||||
if lon >= mid:
|
||||
ch |= _BITS[bit]
|
||||
lon_interval[0] = mid
|
||||
else:
|
||||
lon_interval[1] = mid
|
||||
else:
|
||||
mid = (lat_interval[0] + lat_interval[1]) / 2
|
||||
if lat >= mid:
|
||||
ch |= _BITS[bit]
|
||||
lat_interval[0] = mid
|
||||
else:
|
||||
lat_interval[1] = mid
|
||||
even = not even
|
||||
if bit < 4:
|
||||
bit += 1
|
||||
else:
|
||||
geohash.append(_BASE32[ch])
|
||||
bit = 0
|
||||
ch = 0
|
||||
return "".join(geohash)
|
||||
Loading…
x
Reference in New Issue
Block a user