Files
smart-home-tablet/app/api/voice/tools/events/route.ts
Cosmo 56844a539d
All checks were successful
Deploy / deploy (push) Successful in 2m57s
feat(voice/events): full CRUD — POST/PUT/DELETE with owner routing
Голосовой ассистент теперь может создавать, изменять и удалять события
в календарях Даниила и Светы.

- 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.
2026-04-23 14:34:32 +00:00

173 lines
6.4 KiB
TypeScript

export const dynamic = 'force-dynamic'
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 | 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.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 })
}