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 { NextResponse } from 'next/server'
|
||||||
import { isBearerAuthorized, unauthorized, internalHeaders } from '@/lib/voice-tools'
|
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) {
|
export async function GET(req: Request) {
|
||||||
if (!isBearerAuthorized(req)) return unauthorized()
|
if (!isBearerAuthorized(req)) return unauthorized()
|
||||||
|
|
||||||
const { searchParams } = new URL(req.url)
|
const { searchParams } = new URL(req.url)
|
||||||
const range = searchParams.get('range') || 'today' // today | week
|
const range = searchParams.get('range') || 'today' // today | week | month
|
||||||
|
let url = `${baseUrl()}/api/calendar?range=${encodeURIComponent(range)}`
|
||||||
const baseUrl = `http://localhost:${process.env.PORT || '3000'}`
|
// For month: forward year/month if supplied
|
||||||
const r = await fetch(`${baseUrl}/api/calendar?range=${encodeURIComponent(range)}`, {
|
if (range === 'month') {
|
||||||
cache: 'no-store',
|
const year = searchParams.get('year')
|
||||||
headers: internalHeaders(),
|
const month = searchParams.get('month')
|
||||||
}).catch(() => null)
|
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 })
|
if (!r || !r.ok) return NextResponse.json({ events: [], error: 'unreachable' }, { status: 502 })
|
||||||
|
|
||||||
const j = await r.json()
|
const j = await r.json()
|
||||||
const events = (j.events || []).map((e: any) => ({
|
const events = (j.events || []).map((e: any) => ({
|
||||||
|
id: e.id,
|
||||||
title: e.title,
|
title: e.title,
|
||||||
start: e.start,
|
start: e.start,
|
||||||
end: e.end,
|
end: e.end,
|
||||||
all_day: e.allDay,
|
all_day: e.allDay,
|
||||||
owner: e.ownerName || e.owner,
|
owner: e.owner, // 'daniil' | 'sveta'
|
||||||
|
owner_name: e.ownerName,
|
||||||
}))
|
}))
|
||||||
return NextResponse.json({ events })
|
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