feat(voice/events): full CRUD — POST/PUT/DELETE with owner routing
All checks were successful
Deploy / deploy (push) Successful in 2m57s
All checks were successful
Deploy / deploy (push) Successful in 2m57s
Голосовой ассистент теперь может создавать, изменять и удалять события в календарях Даниила и Светы. - POST /api/voice/tools/events — create (title, date, start_time, end_time, all_day, owner). Маппит owner (daniil/sveta) в calendar_id и проксирует в /api/calendar POST. - PUT — update (event_id, owner, fields). Передаёт только изменённые поля + нужный calendarId. - DELETE ?event_id=X&owner=Y — удаление. - GET — теперь возвращает id события и owner (daniil/sveta), чтобы скрипт мог их передать в update/delete. - range=month поддержан с year/month query params. Все три метода под bearer auth (VOICE_API_KEY), как остальные voice tools. Loopback к /api/calendar идёт через internalHeaders() x-voice-internal.
This commit is contained in:
@@ -4,26 +4,169 @@ export const runtime = 'nodejs'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { isBearerAuthorized, unauthorized, internalHeaders } from '@/lib/voice-tools'
|
||||
|
||||
const baseUrl = () => `http://localhost:${process.env.PORT || '3000'}`
|
||||
|
||||
// Resolve agent-style owner to the Google calendar id used by /api/calendar.
|
||||
// daniil → primary env DANIIL_CALENDAR_ID or default address
|
||||
// sveta → SVETA_CALENDAR_ID
|
||||
function calendarIdFor(owner: string): string {
|
||||
const o = (owner || '').toLowerCase()
|
||||
if (o === 'sveta' || o === 'света' || o === 'lusya') {
|
||||
return process.env.SVETA_CALENDAR_ID || ''
|
||||
}
|
||||
return process.env.DANIIL_CALENDAR_ID || 'daniilklimov25@gmail.com'
|
||||
}
|
||||
|
||||
function ownerTag(owner: string): 'daniil' | 'sveta' {
|
||||
const o = (owner || '').toLowerCase()
|
||||
if (o === 'sveta' || o === 'света' || o === 'lusya') return 'sveta'
|
||||
return 'daniil'
|
||||
}
|
||||
|
||||
// ─────────── GET ─────────── list events
|
||||
export async function GET(req: Request) {
|
||||
if (!isBearerAuthorized(req)) return unauthorized()
|
||||
|
||||
const { searchParams } = new URL(req.url)
|
||||
const range = searchParams.get('range') || 'today' // today | week
|
||||
|
||||
const baseUrl = `http://localhost:${process.env.PORT || '3000'}`
|
||||
const r = await fetch(`${baseUrl}/api/calendar?range=${encodeURIComponent(range)}`, {
|
||||
cache: 'no-store',
|
||||
headers: internalHeaders(),
|
||||
}).catch(() => null)
|
||||
const range = searchParams.get('range') || 'today' // today | week | month
|
||||
let url = `${baseUrl()}/api/calendar?range=${encodeURIComponent(range)}`
|
||||
// For month: forward year/month if supplied
|
||||
if (range === 'month') {
|
||||
const year = searchParams.get('year')
|
||||
const month = searchParams.get('month')
|
||||
if (year) url += `&year=${encodeURIComponent(year)}`
|
||||
if (month) url += `&month=${encodeURIComponent(month)}`
|
||||
}
|
||||
|
||||
const r = await fetch(url, { cache: 'no-store', headers: internalHeaders() }).catch(() => null)
|
||||
if (!r || !r.ok) return NextResponse.json({ events: [], error: 'unreachable' }, { status: 502 })
|
||||
|
||||
const j = await r.json()
|
||||
const events = (j.events || []).map((e: any) => ({
|
||||
id: e.id,
|
||||
title: e.title,
|
||||
start: e.start,
|
||||
end: e.end,
|
||||
all_day: e.allDay,
|
||||
owner: e.ownerName || e.owner,
|
||||
owner: e.owner, // 'daniil' | 'sveta'
|
||||
owner_name: e.ownerName,
|
||||
}))
|
||||
return NextResponse.json({ events })
|
||||
}
|
||||
|
||||
// ─────────── POST ─────────── create event
|
||||
export async function POST(req: Request) {
|
||||
if (!isBearerAuthorized(req)) return unauthorized()
|
||||
|
||||
const body = await req.json().catch(() => null)
|
||||
if (!body) return NextResponse.json({ error: 'body required' }, { status: 400 })
|
||||
|
||||
const title = typeof body.title === 'string' ? body.title.trim() : ''
|
||||
const date = typeof body.date === 'string' ? body.date.trim() : ''
|
||||
const owner = ownerTag(body.owner || 'daniil')
|
||||
const all_day = Boolean(body.all_day)
|
||||
const start_time = typeof body.start_time === 'string' ? body.start_time.trim() : ''
|
||||
const end_time = typeof body.end_time === 'string' ? body.end_time.trim() : ''
|
||||
|
||||
if (!title || !date) {
|
||||
return NextResponse.json({ error: 'title and date required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const payload: Record<string, any> = {
|
||||
title,
|
||||
date,
|
||||
allDay: all_day,
|
||||
owner,
|
||||
}
|
||||
if (!all_day) {
|
||||
payload.startTime = start_time || '12:00'
|
||||
// Default end = start + 1 hour
|
||||
if (end_time) {
|
||||
payload.endTime = end_time
|
||||
} else {
|
||||
const [h, m] = (start_time || '12:00').split(':').map((n: string) => parseInt(n, 10))
|
||||
const endH = (h + 1) % 24
|
||||
payload.endTime = `${endH.toString().padStart(2, '0')}:${(m || 0).toString().padStart(2, '0')}`
|
||||
}
|
||||
}
|
||||
|
||||
const r = await fetch(`${baseUrl()}/api/calendar`, {
|
||||
method: 'POST',
|
||||
headers: { ...internalHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
cache: 'no-store',
|
||||
}).catch(() => null)
|
||||
|
||||
if (!r || !r.ok) {
|
||||
const detail = r ? await r.text().catch(() => '') : ''
|
||||
return NextResponse.json({ error: 'create_failed', detail: detail.slice(0, 300) }, { status: 502 })
|
||||
}
|
||||
const j = await r.json()
|
||||
if (j.error) return NextResponse.json({ error: j.error }, { status: 502 })
|
||||
return NextResponse.json({ event: j.event })
|
||||
}
|
||||
|
||||
// ─────────── PUT ─────────── update event
|
||||
export async function PUT(req: Request) {
|
||||
if (!isBearerAuthorized(req)) return unauthorized()
|
||||
|
||||
const body = await req.json().catch(() => null)
|
||||
if (!body) return NextResponse.json({ error: 'body required' }, { status: 400 })
|
||||
|
||||
const event_id = typeof body.event_id === 'string' ? body.event_id : ''
|
||||
if (!event_id) return NextResponse.json({ error: 'event_id required' }, { status: 400 })
|
||||
|
||||
const owner = ownerTag(body.owner || 'daniil')
|
||||
const calendarId = calendarIdFor(owner)
|
||||
|
||||
const payload: Record<string, any> = {
|
||||
eventId: event_id,
|
||||
calendarId,
|
||||
allDay: Boolean(body.all_day),
|
||||
}
|
||||
if (typeof body.title === 'string') payload.title = body.title.trim()
|
||||
if (typeof body.date === 'string') payload.date = body.date.trim()
|
||||
if (typeof body.start_time === 'string') payload.startTime = body.start_time.trim()
|
||||
if (typeof body.end_time === 'string') payload.endTime = body.end_time.trim()
|
||||
|
||||
const r = await fetch(`${baseUrl()}/api/calendar`, {
|
||||
method: 'PUT',
|
||||
headers: { ...internalHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
cache: 'no-store',
|
||||
}).catch(() => null)
|
||||
|
||||
if (!r || !r.ok) {
|
||||
const detail = r ? await r.text().catch(() => '') : ''
|
||||
return NextResponse.json({ error: 'update_failed', detail: detail.slice(0, 300) }, { status: 502 })
|
||||
}
|
||||
const j = await r.json()
|
||||
if (j.error) return NextResponse.json({ error: j.error }, { status: 502 })
|
||||
return NextResponse.json({ event: j.event })
|
||||
}
|
||||
|
||||
// ─────────── DELETE ─────────── delete event
|
||||
export async function DELETE(req: Request) {
|
||||
if (!isBearerAuthorized(req)) return unauthorized()
|
||||
|
||||
const { searchParams } = new URL(req.url)
|
||||
const event_id = searchParams.get('event_id') || ''
|
||||
const owner = searchParams.get('owner') || 'daniil'
|
||||
|
||||
if (!event_id) return NextResponse.json({ error: 'event_id required' }, { status: 400 })
|
||||
|
||||
const calendarId = calendarIdFor(owner)
|
||||
const url = `${baseUrl()}/api/calendar?eventId=${encodeURIComponent(event_id)}&calendarId=${encodeURIComponent(calendarId)}`
|
||||
|
||||
const r = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: internalHeaders(),
|
||||
cache: 'no-store',
|
||||
}).catch(() => null)
|
||||
|
||||
if (!r || !r.ok) {
|
||||
const detail = r ? await r.text().catch(() => '') : ''
|
||||
return NextResponse.json({ error: 'delete_failed', detail: detail.slice(0, 300) }, { status: 502 })
|
||||
}
|
||||
return NextResponse.json({ deleted: true })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user