feat: Add presence heartbeat for Matrix online status

- matrix-gateway: POST /internal/matrix/presence/online endpoint
- usePresenceHeartbeat hook with activity tracking
- Auto away after 5 min inactivity
- Offline on page close/visibility change
- Integrated in MatrixChatRoom component
This commit is contained in:
Apple
2025-11-27 00:19:40 -08:00
parent 5bed515852
commit 3de3c8cb36
6371 changed files with 1317450 additions and 932 deletions

View File

@@ -0,0 +1,17 @@
FROM python:3.11-slim
WORKDIR /app
# Копіювати requirements
COPY requirements.txt .
# Встановити Python залежності
RUN pip install --no-cache-dir -r requirements.txt
# Копіювати код
COPY app/ ./app/
EXPOSE 8897
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8897"]

View File

@@ -0,0 +1,204 @@
"""
Web Search Service - Пошук в інтернеті для DAARION
Інтеграція з DuckDuckGo, Google, Bing
"""
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import logging
import os
from typing import Optional, List
from datetime import datetime
# Logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(
title="Web Search Service",
description="Web Search для DAARION (DuckDuckGo, Google, Bing)",
version="1.0.0"
)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Lazy import search engines
try:
from duckduckgo_search import DDGS
DDGS_AVAILABLE = True
except ImportError:
DDGS_AVAILABLE = False
logger.warning("⚠️ DuckDuckGo search not available")
try:
from googlesearch import search as google_search
GOOGLE_AVAILABLE = True
except ImportError:
GOOGLE_AVAILABLE = False
logger.warning("⚠️ Google search not available")
# Конфігурація
SEARCH_ENGINE = os.getenv("SEARCH_ENGINE", "duckduckgo") # duckduckgo, google, bing
MAX_RESULTS = int(os.getenv("MAX_RESULTS", "10"))
class SearchRequest(BaseModel):
query: str
engine: Optional[str] = "duckduckgo"
max_results: Optional[int] = 10
region: Optional[str] = "ua-uk" # ua-uk, us-en, etc.
class SearchResult(BaseModel):
title: str
url: str
snippet: str
position: int
class SearchResponse(BaseModel):
query: str
results: List[SearchResult]
engine: str
total: int
timestamp: str
@app.get("/")
async def root():
"""Health check"""
return {
"service": "Web Search Service",
"status": "running",
"engines": {
"duckduckgo": DDGS_AVAILABLE,
"google": GOOGLE_AVAILABLE
},
"default_engine": SEARCH_ENGINE,
"max_results": MAX_RESULTS,
"version": "1.0.0"
}
@app.get("/health")
async def health():
"""Health check endpoint"""
return {
"status": "healthy" if (DDGS_AVAILABLE or GOOGLE_AVAILABLE) else "degraded",
"duckduckgo": "available" if DDGS_AVAILABLE else "unavailable",
"google": "available" if GOOGLE_AVAILABLE else "unavailable"
}
def search_duckduckgo(query: str, max_results: int = 10, region: str = "ua-uk") -> List[dict]:
"""
Пошук через DuckDuckGo
"""
if not DDGS_AVAILABLE:
raise HTTPException(status_code=503, detail="DuckDuckGo not available")
try:
ddgs = DDGS()
results = ddgs.text(query, region=region, max_results=max_results)
formatted_results = []
for idx, result in enumerate(results):
formatted_results.append({
'title': result.get('title', ''),
'url': result.get('href', ''),
'snippet': result.get('body', ''),
'position': idx + 1
})
return formatted_results
except Exception as e:
logger.error(f"❌ DuckDuckGo search error: {e}")
raise HTTPException(status_code=500, detail=f"DuckDuckGo error: {str(e)}")
def search_google(query: str, max_results: int = 10) -> List[dict]:
"""
Пошук через Google
"""
if not GOOGLE_AVAILABLE:
raise HTTPException(status_code=503, detail="Google search not available")
try:
results = []
for idx, url in enumerate(google_search(query, num_results=max_results)):
results.append({
'title': url, # Google search library не повертає title
'url': url,
'snippet': '',
'position': idx + 1
})
return results
except Exception as e:
logger.error(f"❌ Google search error: {e}")
raise HTTPException(status_code=500, detail=f"Google error: {str(e)}")
@app.post("/api/search", response_model=SearchResponse)
async def web_search(request: SearchRequest):
"""
Виконує пошук в інтернеті
Body:
{
"query": "DAARION MicroDAO",
"engine": "duckduckgo",
"max_results": 10,
"region": "ua-uk"
}
"""
try:
logger.info(f"🔍 Search request: '{request.query}' via {request.engine}")
engine = request.engine or SEARCH_ENGINE
max_results = request.max_results or MAX_RESULTS
results = []
if engine == 'duckduckgo':
results = search_duckduckgo(request.query, max_results, request.region)
elif engine == 'google':
results = search_google(request.query, max_results)
else:
raise HTTPException(status_code=400, detail=f"Unknown engine: {engine}")
logger.info(f"✅ Found {len(results)} results")
return SearchResponse(
query=request.query,
results=[SearchResult(**r) for r in results],
engine=engine,
total=len(results),
timestamp=datetime.utcnow().isoformat()
)
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Search error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/search")
async def web_search_get(query: str, engine: str = "duckduckgo", max_results: int = 10):
"""
Виконує пошук в інтернеті (GET метод)
Query params:
- query: search query
- engine: duckduckgo | google
- max_results: number of results (default: 10)
"""
request = SearchRequest(query=query, engine=engine, max_results=max_results)
return await web_search(request)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8897)

View File

@@ -0,0 +1,20 @@
version: '3.8'
services:
web-search-service:
build: .
container_name: dagi-web-search-service
ports:
- "8897:8897"
environment:
- SEARCH_ENGINE=duckduckgo
- MAX_RESULTS=10
volumes:
- ./app:/app/app
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8897/health"]
interval: 30s
timeout: 10s
retries: 3

View File

@@ -0,0 +1,6 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
duckduckgo-search==4.1.1
googlesearch-python==1.2.3
requests==2.31.0