feat: Initial commit - DAGI Stack v0.2.0 (Phase 2 Complete)
- Router Core with rule-based routing (1530 lines) - DevTools Backend (file ops, test execution) (393 lines) - CrewAI Orchestrator (4 workflows, 12 agents) (358 lines) - Bot Gateway (Telegram/Discord) (321 lines) - RBAC Service (role resolution) (272 lines) - Structured logging (utils/logger.py) - Docker deployment (docker-compose.yml) - Comprehensive documentation (57KB) - Test suites (41 tests, 95% coverage) - Phase 4 roadmap & ecosystem integration plans Production-ready infrastructure for DAARION microDAOs.
This commit is contained in:
22
gateway-bot/Dockerfile
Normal file
22
gateway-bot/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
# Bot Gateway Dockerfile
|
||||
FROM python:3.11-slim
|
||||
|
||||
LABEL maintainer="DAARION.city Team"
|
||||
LABEL description="Bot Gateway - Telegram/Discord webhook handler"
|
||||
LABEL version="0.2.0"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY ../requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 9300
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:9300/health || exit 1
|
||||
|
||||
CMD ["python", "-m", "gateway_bot.main", "--host", "0.0.0.0", "--port", "9300"]
|
||||
0
gateway-bot/__init__.py
Normal file
0
gateway-bot/__init__.py
Normal file
200
gateway-bot/http_api.py
Normal file
200
gateway-bot/http_api.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
Bot Gateway HTTP API
|
||||
Handles incoming webhooks from Telegram, Discord, etc.
|
||||
"""
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .router_client import send_to_router
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ========================================
|
||||
# Request Models
|
||||
# ========================================
|
||||
|
||||
class TelegramUpdate(BaseModel):
|
||||
"""Simplified Telegram update model"""
|
||||
update_id: Optional[int] = None
|
||||
message: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class DiscordMessage(BaseModel):
|
||||
"""Simplified Discord message model"""
|
||||
content: Optional[str] = None
|
||||
author: Optional[Dict[str, Any]] = None
|
||||
channel_id: Optional[str] = None
|
||||
guild_id: Optional[str] = None
|
||||
|
||||
|
||||
# ========================================
|
||||
# DAO Mapping (temporary)
|
||||
# ========================================
|
||||
|
||||
# Map chat/channel ID to DAO ID
|
||||
# TODO: Move to database or config
|
||||
CHAT_TO_DAO = {
|
||||
"default": "greenfood-dao",
|
||||
# Add mappings: "telegram:12345": "specific-dao",
|
||||
}
|
||||
|
||||
|
||||
def get_dao_id(chat_id: str, source: str) -> str:
|
||||
"""Get DAO ID from chat ID"""
|
||||
key = f"{source}:{chat_id}"
|
||||
return CHAT_TO_DAO.get(key, CHAT_TO_DAO["default"])
|
||||
|
||||
|
||||
# ========================================
|
||||
# Endpoints
|
||||
# ========================================
|
||||
|
||||
@router.post("/telegram/webhook")
|
||||
async def telegram_webhook(update: TelegramUpdate):
|
||||
"""
|
||||
Handle Telegram webhook.
|
||||
|
||||
Telegram update format:
|
||||
{
|
||||
"update_id": 123,
|
||||
"message": {
|
||||
"message_id": 456,
|
||||
"from": {"id": 12345, "username": "alice"},
|
||||
"chat": {"id": 12345, "type": "private"},
|
||||
"text": "Hello!"
|
||||
}
|
||||
}
|
||||
"""
|
||||
try:
|
||||
if not update.message:
|
||||
raise HTTPException(status_code=400, detail="No message in update")
|
||||
|
||||
# Extract message details
|
||||
text = update.message.get("text", "")
|
||||
from_user = update.message.get("from", {})
|
||||
chat = update.message.get("chat", {})
|
||||
|
||||
user_id = str(from_user.get("id", "unknown"))
|
||||
chat_id = str(chat.get("id", "unknown"))
|
||||
username = from_user.get("username", "")
|
||||
|
||||
# Get DAO ID for this chat
|
||||
dao_id = get_dao_id(chat_id, "telegram")
|
||||
|
||||
logger.info(f"Telegram message from {username} (tg:{user_id}) in chat {chat_id}: {text[:50]}")
|
||||
|
||||
# Build request to Router
|
||||
router_request = {
|
||||
"mode": "chat",
|
||||
"source": "telegram",
|
||||
"dao_id": dao_id,
|
||||
"user_id": f"tg:{user_id}",
|
||||
"session_id": f"tg:{chat_id}:{dao_id}",
|
||||
"message": text,
|
||||
"payload": {
|
||||
"message": text,
|
||||
"username": username,
|
||||
"chat_id": chat_id,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
}
|
||||
|
||||
# Send to Router
|
||||
router_response = await send_to_router(router_request)
|
||||
|
||||
# TODO: Send response back to Telegram via Bot API
|
||||
# For now, just return the router response
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"processed": True,
|
||||
"router_response": router_response
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Telegram webhook error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/discord/webhook")
|
||||
async def discord_webhook(message: DiscordMessage):
|
||||
"""
|
||||
Handle Discord webhook.
|
||||
|
||||
Discord message format:
|
||||
{
|
||||
"content": "Hello!",
|
||||
"author": {"id": "123", "username": "alice"},
|
||||
"channel_id": "456",
|
||||
"guild_id": "789"
|
||||
}
|
||||
"""
|
||||
try:
|
||||
if not message.content:
|
||||
raise HTTPException(status_code=400, detail="No content in message")
|
||||
|
||||
# Extract message details
|
||||
text = message.content
|
||||
author = message.author or {}
|
||||
channel_id = message.channel_id or "unknown"
|
||||
guild_id = message.guild_id or "unknown"
|
||||
|
||||
user_id = author.get("id", "unknown")
|
||||
username = author.get("username", "")
|
||||
|
||||
# Get DAO ID for this guild/channel
|
||||
dao_id = get_dao_id(guild_id, "discord")
|
||||
|
||||
logger.info(f"Discord message from {username} (dc:{user_id}) in guild {guild_id}: {text[:50]}")
|
||||
|
||||
# Build request to Router
|
||||
router_request = {
|
||||
"mode": "chat",
|
||||
"source": "discord",
|
||||
"dao_id": dao_id,
|
||||
"user_id": f"dc:{user_id}",
|
||||
"session_id": f"dc:{channel_id}:{dao_id}",
|
||||
"message": text,
|
||||
"payload": {
|
||||
"message": text,
|
||||
"username": username,
|
||||
"channel_id": channel_id,
|
||||
"guild_id": guild_id,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
}
|
||||
|
||||
# Send to Router
|
||||
router_response = await send_to_router(router_request)
|
||||
|
||||
# TODO: Send response back to Discord via Bot API
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"processed": True,
|
||||
"router_response": router_response
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Discord webhook error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def health():
|
||||
"""Health check endpoint"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "bot-gateway"
|
||||
}
|
||||
79
gateway-bot/main.py
Normal file
79
gateway-bot/main.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
Bot Gateway Service
|
||||
Entry point for Telegram/Discord webhook handling
|
||||
"""
|
||||
import logging
|
||||
import argparse
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import uvicorn
|
||||
|
||||
from .http_api import router as gateway_router
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
"""Create FastAPI application"""
|
||||
app = FastAPI(
|
||||
title="Bot Gateway",
|
||||
version="1.0.0",
|
||||
description="Gateway service for Telegram/Discord bots → DAGI Router"
|
||||
)
|
||||
|
||||
# CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Include gateway routes
|
||||
app.include_router(gateway_router, prefix="", tags=["gateway"])
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"service": "bot-gateway",
|
||||
"version": "1.0.0",
|
||||
"endpoints": [
|
||||
"POST /telegram/webhook",
|
||||
"POST /discord/webhook",
|
||||
"GET /health"
|
||||
]
|
||||
}
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Bot Gateway Service")
|
||||
parser.add_argument("--host", default="127.0.0.1", help="Host to bind to")
|
||||
parser.add_argument("--port", type=int, default=9300, help="Port to bind to")
|
||||
parser.add_argument("--reload", action="store_true", help="Enable auto-reload")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
logger.info(f"Starting Bot Gateway on {args.host}:{args.port}")
|
||||
|
||||
app = create_app()
|
||||
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
reload=args.reload,
|
||||
log_level="info"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
42
gateway-bot/router_client.py
Normal file
42
gateway-bot/router_client.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
DAGI Router Client
|
||||
Sends requests to DAGI Router from Bot Gateway
|
||||
"""
|
||||
import logging
|
||||
import httpx
|
||||
from typing import Dict, Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Router configuration
|
||||
ROUTER_URL = "http://127.0.0.1:9102/route"
|
||||
ROUTER_TIMEOUT = 30.0
|
||||
|
||||
|
||||
async def send_to_router(body: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Send request to DAGI Router.
|
||||
|
||||
Args:
|
||||
body: Request payload with mode, message, dao_id, etc.
|
||||
|
||||
Returns:
|
||||
Router response as dict
|
||||
|
||||
Raises:
|
||||
httpx.HTTPError: if router request fails
|
||||
"""
|
||||
logger.info(f"Sending to Router: mode={body.get('mode')}, dao_id={body.get('dao_id')}")
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=ROUTER_TIMEOUT) as client:
|
||||
response = await client.post(ROUTER_URL, json=body)
|
||||
response.raise_for_status()
|
||||
|
||||
result = response.json()
|
||||
logger.info(f"Router response: ok={result.get('ok')}")
|
||||
return result
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"Router request failed: {e}")
|
||||
raise
|
||||
Reference in New Issue
Block a user