idk why I ever tried IPC there
All checks were successful
Build and Publish college-bot-v2 / build_and_publish (push) Successful in 13s

This commit is contained in:
Nexus 2024-06-05 01:53:07 +01:00
parent babc888422
commit 65acd62d12
Signed by: nex
GPG key ID: 0FA334385D0B689F
3 changed files with 85 additions and 174 deletions

View file

@ -18,11 +18,14 @@ services:
container_name: jimmy-webhook-server container_name: jimmy-webhook-server
image: git.i-am.nexus/nex/college-bot:latest image: git.i-am.nexus/nex/college-bot:latest
restart: unless-stopped restart: unless-stopped
volumes:
- ./ipc/:/tmp/ipc/
depends_on: depends_on:
- jimmy - jimmy
command: ["/app/venv/bin/uvicorn", "--host", "0.0.0.0", "--port", "1111", "server:app"] - redis
environment:
- REDIS=redis://redis:6379
ports:
- 1111:1111
command: ["/app/venv/bin/python", "/app/server.py"]
ollama: ollama:
image: ollama/ollama:latest image: ollama/ollama:latest
container_name: ollama container_name: ollama

View file

@ -112,107 +112,10 @@ for logger in CONFIG["logging"].get("suppress", []):
class Client(commands.Bot): class Client(commands.Bot):
def __init_(self, *args, **kwargs): def __init_(self, *args, **kwargs):
self.web = None
self.uptime_thread = None self.uptime_thread = None
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
async def _webhook_reader(self):
await self.wait_until_ready()
fifo = CONFIG["jimmy"].get("fifo")
if not fifo:
return
if not CONFIG["redis"]:
return
db = redis.asyncio.Redis(**CONFIG["redis"])
while True:
await asyncio.sleep(1)
while not os.path.exists(fifo):
await asyncio.sleep(1)
try:
async with aiofiles.open(fifo, "w") as fifo_fd:
async for line in fifo_fd:
log.debug("Webhook reader got this line: %r", line)
try:
data = json.loads(line)
token = data.get("token")
assert token, "No token in JSON"
except (json.JSONDecodeError, AssertionError) as e:
log.error("Failed to decode JSON from fifo: %r", line, exc_info=e)
await fifo_fd.write(
json.dumps(
{
"status": 400,
"detail": "what the fuck did you give me"
}
)
)
else:
if data.get("op") == "ping":
await fifo_fd.write(
json.dumps(
{
"token": token,
"status": 200,
}
)
)
elif data.get("id"):
existing = await db.get(
"truth-%s" % data["id"]
)
if existing:
data_notk = data.copy()
data_notk.pop("token")
existing = json.loads(existing)
if existing == data_notk:
await fifo_fd.write(
json.dumps(
{
"token": token,
"status": 204,
}
)
)
else:
await fifo_fd.write(
json.dumps(
{
"token": token,
"status": 409,
"detail": existing
}
)
)
else:
await db.set(
"truth-%s" % data["id"],
json.dumps(data)
)
await fifo_fd.write(
json.dumps(
{
"token": token,
"status": 201,
}
)
)
else:
await fifo_fd.write(
json.dumps(
{
"token": token,
"status": 400,
"detail": "Unknown operation"
}
)
)
except Exception as err:
log.error("Webhook reader failed", exc_info=err)
continue
async def on_connect(self): async def on_connect(self):
if not any(self.walk_application_commands()): if not any(self.walk_application_commands()):
log.warning("No application commands. using pending.") log.warning("No application commands. using pending.")
@ -242,19 +145,12 @@ class Client(commands.Bot):
CONFIG["jimmy"]["uptime_kuma_url"], CONFIG["jimmy"].get("uptime_kuma_interval", 58.0) CONFIG["jimmy"]["uptime_kuma_url"], CONFIG["jimmy"].get("uptime_kuma_interval", 58.0)
) )
self.uptime_thread.start() self.uptime_thread.start()
if CONFIG["jimmy"].get("fifo"):
if getattr(self, "web", None):
self.web.cancel()
self.web = None
self.web = asyncio.create_task(self._webhook_reader())
await super().start(token, reconnect=reconnect) await super().start(token, reconnect=reconnect)
async def close(self) -> None: async def close(self) -> None:
if getattr(self, "uptime_thread", None): if getattr(self, "uptime_thread", None):
self.uptime_thread.kill.set() self.uptime_thread.kill.set()
await asyncio.get_event_loop().run_in_executor(None, self.uptime_thread.join) await asyncio.get_event_loop().run_in_executor(None, self.uptime_thread.join)
if getattr(self, "web", None):
self.web.cancel()
await super().close() await super().close()

View file

@ -1,18 +1,16 @@
#!/bin/env python3
import json import json
import redis
import os import os
import secrets import secrets
import asyncio
import aiofiles
import typing import typing
import time import time
import uuid
from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends, HTTPException from fastapi import FastAPI, Depends, HTTPException
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse, Response
from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.security import HTTPBasic, HTTPBasicCredentials
from pydantic import BaseModel, Field, ValidationError from pydantic import BaseModel, Field, ValidationError
FIFO = "/tmp/ipc/ipc.socket"
JSON: typing.Union[ JSON: typing.Union[
str, int, float, bool, None, typing.Dict[str, "JSON"], typing.List["JSON"] str, int, float, bool, None, typing.Dict[str, "JSON"], typing.List["JSON"]
] = typing.Union[ ] = typing.Union[
@ -28,22 +26,6 @@ class TruthPayload(BaseModel):
extra: typing.Optional[JSON] = None extra: typing.Optional[JSON] = None
class UpstreamPayload(BaseModel):
id: uuid.UUID
status: int = 200
detail: typing.Optional[JSON] = None
@asynccontextmanager
async def lifetime(_app):
if os.path.exists(FIFO):
os.remove(FIFO)
os.mkfifo(FIFO)
try:
yield
finally:
os.remove(FIFO)
security = HTTPBasic(realm="Jimmy") security = HTTPBasic(realm="Jimmy")
USERNAME = os.getenv("WEB_USERNAME", os.urandom(32).hex()) USERNAME = os.getenv("WEB_USERNAME", os.urandom(32).hex())
PASSWORD = os.getenv("WEB_PASSWORD", os.urandom(32).hex()) PASSWORD = os.getenv("WEB_PASSWORD", os.urandom(32).hex())
@ -58,6 +40,15 @@ def check_credentials(credentials: HTTPBasicCredentials = Depends(security)):
return credentials return credentials
def get_db() -> redis.Redis:
uri = os.getenv("REDIS_URL", "redis://redis")
conn = redis.Redis.from_url(uri)
try:
yield conn
finally:
conn.close()
app = FastAPI( app = FastAPI(
title="Jimmy v3 API", title="Jimmy v3 API",
version="3.0.0", version="3.0.0",
@ -66,54 +57,75 @@ app = FastAPI(
) )
async def get_upstream_response(fd, token: str) -> UpstreamPayload: @app.get("/api/truths/all")
t = 0 def get_all_truths(db: redis.Redis = Depends(get_db)):
async for line in fd: """Retrieves all stored truths"""
if t >= 5: keys = db.keys()
break truths = [json.loads(db.get(key)) for key in keys]
t += 1 return truths
try:
upstream_response: UpstreamPayload = UpstreamPayload.model_validate_json(line)
if upstream_response.id.hex == token:
return upstream_response
except ValidationError:
continue
raise RuntimeError("Timeout while waiting for response from upstream")
@app.post("/api/truths", response_model=UpstreamPayload) @app.get("/api/truths/{truth_id}")
async def post_truth(payload: TruthPayload, response: JSONResponse): def get_truth(truth_id: str, db: redis.Redis = Depends(get_db)):
token = uuid.uuid4().hex """Retrieves a stored truth"""
data = db.get(truth_id)
if not data:
raise HTTPException(404, detail="%r not found." % id)
return json.loads(data)
@app.head("/api/truths/{truth_id}")
def head_truth(truth_id: str, db: redis.Redis = Depends(get_db)):
"""Checks that a truth exists"""
data = db.get(truth_id)
if not data:
raise HTTPException(404)
return Response()
@app.post("/api/truths", status_code=201)
def post_truth(payload: TruthPayload, response: JSONResponse, db: redis.Redis = Depends(get_db)):
"""Stores a new truth"""
data = payload.model_dump() data = payload.model_dump()
data["token"] = token existing = db.get(data["id"])
async with aiofiles.open(FIFO, "w") as fifo: if existing:
await fifo.write(payload.json()) parsed = json.loads(existing)
try: if parsed == existing:
upstream_response = await asyncio.wait_for(get_upstream_response(fifo, token), timeout=5) return Response(status_code=204)
except (asyncio.TimeoutError, RuntimeError): raise HTTPException(409, detail="%r already exists." % data["id"])
raise HTTPException( db.set(data["id"], json.dumps(data))
500, response.status_code = 201
detail="Timeout while waiting for response from upstream" return data
)
if upstream_response.id.hex == token:
response.status_code = upstream_response.status @app.put("/api/truths/{truth_id}")
return upstream_response def put_truth(truth_id: str, payload: TruthPayload, db: redis.Redis = Depends(get_db)):
"""Replaces a stored truth"""
data = payload.model_dump()
existing = db.get(truth_id)
if not existing:
raise HTTPException(404, detail="%r not found." % truth_id)
db.set(truth_id, json.dumps(data))
return data
@app.delete("/api/truths/{truth_id}", status_code=204)
def delete_truth(truth_id: str, db: redis.Redis = Depends(get_db)):
"""Deletes a stored truth"""
if not db.delete(truth_id):
raise HTTPException(404, detail="%r not found." % truth_id)
return Response(status_code=204)
@app.get("/api/health") @app.get("/api/health")
async def health(response: JSONResponse): def health(db: redis.Redis = Depends(get_db)):
payload = { try:
"token": uuid.uuid4().hex, db.ping()
"op": "ping" except ConnectionError:
} raise HTTPException(500, detail="Database connection error")
async with aiofiles.open(FIFO, "w") as fifo: return {"status": "ok"}
fifo.write(json.dumps(payload))
try:
upstream_response = await asyncio.wait_for(get_upstream_response(fifo, payload["token"]), timeout=5) if __name__ == "__main__":
except (asyncio.TimeoutError, RuntimeError): import uvicorn
raise HTTPException( uvicorn.run(app, host="0.0.0.0", port=1111, forwarded_allow_ips="*")
500,
detail="Timeout while waiting for response from upstream"
)
response.status_code = upstream_response.status
return upstream_response