export type ConversationChannel='say'|'whisper'; export type ConversationRequest={ id:string; createdAt:string; speakerName:string; message:string; channel:ConversationChannel; zoneName:string; receiver:string; selectedPlayer:string; nearbyBots:string[]; status:'pending'|'processing'|'awaiting_delivery'|'completed'|'failed'; attempts:number; }; async function ensure(db:D1Database){ await db.prepare(`CREATE TABLE IF NOT EXISTS conversation_requests ( id TEXT PRIMARY KEY NOT NULL, created_at TEXT NOT NULL, speaker_name TEXT NOT NULL, message TEXT NOT NULL, channel TEXT NOT NULL, zone_name TEXT NOT NULL, receiver TEXT NOT NULL, selected_player TEXT NOT NULL, nearby_bots TEXT NOT NULL, status TEXT NOT NULL, attempts INTEGER NOT NULL DEFAULT 0, started_at TEXT, completed_at TEXT, result TEXT, error TEXT )`).run(); await db.prepare(`CREATE INDEX IF NOT EXISTS idx_conversation_requests_status_created ON conversation_requests(status,created_at)`).run(); } function text(payload:Record,key:string,max:number){ const value=payload[key]; return typeof value==='string'?value.trim().slice(0,max):''; } export async function queueConversationEvent(db:D1Database,kind:string,payload:Record){ if(payload.isBot===true)return null; if(kind!=='player.chat'&&kind!=='player.whisper')return null; const speakerName=text(payload,'name',40); const message=text(payload,'message',500); if(!speakerName||!message)return null; if(message.startsWith('.')||message.startsWith('!'))return null; if(kind==='player.whisper'&&payload.receiverIsBot!==true)return null; if(kind==='player.chat'&&!text(payload,'selectedPlayer',40)&&!text(payload,'nearbyBots',1000))return null; await ensure(db); const id=crypto.randomUUID(); const createdAt=new Date().toISOString(); const channel:ConversationChannel=kind==='player.whisper'?'whisper':'say'; await db.prepare(`INSERT INTO conversation_requests (id,created_at,speaker_name,message,channel,zone_name,receiver,selected_player,nearby_bots,status,attempts) VALUES (?,?,?,?,?,?,?,?,?,'pending',0)`) .bind( id,createdAt,speakerName,message,channel, text(payload,'zoneName',120), text(payload,'receiver',40), text(payload,'selectedPlayer',40), text(payload,'nearbyBots',1000) ).run(); return {id,createdAt}; } export async function leaseConversation(db:D1Database):Promise{ await ensure(db); const staleBefore=new Date(Date.now()-90_000).toISOString(); await db.prepare(`UPDATE conversation_requests SET status='pending',started_at=NULL WHERE status='processing' AND started_at IS NOT NULL AND started_at(); if(!row)return null; const startedAt=new Date().toISOString(); const updated=await db.prepare(`UPDATE conversation_requests SET status='processing',started_at=?,attempts=attempts+1 WHERE id=? AND status='pending'`).bind(startedAt,row.id).run(); if(updated.meta.changes!==1)return null; return { id:row.id, createdAt:row.created_at, speakerName:row.speaker_name, message:row.message, channel:row.channel==='whisper'?'whisper':'say', zoneName:row.zone_name, receiver:row.receiver, selectedPlayer:row.selected_player, nearbyBots:String(row.nearby_bots||'').split(',').map((x:string)=>x.trim()).filter(Boolean), status:'processing', attempts:Number(row.attempts||0)+1 }; } export async function completeConversation(db:D1Database,id:string,result:string){ await ensure(db); await db.prepare(`UPDATE conversation_requests SET status='completed',result=?,error=NULL,completed_at=? WHERE id=?`).bind(result.slice(0,2000),new Date().toISOString(),id).run(); } export async function failConversation(db:D1Database,id:string,error:string){ await ensure(db); await db.prepare(`UPDATE conversation_requests SET status='failed',error=?,completed_at=? WHERE id=?`).bind(error.slice(0,2000),new Date().toISOString(),id).run(); } export async function cleanupConversations(db:D1Database){ await ensure(db); const before=new Date(Date.now()-24*60*60_000).toISOString(); await db.prepare(`DELETE FROM conversation_requests WHERE status IN ('completed','failed') AND completed_at IS NOT NULL AND completed_at(); const deliveries=[]; for(const row of rows.results){ const delivery=JSON.parse(row.result) as {request:ConversationRequest;botId:string;reply:string;commandId:string}; const command=await db.prepare('SELECT status,error FROM ale_commands WHERE id=?').bind(delivery.commandId).first<{status:string;error:string|null}>(); deliveries.push({...delivery,status:command?.status??'pending',error:command?.error}); } return deliveries; }