@rizvanua/contact-chat 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +496 -0
- package/dist/index.cjs +704 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +688 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +326 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +118 -0
- package/dist/react/index.d.ts +118 -0
- package/dist/react/index.js +306 -0
- package/dist/react/index.js.map +1 -0
- package/dist/server/index.cjs +704 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +98 -0
- package/dist/server/index.d.ts +98 -0
- package/dist/server/index.js +688 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/nextjs.cjs +10 -0
- package/dist/server/nextjs.cjs.map +1 -0
- package/dist/server/nextjs.d.cts +17 -0
- package/dist/server/nextjs.d.ts +17 -0
- package/dist/server/nextjs.js +8 -0
- package/dist/server/nextjs.js.map +1 -0
- package/dist/stores/index.cjs +244 -0
- package/dist/stores/index.cjs.map +1 -0
- package/dist/stores/index.d.cts +58 -0
- package/dist/stores/index.d.ts +58 -0
- package/dist/stores/index.js +241 -0
- package/dist/stores/index.js.map +1 -0
- package/dist/transports/index.cjs +156 -0
- package/dist/transports/index.cjs.map +1 -0
- package/dist/transports/index.d.cts +48 -0
- package/dist/transports/index.d.ts +48 -0
- package/dist/transports/index.js +151 -0
- package/dist/transports/index.js.map +1 -0
- package/dist/types-BcoqxSLg.d.cts +42 -0
- package/dist/types-BcoqxSLg.d.ts +42 -0
- package/dist/ui/index.cjs +609 -0
- package/dist/ui/index.cjs.map +1 -0
- package/dist/ui/index.d.cts +127 -0
- package/dist/ui/index.d.ts +127 -0
- package/dist/ui/index.js +604 -0
- package/dist/ui/index.js.map +1 -0
- package/package.json +146 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/types.ts","../src/core/config.ts","../src/core/ratelimit.ts","../src/core/name.ts","../src/core/handlers.ts","../src/core/index.ts","../src/stores/upstash-redis.ts","../src/stores/memory.ts","../src/transports/telegram.ts"],"names":["ttlSeconds"],"mappings":"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","file":"index.js","sourcesContent":["export type ChatAuthor = 'visitor' | 'owner';\n\nexport interface ChatMessage {\n id: string;\n from: ChatAuthor;\n text: string;\n ts: number;\n}\n\nexport interface ChatSession {\n topicId: number;\n createdAt: number;\n msgCount: number;\n // Optional because sessions created before the name gate shipped have none.\n name?: string;\n}\n\nexport interface SendRequestBody {\n sessionId: string;\n text: string;\n name?: string;\n honeypot?: string;\n}\n\nexport interface SendResponseBody {\n message: ChatMessage;\n cursor: number;\n}\n\nexport interface PollResponseBody {\n messages: ChatMessage[];\n cursor: number;\n}\n\nexport type ChatErrorCode =\n | 'validation'\n | 'rate_limited'\n | 'store_unavailable'\n | 'telegram_unavailable';\n\nexport interface ChatErrorBody {\n code: ChatErrorCode;\n error: string;\n}\n\n// @deprecated Use DEFAULT_CHAT_LIMITS.maxMessageLength instead.\nexport const MAX_MESSAGE_LENGTH = 1000;\n// @deprecated Use DEFAULT_CHAT_LIMITS.maxMessagesPerSession instead.\nexport const MAX_MESSAGES_PER_SESSION = 40;\n// @deprecated Use DEFAULT_CHAT_LIMITS.sessionTtlSeconds instead.\nexport const SESSION_TTL_SECONDS = 604800; // 7 days\n// @deprecated Use DEFAULT_CHAT_LIMITS.ratePerMinute instead.\nexport const RATE_PER_MINUTE = 5;\n// @deprecated Use DEFAULT_CHAT_LIMITS.ratePerDay instead.\nexport const RATE_PER_DAY = 30;\n\nexport interface ChatLimits {\n maxMessageLength: number;\n maxMessagesPerSession: number;\n sessionTtlSeconds: number;\n ratePerMinute: number;\n ratePerDay: number;\n}\n\nexport const DEFAULT_CHAT_LIMITS: ChatLimits = {\n maxMessageLength: MAX_MESSAGE_LENGTH,\n maxMessagesPerSession: MAX_MESSAGES_PER_SESSION,\n sessionTtlSeconds: SESSION_TTL_SECONDS,\n ratePerMinute: RATE_PER_MINUTE,\n ratePerDay: RATE_PER_DAY,\n};\n","import type { ChatStore } from './store-interface.js';\nimport type { ChatTransport } from './transport-interface.js';\nimport { DEFAULT_CHAT_LIMITS } from './types.js';\nimport type { ChatLimits } from './types.js';\n\n/**\n * Visitor-facing HTTP error strings this package emits in ChatErrorBody.error fields.\n * Override individual strings via config.copy to localise or customise them.\n */\nexport interface ChatServerCopy {\n validation: {\n /** Returned when the request body cannot be parsed as JSON. */\n malformedBody: string;\n /** Returned when sessionId is missing or too short. */\n missingSession: string;\n /** Returned when the honeypot field is filled (bot detected). */\n rejected: string;\n /** Returned when the text field is missing or not a string. */\n messageRequired: string;\n /** Returned when the trimmed text is empty. */\n messageEmpty: string;\n /** Returned when the trimmed text exceeds the configured limit. Takes the resolved limit. */\n messageTooLong: (limit: number) => string;\n /** Returned when a new session is requested but no name was provided. */\n nameRequired: string;\n };\n /** Returned when the rate limit is exceeded (HTTP 429). */\n rateLimited: string;\n /** Returned when the session has reached its message limit. */\n messageLimitReached: string;\n /** Returned when the store (Redis) is unavailable (HTTP 503). */\n storeUnavailable: string;\n /** Returned when Telegram delivery fails (HTTP 502). */\n telegramUnavailable: string;\n}\n\nexport const DEFAULT_CHAT_SERVER_COPY: ChatServerCopy = {\n validation: {\n malformedBody: 'Malformed request body.',\n missingSession: 'Missing session.',\n rejected: 'Rejected.',\n messageRequired: 'Message is required.',\n messageEmpty: 'Message is empty.',\n messageTooLong: (limit: number) => `Please keep it under ${limit} characters.`,\n nameRequired: 'Please tell me your name first.',\n },\n rateLimited: 'That is a lot of messages — please wait a moment.',\n messageLimitReached: 'This conversation has reached its message limit.',\n storeUnavailable: 'Chat is unavailable right now.',\n telegramUnavailable: 'Could not deliver that message.',\n};\n\nexport type ChatServerCopyPartial = Partial<Omit<ChatServerCopy, 'validation'>> & {\n validation?: Partial<ChatServerCopy['validation']>;\n};\n\nexport interface ChatServerConfig {\n store: ChatStore;\n transport: ChatTransport;\n limits?: Partial<ChatLimits>;\n ipHashSalt: string;\n copy?: ChatServerCopyPartial;\n onError?: (scope: string, error: unknown) => void;\n}\n\nexport interface ResolvedChatServerConfig {\n store: ChatStore;\n transport: ChatTransport;\n limits: ChatLimits;\n ipHashSalt: string;\n copy: ChatServerCopy;\n onError: (scope: string, error: unknown) => void;\n}\n\nexport function resolveConfig(config: ChatServerConfig): ResolvedChatServerConfig {\n return {\n store: config.store,\n transport: config.transport,\n ipHashSalt: config.ipHashSalt,\n limits: { ...DEFAULT_CHAT_LIMITS, ...config.limits },\n copy: {\n ...DEFAULT_CHAT_SERVER_COPY,\n ...config.copy,\n validation: {\n ...DEFAULT_CHAT_SERVER_COPY.validation,\n ...(config.copy?.validation ?? {}),\n },\n },\n onError: config.onError ?? (() => {}),\n };\n}\n","import { createHash } from 'node:crypto';\nimport type { ChatStore } from './store-interface.js';\nimport type { ChatLimits } from './types.js';\n\nconst MINUTE_SECONDS = 60;\nconst DAY_SECONDS = 86400;\n\nexport function getClientIp(headers: Headers): string {\n const forwarded = headers.get('x-forwarded-for');\n if (forwarded) {\n const first = forwarded.split(',')[0]?.trim();\n if (first) return first;\n }\n return headers.get('x-real-ip')?.trim() || 'unknown';\n}\n\n/** The raw IP is never stored — only this salted digest. */\nexport function hashIp(ip: string, salt: string): string {\n return createHash('sha256').update(`${ip}:${salt}`).digest('hex').slice(0, 32);\n}\n\nexport interface RateLimitDeps {\n store: ChatStore;\n ipHashSalt: string;\n limits: Pick<ChatLimits, 'ratePerMinute' | 'ratePerDay'>;\n}\n\nexport interface RateLimitResult {\n allowed: boolean;\n retryAfterSeconds: number;\n}\n\n/**\n * INCR returns the post-increment value, so a fresh key returns 1 and EXPIRE is\n * set only on that first hit. That keeps the window fixed rather than sliding\n * forward on every request, which would make the limit unreachable.\n * The store honours this fixed-window semantic inside incrementCounter.\n */\nexport async function checkRateLimit(\n ip: string,\n deps: RateLimitDeps,\n): Promise<RateLimitResult> {\n const id = hashIp(ip, deps.ipHashSalt);\n\n // Store honours fixed-window semantics: TTL is set only on the first hit (count === 1).\n const perMinute = await deps.store.incrementCounter(`chat:rate:${id}:min`, MINUTE_SECONDS);\n if (perMinute > deps.limits.ratePerMinute) {\n return { allowed: false, retryAfterSeconds: MINUTE_SECONDS };\n }\n\n const perDay = await deps.store.incrementCounter(`chat:rate:${id}:day`, DAY_SECONDS);\n if (perDay > deps.limits.ratePerDay) {\n return { allowed: false, retryAfterSeconds: DAY_SECONDS };\n }\n\n return { allowed: true, retryAfterSeconds: 0 };\n}\n","export const MAX_NAME_LENGTH = 60;\n\n/**\n * Normalises a visitor-supplied name for display in the owner's Telegram.\n * Returns '' when nothing usable remains — emptiness is the only failure.\n *\n * Not an injection defence: Telegram does not parse topic names as markup.\n * This exists so control characters and runaway length cannot reach the\n * owner's topic list.\n */\nexport function sanitizeName(raw: unknown): string {\n if (typeof raw !== 'string') return '';\n return raw\n // Strip control characters, including newlines and tabs.\n .replace(/[\\u0000-\\u001F\\u007F]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, MAX_NAME_LENGTH);\n}\n","import { checkRateLimit, getClientIp } from './ratelimit.js';\nimport { sanitizeName } from './name.js';\nimport type { ResolvedChatServerConfig } from './config.js';\nimport type { ChatMessage, SendResponseBody, PollResponseBody, ChatErrorBody } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Private helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Wraps a transport call so its failure can be identified separately from\n * store failures. The outer catch in createSendHandler unwraps this to decide\n * between 502 (transport) and 503 (store/unexpected).\n */\nclass TransportFailure extends Error {\n constructor(readonly cause: unknown) {\n super('transport failure');\n }\n}\n\nasync function callTransport<T>(op: () => Promise<T>): Promise<T> {\n try {\n return await op();\n } catch (error) {\n throw new TransportFailure(error);\n }\n}\n\nfunction fail(\n status: number,\n body: ChatErrorBody,\n headers?: Record<string, string>,\n): Response {\n return Response.json(body, { status, headers });\n}\n\n// ---------------------------------------------------------------------------\n// createSendHandler\n// ---------------------------------------------------------------------------\n\nexport function createSendHandler(\n resolved: ResolvedChatServerConfig,\n): (request: Request) => Promise<Response> {\n const { store, transport, limits, copy, ipHashSalt, onError } = resolved;\n\n return async function sendHandler(request: Request): Promise<Response> {\n let payload: unknown;\n try {\n payload = await request.json();\n } catch {\n return fail(400, { code: 'validation', error: copy.validation.malformedBody });\n }\n\n const { sessionId, text, name, honeypot } = (payload ?? {}) as Record<\n string,\n unknown\n >;\n\n // Validation runs before Redis so bad input costs no round trips.\n if (typeof sessionId !== 'string' || sessionId.length < 8) {\n return fail(400, { code: 'validation', error: copy.validation.missingSession });\n }\n if (typeof honeypot === 'string' && honeypot.length > 0) {\n return fail(400, { code: 'validation', error: copy.validation.rejected });\n }\n if (typeof text !== 'string') {\n return fail(400, { code: 'validation', error: copy.validation.messageRequired });\n }\n\n const trimmed = text.trim();\n if (trimmed.length === 0) {\n return fail(400, { code: 'validation', error: copy.validation.messageEmpty });\n }\n if (trimmed.length > limits.maxMessageLength) {\n return fail(400, {\n code: 'validation',\n error: copy.validation.messageTooLong(limits.maxMessageLength),\n });\n }\n\n try {\n const limit = await checkRateLimit(getClientIp(request.headers), {\n store,\n ipHashSalt,\n limits,\n });\n if (!limit.allowed) {\n return fail(\n 429,\n {\n code: 'rate_limited',\n error: copy.rateLimited,\n },\n { 'Retry-After': String(limit.retryAfterSeconds) },\n );\n }\n\n let session = await store.getSession(sessionId);\n\n if (session && session.msgCount >= limits.maxMessagesPerSession) {\n return fail(400, {\n code: 'validation',\n error: copy.messageLimitReached,\n });\n }\n\n const userAgent = request.headers.get('user-agent');\n\n if (!session) {\n // The name is required only here. A returning visitor whose stored name\n // was cleared must not be rejected: their topic is already titled.\n const visitorName = sanitizeName(name);\n if (!visitorName) {\n return fail(400, {\n code: 'validation',\n error: copy.validation.nameRequired,\n });\n }\n const topicId = await callTransport(() =>\n transport.createThread({ name: visitorName, userAgent, now: new Date() }),\n );\n session = await store.createSession(sessionId, topicId, visitorName);\n } else if (!session.name) {\n // Sessions created before the name gate shipped carry no name. Adopt the\n // one now supplied and retitle the topic, rather than discarding it —\n // asking for a name and then ignoring it is worse than never asking.\n const visitorName = sanitizeName(name);\n if (visitorName) {\n const updated = await store.setSessionName(sessionId, visitorName);\n if (updated) session = updated;\n try {\n await callTransport(() =>\n transport.renameThread(session!.topicId, {\n name: visitorName,\n userAgent,\n // First contact, matching how a fresh topic is stamped.\n now: new Date(session!.createdAt),\n }),\n );\n } catch (error) {\n // Cosmetic: the name is stored either way.\n onError(\n 'chat.send.topicRename',\n error instanceof TransportFailure ? error.cause : error,\n );\n }\n }\n }\n\n try {\n await callTransport(() => transport.send(session!.topicId, trimmed));\n } catch (error) {\n // Unwrap the TransportFailure to get the inner error for isMissingThreadError.\n const inner = error instanceof TransportFailure ? error.cause : error;\n if (!transport.isMissingThreadError(inner)) throw error;\n // The owner deleted the topic, which would otherwise brick this session\n // forever. Create a replacement, remap, and retry once.\n onError(\n 'chat.send.topicRecreate',\n `topic ${session!.topicId} is gone, recreating for session ${sessionId}`,\n );\n const topicId = await callTransport(() =>\n transport.createThread({\n name: session!.name ?? 'Visitor',\n userAgent,\n now: new Date(),\n }),\n );\n session = await store.retopicSession(sessionId, session!, topicId);\n await callTransport(() => transport.send(topicId, trimmed));\n }\n\n const message: ChatMessage = {\n id: crypto.randomUUID(),\n from: 'visitor',\n text: trimmed,\n ts: Date.now(),\n };\n const cursor = await store.appendMessage(sessionId, message);\n\n const body: SendResponseBody = { message, cursor };\n return Response.json(body);\n } catch (error) {\n if (error instanceof TransportFailure) {\n // Log Telegram's own description. The visitor-facing message stays\n // generic, but without this the real cause is invisible in the logs.\n onError('chat.send.telegram', error.cause);\n return fail(502, {\n code: 'telegram_unavailable',\n error: copy.telegramUnavailable,\n });\n }\n onError(\n 'chat.send',\n error instanceof Error ? error.message : error,\n );\n return fail(503, {\n code: 'store_unavailable',\n error: copy.storeUnavailable,\n });\n }\n };\n}\n\n// ---------------------------------------------------------------------------\n// createPollHandler\n// ---------------------------------------------------------------------------\n\nexport function createPollHandler(\n resolved: ResolvedChatServerConfig,\n): (request: Request) => Promise<Response> {\n const { store, copy } = resolved;\n\n return async function pollHandler(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const sessionId = url.searchParams.get('sessionId');\n const since = Number(url.searchParams.get('since') ?? '0');\n\n if (!sessionId || sessionId.length < 8) {\n const body: ChatErrorBody = {\n code: 'validation',\n error: copy.validation.missingSession,\n };\n return Response.json(body, { status: 400 });\n }\n\n try {\n // An unknown or expired session yields an empty transcript rather than a\n // 404: the visitor should see an empty panel, not an error.\n const { messages, cursor } = await store.getMessagesSince(\n sessionId,\n Number.isFinite(since) ? since : 0,\n );\n const body: PollResponseBody = { messages, cursor };\n return Response.json(body, {\n headers: { 'Cache-Control': 'no-store' },\n });\n } catch {\n const body: ChatErrorBody = {\n code: 'store_unavailable',\n error: copy.storeUnavailable,\n };\n return Response.json(body, { status: 503 });\n }\n };\n}\n\n// ---------------------------------------------------------------------------\n// createWebhookHandler\n// ---------------------------------------------------------------------------\n\nexport function createWebhookHandler(\n resolved: ResolvedChatServerConfig,\n): (request: Request) => Promise<Response> {\n const { store, transport } = resolved;\n\n const ok = () => Response.json({ ok: true });\n\n return async function webhookHandler(request: Request): Promise<Response> {\n if (!transport.verifyWebhook(request)) {\n return Response.json({ ok: false }, { status: 401 });\n }\n\n let update: unknown;\n try {\n update = await request.json();\n } catch {\n return ok();\n }\n\n const reply = transport.parseInboundReply(update);\n if (!reply) return ok();\n\n try {\n const sessionId = await store.resolveTopic(reply.threadId);\n if (!sessionId) return ok();\n\n const message: ChatMessage = {\n id: `tg-${reply.messageId}`,\n from: 'owner',\n text: reply.text,\n ts: Date.now(),\n };\n await store.appendMessage(sessionId, message);\n } catch {\n // Swallowed deliberately: any non-2xx makes Telegram retry this same\n // update indefinitely.\n }\n\n return ok();\n };\n}\n","import type { ChatServerConfig } from './config.js';\nimport { resolveConfig } from './config.js';\nimport {\n createSendHandler,\n createPollHandler,\n createWebhookHandler,\n} from './handlers.js';\n\nexport interface ChatServer {\n send: (request: Request) => Promise<Response>;\n poll: (request: Request) => Promise<Response>;\n webhook: (request: Request) => Promise<Response>;\n}\n\nexport function createChatServer(config: ChatServerConfig): ChatServer {\n const resolved = resolveConfig(config);\n return {\n send: createSendHandler(resolved),\n poll: createPollHandler(resolved),\n webhook: createWebhookHandler(resolved),\n };\n}\n","import { Redis } from '@upstash/redis';\nimport type { ChatStore } from '../core/store-interface.js';\nimport type { ChatMessage, ChatSession } from '../core/types.js';\nimport { DEFAULT_CHAT_LIMITS } from '../core/types.js';\n\nexport interface UpstashRedisStoreOptions {\n url: string;\n token: string;\n /**\n * Sliding TTL applied to session, topic-map, and message-list writes.\n * Default: 604800 (7 days).\n */\n ttlSeconds?: number;\n}\n\n/**\n * Creates a ChatStore backed by Upstash Redis.\n *\n * The consumer resolves credentials. `Redis.fromEnv()` only reads\n * `UPSTASH_REDIS_REST_URL` / `UPSTASH_REDIS_REST_TOKEN`, but the Vercel\n * Upstash marketplace provisions `KV_REST_API_URL` / `KV_REST_API_TOKEN`.\n * Consumers should fall back between the two pairs (using `||`, not `??`,\n * since `vercel env pull` can write blank entries) and pass the resolved\n * `{ url, token }` here.\n */\nexport function upstashRedisStore(opts: UpstashRedisStoreOptions): ChatStore {\n const redis = new Redis({ url: opts.url, token: opts.token });\n const ttlSeconds = opts.ttlSeconds ?? DEFAULT_CHAT_LIMITS.sessionTtlSeconds;\n\n const sessionKey = (sessionId: string) => `chat:sess:${sessionId}`;\n const topicKey = (topicId: number) => `chat:topic:${topicId}`;\n const messagesKey = (sessionId: string) => `chat:msgs:${sessionId}`;\n\n /**\n * Writes the session and its reverse topic mapping, refreshing both TTLs.\n * Called on every session touch, which is what makes the 7-day window sliding.\n */\n async function writeSession(\n sessionId: string,\n session: ChatSession,\n ): Promise<void> {\n await redis.set(sessionKey(sessionId), session, { ex: ttlSeconds });\n await redis.set(topicKey(session.topicId), sessionId, {\n ex: ttlSeconds,\n });\n }\n\n return {\n async getSession(sessionId: string): Promise<ChatSession | null> {\n const raw = await redis.get<ChatSession>(sessionKey(sessionId));\n return raw ?? null;\n },\n\n async createSession(\n sessionId: string,\n topicId: number,\n name: string,\n ): Promise<ChatSession> {\n const session: ChatSession = {\n topicId,\n createdAt: Date.now(),\n msgCount: 0,\n name,\n };\n await writeSession(sessionId, session);\n return session;\n },\n\n /**\n * Adopts a name onto an existing session. Returns null when the session has\n * expired between the caller's read and this write.\n */\n async setSessionName(\n sessionId: string,\n name: string,\n ): Promise<ChatSession | null> {\n const session = await this.getSession(sessionId);\n if (!session) return null;\n const next: ChatSession = { ...session, name };\n await writeSession(sessionId, next);\n return next;\n },\n\n /**\n * Points a session at a replacement topic, dropping the reverse mapping for\n * the old one so a recycled thread id can never resolve to a stale session.\n */\n async retopicSession(\n sessionId: string,\n session: ChatSession,\n topicId: number,\n ): Promise<ChatSession> {\n if (session.topicId !== topicId) {\n await redis.del(topicKey(session.topicId));\n }\n const next: ChatSession = { ...session, topicId };\n await writeSession(sessionId, next);\n return next;\n },\n\n async resolveTopic(topicId: number): Promise<string | null> {\n return (await redis.get<string>(topicKey(topicId))) ?? null;\n },\n\n /**\n * Appends a message and returns the new cursor (the list length).\n *\n * msgCount is incremented only for visitor messages, so the owner's replies\n * never consume the visitor's per-session message budget.\n */\n async appendMessage(\n sessionId: string,\n message: ChatMessage,\n ): Promise<number> {\n const cursor = await redis.rpush(messagesKey(sessionId), message);\n await redis.expire(messagesKey(sessionId), ttlSeconds);\n\n const session = await this.getSession(sessionId);\n if (session) {\n await writeSession(sessionId, {\n ...session,\n msgCount:\n message.from === 'visitor' ? session.msgCount + 1 : session.msgCount,\n });\n }\n\n return cursor;\n },\n\n async getMessagesSince(\n sessionId: string,\n since: number,\n ): Promise<{ messages: ChatMessage[]; cursor: number }> {\n const start = Number.isFinite(since) && since > 0 ? Math.floor(since) : 0;\n const messages = await redis.lrange<ChatMessage>(\n messagesKey(sessionId),\n start,\n -1,\n );\n return { messages, cursor: start + messages.length };\n },\n\n /**\n * INCR returns the post-increment value, so a fresh key returns 1 and EXPIRE is\n * set only on that first hit. That keeps the window fixed rather than sliding\n * forward on every request, which would make the limit unreachable.\n */\n async incrementCounter(key: string, ttlSeconds: number): Promise<number> {\n const count = await redis.incr(key);\n if (count === 1) await redis.expire(key, ttlSeconds);\n return count;\n },\n };\n}\n","import type { ChatStore } from '../core/store-interface.js';\nimport type { ChatMessage, ChatSession } from '../core/types.js';\nimport { DEFAULT_CHAT_LIMITS } from '../core/types.js';\n\nexport interface InMemoryStoreOptions {\n /**\n * Sliding TTL applied to session, topic-map, and message-list writes.\n * Default: 604800 (7 days).\n */\n ttlSeconds?: number;\n /** Injectable clock for deterministic tests. Default: Date.now. */\n now?: () => number;\n}\n\ninterface StoredSession {\n session: ChatSession;\n expiresAt: number;\n}\n\ninterface StoredMessages {\n messages: ChatMessage[];\n expiresAt: number;\n}\n\ninterface StoredCounter {\n count: number;\n expiresAt: number;\n}\n\n/** Symbol for accessing internal key names in tests. */\nexport const inMemoryStoreKeysSymbol = Symbol.for('contact-chat.inMemoryStore.keys');\n\n/**\n * Creates a deterministic in-memory ChatStore, suitable for tests and the demo app.\n *\n * Each call returns a new, fully independent store instance with no shared state.\n */\nexport function inMemoryStore(opts?: InMemoryStoreOptions): ChatStore & {\n [inMemoryStoreKeysSymbol]: () => string[];\n} {\n const ttlSeconds = opts?.ttlSeconds ?? DEFAULT_CHAT_LIMITS.sessionTtlSeconds;\n const now = opts?.now ?? Date.now;\n\n // Internal maps using the same key namespace as UpstashRedisStore.\n const sessions = new Map<string, StoredSession>();\n const topics = new Map<string, string>(); // topicKey -> sessionId\n const messages = new Map<string, StoredMessages>();\n const counters = new Map<string, StoredCounter>();\n\n const sessionKey = (sessionId: string) => `chat:sess:${sessionId}`;\n const topicKey = (topicId: number) => `chat:topic:${topicId}`;\n const messagesKey = (sessionId: string) => `chat:msgs:${sessionId}`;\n\n /**\n * Writes the session and its reverse topic mapping, refreshing both TTLs.\n * Called on every session touch, which is what makes the 7-day window sliding.\n */\n function writeSession(sessionId: string, session: ChatSession): void {\n const expiresAt = now() + ttlSeconds * 1000;\n sessions.set(sessionKey(sessionId), {\n session: structuredClone(session),\n expiresAt,\n });\n topics.set(topicKey(session.topicId), sessionId);\n }\n\n function getStoredSession(sessionId: string): ChatSession | null {\n const stored = sessions.get(sessionKey(sessionId));\n if (!stored) return null;\n if (stored.expiresAt <= now()) {\n sessions.delete(sessionKey(sessionId));\n return null;\n }\n return structuredClone(stored.session);\n }\n\n const store = {\n async getSession(sessionId: string): Promise<ChatSession | null> {\n return getStoredSession(sessionId);\n },\n\n async createSession(\n sessionId: string,\n topicId: number,\n name: string,\n ): Promise<ChatSession> {\n const session: ChatSession = {\n topicId,\n createdAt: now(),\n msgCount: 0,\n name,\n };\n writeSession(sessionId, session);\n return structuredClone(session);\n },\n\n /**\n * Adopts a name onto an existing session. Returns null when the session has\n * expired between the caller's read and this write.\n */\n async setSessionName(\n sessionId: string,\n name: string,\n ): Promise<ChatSession | null> {\n const session = getStoredSession(sessionId);\n if (!session) return null;\n const next: ChatSession = { ...session, name };\n writeSession(sessionId, next);\n return structuredClone(next);\n },\n\n /**\n * Points a session at a replacement topic, dropping the reverse mapping for\n * the old one so a recycled thread id can never resolve to a stale session.\n */\n async retopicSession(\n sessionId: string,\n session: ChatSession,\n topicId: number,\n ): Promise<ChatSession> {\n if (session.topicId !== topicId) {\n topics.delete(topicKey(session.topicId));\n }\n const next: ChatSession = { ...session, topicId };\n writeSession(sessionId, next);\n return structuredClone(next);\n },\n\n async resolveTopic(topicId: number): Promise<string | null> {\n const key = topicKey(topicId);\n const sessionId = topics.get(key);\n if (!sessionId) return null;\n // Verify the session hasn't expired.\n const session = getStoredSession(sessionId);\n if (!session) {\n topics.delete(key);\n return null;\n }\n return sessionId;\n },\n\n /**\n * Appends a message and returns the new cursor (the list length).\n *\n * msgCount is incremented only for visitor messages, so the owner's replies\n * never consume the visitor's per-session message budget.\n */\n async appendMessage(\n sessionId: string,\n message: ChatMessage,\n ): Promise<number> {\n const key = messagesKey(sessionId);\n const expiresAt = now() + ttlSeconds * 1000;\n const stored = messages.get(key);\n const existing = stored && stored.expiresAt > now() ? stored : null;\n const list: ChatMessage[] = existing ? [...existing.messages] : [];\n list.push(structuredClone(message));\n messages.set(key, { messages: list, expiresAt });\n\n const session = getStoredSession(sessionId);\n if (session) {\n writeSession(sessionId, {\n ...session,\n msgCount:\n message.from === 'visitor' ? session.msgCount + 1 : session.msgCount,\n });\n }\n\n return list.length;\n },\n\n async getMessagesSince(\n sessionId: string,\n since: number,\n ): Promise<{ messages: ChatMessage[]; cursor: number }> {\n const start = Number.isFinite(since) && since > 0 ? Math.floor(since) : 0;\n const key = messagesKey(sessionId);\n const stored = messages.get(key);\n if (!stored || stored.expiresAt <= now()) {\n return { messages: [], cursor: start };\n }\n const slice = stored.messages.slice(start);\n return { messages: structuredClone(slice), cursor: start + slice.length };\n },\n\n /**\n * INCR returns the post-increment value, so a fresh key returns 1 and EXPIRE is\n * set only on that first hit. That keeps the window fixed rather than sliding\n * forward on every request, which would make the limit unreachable.\n */\n async incrementCounter(key: string, ttlSeconds: number): Promise<number> {\n const existing = counters.get(key);\n if (!existing || existing.expiresAt <= now()) {\n counters.set(key, { count: 1, expiresAt: now() + ttlSeconds * 1000 });\n return 1;\n }\n existing.count++;\n return existing.count;\n },\n\n /** Internal accessor for tests — not part of the ChatStore interface. */\n [inMemoryStoreKeysSymbol](): string[] {\n return [\n ...sessions.keys(),\n ...topics.keys(),\n ...messages.keys(),\n ...counters.keys(),\n ];\n },\n };\n\n return store;\n}\n","import type { ChatTransport, TopicNameInput } from '../core/transport-interface.js';\n\nexport class TelegramError extends Error {\n constructor(\n message: string,\n /** Telegram's own `description`, kept so callers can classify the failure. */\n readonly description?: string\n ) {\n super(message);\n }\n}\n\n/**\n * True when Telegram rejected the send because the forum topic no longer\n * exists — typically because the owner deleted it while tidying the group.\n * The session's threadId is then permanently dead and must be replaced.\n */\nexport function isMissingThreadError(error: unknown): boolean {\n if (!(error instanceof TelegramError)) return false;\n const detail = (error.description ?? error.message).toLowerCase();\n return (\n detail.includes('thread not found') ||\n detail.includes('topic_deleted') ||\n detail.includes('topic was deleted') ||\n detail.includes('thread_not_found')\n );\n}\n\nconst API_BASE = 'https://api.telegram.org';\n\nexport function describeBrowser(userAgent: string | null): string {\n if (!userAgent) return 'Unknown browser';\n if (/edg\\//i.test(userAgent)) return 'Edge';\n if (/opr\\//i.test(userAgent)) return 'Opera';\n if (/chrome\\//i.test(userAgent)) return 'Chrome';\n if (/firefox\\//i.test(userAgent)) return 'Firefox';\n if (/safari\\//i.test(userAgent)) return 'Safari';\n return 'Unknown browser';\n}\n\n/**\n * Produces e.g. \"Roman-04-08-17-42-Chrome\".\n *\n * Date and time come from Intl with an explicit timeZone: the server runs UTC,\n * so reading Date methods directly would stamp titles three hours off Kyiv.\n */\nexport function buildTopicName(input: TopicNameInput): string {\n const parts = new Intl.DateTimeFormat('en-GB', {\n timeZone: 'Europe/Kyiv',\n day: '2-digit',\n month: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n hour12: false,\n }).formatToParts(input.now);\n\n const at = (type: Intl.DateTimeFormatPartTypes) =>\n parts.find((part) => part.type === type)?.value ?? '00';\n\n return [\n input.name,\n at('day'),\n at('month'),\n at('hour'),\n at('minute'),\n describeBrowser(input.userAgent),\n ]\n .join('-')\n .slice(0, 128);\n}\n\nexport interface TelegramTransportOptions {\n botToken: string;\n chatId: string;\n /** The transport verifies inbound webhooks by comparing this against the x-telegram-bot-api-secret-token header. */\n webhookSecret: string;\n /** Telegram's fixed six-colour palette. Default 0xcb86db (purple). */\n topicIconColor?: number;\n /** Override the default topic-name format. Default: `${name}-DD-MM-HH-MM-${browser}` in Europe/Kyiv, sliced to 128 chars. */\n buildTopicName?: (input: TopicNameInput) => string;\n /** Optional fetch override for tests / edge runtimes. Default: globalThis.fetch. */\n fetch?: typeof fetch;\n}\n\nexport function telegramTransport(opts: TelegramTransportOptions): ChatTransport {\n const {\n botToken,\n chatId,\n webhookSecret,\n topicIconColor = 0xcb86db,\n buildTopicName: buildTopicNameFn = buildTopicName,\n fetch: fetchFn = globalThis.fetch,\n } = opts;\n\n if (!botToken) {\n throw new Error('telegramTransport: botToken is required.');\n }\n if (!chatId) {\n throw new Error('telegramTransport: chatId is required.');\n }\n if (!webhookSecret) {\n throw new Error(\n 'telegramTransport: webhookSecret is required. Pass the same value you configured with Telegram via /setWebhook.',\n );\n }\n\n async function callApi<T>(\n method: string,\n body: Record<string, unknown>\n ): Promise<T> {\n const response = await fetchFn(`${API_BASE}/bot${botToken}/${method}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n cache: 'no-store',\n });\n\n const payload = (await response.json()) as {\n ok: boolean;\n result?: T;\n description?: string;\n };\n\n if (!response.ok || !payload.ok) {\n throw new TelegramError(\n `${method} failed: ${payload.description ?? response.status}`,\n payload.description\n );\n }\n\n return payload.result as T;\n }\n\n return {\n async createThread(input: TopicNameInput): Promise<number> {\n const name = buildTopicNameFn(input);\n const result = await callApi<{ message_thread_id: number }>(\n 'createForumTopic',\n {\n chat_id: chatId,\n name: name.slice(0, 128),\n // Telegram snaps icon_color to a fixed six-colour palette and silently\n // substitutes the nearest allowed value. 0xCB86DB is the purple one;\n // an arbitrary brand hex such as 0x9333EA comes back as blue.\n icon_color: topicIconColor,\n }\n );\n return result.message_thread_id;\n },\n\n async send(threadId: number, text: string): Promise<void> {\n // No parse_mode: visitor text must never be parsed as Markdown or HTML.\n await callApi('sendMessage', {\n chat_id: chatId,\n message_thread_id: threadId,\n text,\n });\n },\n\n /**\n * Retitles an existing topic. Needed when a session created before the name\n * gate shipped supplies a name for the first time — the topic already exists,\n * so it has to be renamed rather than created.\n */\n async renameThread(threadId: number, input: TopicNameInput): Promise<void> {\n const name = buildTopicNameFn(input);\n await callApi('editForumTopic', {\n chat_id: chatId,\n message_thread_id: threadId,\n name: name.slice(0, 128),\n });\n },\n\n isMissingThreadError,\n\n /**\n * Narrows an inbound webhook update to an owner reply, or null.\n *\n * Every inbound filter lives here: not a message, sent by a bot, missing a\n * message_thread_id (i.e. posted in the group's General area rather than a\n * topic), or from an unexpected chat all return null.\n */\n parseInboundReply(\n update: unknown\n ): { threadId: number; text: string; messageId: number } | null {\n if (typeof update !== 'object' || update === null) return null;\n\n const message = (update as { message?: unknown }).message;\n if (typeof message !== 'object' || message === null) return null;\n\n const {\n message_id: messageId,\n message_thread_id: threadId,\n text,\n chat,\n from,\n } = message as {\n message_id?: unknown;\n message_thread_id?: unknown;\n text?: unknown;\n chat?: { id?: unknown };\n from?: { is_bot?: unknown };\n };\n\n if (from?.is_bot === true) return null;\n if (typeof threadId !== 'number') return null;\n if (typeof messageId !== 'number') return null;\n if (typeof text !== 'string' || text.trim().length === 0) return null;\n\n if (!chatId || String(chat?.id) !== chatId) return null;\n\n return { threadId, text, messageId };\n },\n\n verifyWebhook(request: Request): boolean {\n const header = request.headers.get('x-telegram-bot-api-secret-token');\n if (!header) return false;\n return header === webhookSecret;\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
var jotai = require('jotai');
|
|
6
|
+
|
|
7
|
+
// src/react/config.tsx
|
|
8
|
+
|
|
9
|
+
// src/core/types.ts
|
|
10
|
+
var MAX_MESSAGE_LENGTH = 1e3;
|
|
11
|
+
var DEFAULT_CHAT_LIMITS = {
|
|
12
|
+
maxMessageLength: MAX_MESSAGE_LENGTH};
|
|
13
|
+
var DEFAULT_CHAT_CLIENT_CONFIG = {
|
|
14
|
+
basePath: "/api/chat",
|
|
15
|
+
sessionStorageKey: "contact-chat-session",
|
|
16
|
+
nameStorageKey: "contact-chat-name",
|
|
17
|
+
chime: true,
|
|
18
|
+
maxMessageLength: DEFAULT_CHAT_LIMITS.maxMessageLength
|
|
19
|
+
};
|
|
20
|
+
var Ctx = react.createContext(DEFAULT_CHAT_CLIENT_CONFIG);
|
|
21
|
+
function ChatClientProvider({
|
|
22
|
+
children,
|
|
23
|
+
...overrides
|
|
24
|
+
}) {
|
|
25
|
+
const merged = react.useMemo(
|
|
26
|
+
() => ({ ...DEFAULT_CHAT_CLIENT_CONFIG, ...overrides }),
|
|
27
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
28
|
+
[
|
|
29
|
+
overrides.basePath,
|
|
30
|
+
overrides.sessionStorageKey,
|
|
31
|
+
overrides.nameStorageKey,
|
|
32
|
+
overrides.chime,
|
|
33
|
+
overrides.maxMessageLength,
|
|
34
|
+
overrides.onEvent
|
|
35
|
+
]
|
|
36
|
+
);
|
|
37
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Ctx.Provider, { value: merged, children });
|
|
38
|
+
}
|
|
39
|
+
function useChatConfig() {
|
|
40
|
+
return react.useContext(Ctx);
|
|
41
|
+
}
|
|
42
|
+
var chatOpenAtom = jotai.atom(false);
|
|
43
|
+
var chatSessionIdAtom = jotai.atom(null);
|
|
44
|
+
var chatNameAtom = jotai.atom(null);
|
|
45
|
+
var chatMessagesAtom = jotai.atom([]);
|
|
46
|
+
var chatCursorAtom = jotai.atom(0);
|
|
47
|
+
var chatUnreadAtom = jotai.atom(0);
|
|
48
|
+
var chatStatusAtom = jotai.atom("idle");
|
|
49
|
+
var chatErrorAtom = jotai.atom(null);
|
|
50
|
+
|
|
51
|
+
// src/core/name.ts
|
|
52
|
+
var MAX_NAME_LENGTH = 60;
|
|
53
|
+
function sanitizeName(raw) {
|
|
54
|
+
if (typeof raw !== "string") return "";
|
|
55
|
+
return raw.replace(/[\u0000-\u001F\u007F]/g, " ").replace(/\s+/g, " ").trim().slice(0, MAX_NAME_LENGTH);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/react/client.ts
|
|
59
|
+
function getStoredName(nameStorageKey) {
|
|
60
|
+
if (typeof window === "undefined") return null;
|
|
61
|
+
return window.localStorage.getItem(nameStorageKey);
|
|
62
|
+
}
|
|
63
|
+
function storeName(nameStorageKey, name) {
|
|
64
|
+
window.localStorage.setItem(nameStorageKey, name);
|
|
65
|
+
}
|
|
66
|
+
function getStoredSessionId(sessionStorageKey) {
|
|
67
|
+
if (typeof window === "undefined") return null;
|
|
68
|
+
return window.localStorage.getItem(sessionStorageKey);
|
|
69
|
+
}
|
|
70
|
+
function ensureSessionId(sessionStorageKey) {
|
|
71
|
+
const existing = getStoredSessionId(sessionStorageKey);
|
|
72
|
+
if (existing) return existing;
|
|
73
|
+
const created = window.crypto.randomUUID();
|
|
74
|
+
window.localStorage.setItem(sessionStorageKey, created);
|
|
75
|
+
return created;
|
|
76
|
+
}
|
|
77
|
+
function mergeMessages(current, incoming) {
|
|
78
|
+
const seen = new Set(current.map((message) => message.id));
|
|
79
|
+
const fresh = incoming.filter((message) => !seen.has(message.id));
|
|
80
|
+
if (fresh.length === 0) return current;
|
|
81
|
+
return [...current, ...fresh].sort((a, b) => a.ts - b.ts);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/react/use-chat-actions.ts
|
|
85
|
+
function useChatActions() {
|
|
86
|
+
const { basePath, sessionStorageKey, nameStorageKey, maxMessageLength, onEvent } = useChatConfig();
|
|
87
|
+
const [open, originalSetOpen] = jotai.useAtom(chatOpenAtom);
|
|
88
|
+
const messages = jotai.useAtomValue(chatMessagesAtom);
|
|
89
|
+
const setMessages = jotai.useSetAtom(chatMessagesAtom);
|
|
90
|
+
const unread = jotai.useAtomValue(chatUnreadAtom);
|
|
91
|
+
const [status, setStatus] = jotai.useAtom(chatStatusAtom);
|
|
92
|
+
const [error, setError] = jotai.useAtom(chatErrorAtom);
|
|
93
|
+
const [name, setNameAtomValue] = jotai.useAtom(chatNameAtom);
|
|
94
|
+
const setSessionId = jotai.useSetAtom(chatSessionIdAtom);
|
|
95
|
+
const setOpen = react.useCallback(
|
|
96
|
+
(v) => {
|
|
97
|
+
const next = typeof v === "function" ? v(open) : v;
|
|
98
|
+
originalSetOpen(next);
|
|
99
|
+
if (next === true) onEvent?.({ type: "chat.open" });
|
|
100
|
+
else if (next === false) onEvent?.({ type: "chat.close" });
|
|
101
|
+
},
|
|
102
|
+
[open, originalSetOpen, onEvent]
|
|
103
|
+
);
|
|
104
|
+
const setName = react.useCallback(
|
|
105
|
+
(raw) => {
|
|
106
|
+
const clean = sanitizeName(raw);
|
|
107
|
+
if (!clean) return false;
|
|
108
|
+
storeName(nameStorageKey, clean);
|
|
109
|
+
setNameAtomValue(clean);
|
|
110
|
+
return true;
|
|
111
|
+
},
|
|
112
|
+
[nameStorageKey, setNameAtomValue]
|
|
113
|
+
);
|
|
114
|
+
const send = react.useCallback(
|
|
115
|
+
async (text) => {
|
|
116
|
+
const trimmed = text.trim();
|
|
117
|
+
if (trimmed.length === 0 || trimmed.length > maxMessageLength) return;
|
|
118
|
+
const sessionId = ensureSessionId(sessionStorageKey);
|
|
119
|
+
setSessionId(sessionId);
|
|
120
|
+
setStatus("sending");
|
|
121
|
+
setError(null);
|
|
122
|
+
onEvent?.({ type: "chat.send.attempt" });
|
|
123
|
+
try {
|
|
124
|
+
const response = await fetch(`${basePath}/send`, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: { "Content-Type": "application/json" },
|
|
127
|
+
body: JSON.stringify({
|
|
128
|
+
sessionId,
|
|
129
|
+
text: trimmed,
|
|
130
|
+
name: getStoredName(nameStorageKey) ?? "",
|
|
131
|
+
honeypot: ""
|
|
132
|
+
})
|
|
133
|
+
});
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
const body = await response.json();
|
|
136
|
+
setError(body.error);
|
|
137
|
+
setStatus(
|
|
138
|
+
body.code === "store_unavailable" ? "unavailable" : "error"
|
|
139
|
+
);
|
|
140
|
+
onEvent?.({ type: "chat.send.error", code: body.code });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const data = await response.json();
|
|
144
|
+
setMessages((current) => mergeMessages(current, [data.message]));
|
|
145
|
+
setStatus("idle");
|
|
146
|
+
onEvent?.({ type: "chat.send.success" });
|
|
147
|
+
} catch {
|
|
148
|
+
setError("Chat is unavailable right now.");
|
|
149
|
+
setStatus("unavailable");
|
|
150
|
+
onEvent?.({ type: "chat.send.error", code: "network" });
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
[
|
|
154
|
+
basePath,
|
|
155
|
+
maxMessageLength,
|
|
156
|
+
nameStorageKey,
|
|
157
|
+
onEvent,
|
|
158
|
+
sessionStorageKey,
|
|
159
|
+
setError,
|
|
160
|
+
setMessages,
|
|
161
|
+
setSessionId,
|
|
162
|
+
setStatus
|
|
163
|
+
]
|
|
164
|
+
);
|
|
165
|
+
return {
|
|
166
|
+
open,
|
|
167
|
+
setOpen,
|
|
168
|
+
messages,
|
|
169
|
+
unread,
|
|
170
|
+
status,
|
|
171
|
+
error,
|
|
172
|
+
name,
|
|
173
|
+
setName,
|
|
174
|
+
send,
|
|
175
|
+
canSend: status !== "sending"
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/react/chime.ts
|
|
180
|
+
var context = null;
|
|
181
|
+
function getContext() {
|
|
182
|
+
if (typeof window === "undefined") return null;
|
|
183
|
+
const Ctor = window.AudioContext ?? window.webkitAudioContext;
|
|
184
|
+
if (!Ctor) return null;
|
|
185
|
+
if (!context) context = new Ctor();
|
|
186
|
+
return context;
|
|
187
|
+
}
|
|
188
|
+
function playChime() {
|
|
189
|
+
try {
|
|
190
|
+
const audio = getContext();
|
|
191
|
+
if (!audio) return;
|
|
192
|
+
if (audio.state === "suspended") void audio.resume();
|
|
193
|
+
const startedAt = audio.currentTime;
|
|
194
|
+
const notes = [
|
|
195
|
+
{ frequency: 880, offset: 0 },
|
|
196
|
+
{ frequency: 1320, offset: 0.1 }
|
|
197
|
+
];
|
|
198
|
+
for (const { frequency, offset } of notes) {
|
|
199
|
+
const oscillator = audio.createOscillator();
|
|
200
|
+
const gain = audio.createGain();
|
|
201
|
+
oscillator.type = "sine";
|
|
202
|
+
oscillator.frequency.value = frequency;
|
|
203
|
+
const start = startedAt + offset;
|
|
204
|
+
const end = start + 0.09;
|
|
205
|
+
gain.gain.setValueAtTime(0, start);
|
|
206
|
+
gain.gain.linearRampToValueAtTime(0.15, start + 0.01);
|
|
207
|
+
gain.gain.linearRampToValueAtTime(0, end);
|
|
208
|
+
oscillator.connect(gain).connect(audio.destination);
|
|
209
|
+
oscillator.start(start);
|
|
210
|
+
oscillator.stop(end + 0.02);
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/react/use-chat-sync.ts
|
|
217
|
+
var FAST_INTERVAL_MS = 4e3;
|
|
218
|
+
var SLOW_INTERVAL_MS = 15e3;
|
|
219
|
+
var IDLE_BEFORE_SLOW_MS = 12e4;
|
|
220
|
+
function useChatSync() {
|
|
221
|
+
const { basePath, sessionStorageKey, nameStorageKey, chime } = useChatConfig();
|
|
222
|
+
const open = jotai.useAtomValue(chatOpenAtom);
|
|
223
|
+
const setSessionId = jotai.useSetAtom(chatSessionIdAtom);
|
|
224
|
+
const setName = jotai.useSetAtom(chatNameAtom);
|
|
225
|
+
const setMessages = jotai.useSetAtom(chatMessagesAtom);
|
|
226
|
+
const [cursor, setCursor] = jotai.useAtom(chatCursorAtom);
|
|
227
|
+
const setUnread = jotai.useSetAtom(chatUnreadAtom);
|
|
228
|
+
const [active, setActive] = react.useState(true);
|
|
229
|
+
const cursorRef = react.useRef(cursor);
|
|
230
|
+
const lastMessageAtRef = react.useRef(0);
|
|
231
|
+
const hasHydratedRef = react.useRef(false);
|
|
232
|
+
react.useEffect(() => {
|
|
233
|
+
cursorRef.current = cursor;
|
|
234
|
+
}, [cursor]);
|
|
235
|
+
react.useEffect(() => {
|
|
236
|
+
const storedSession = getStoredSessionId(sessionStorageKey);
|
|
237
|
+
if (storedSession) setSessionId(storedSession);
|
|
238
|
+
const storedName = getStoredName(nameStorageKey);
|
|
239
|
+
if (storedName) setName(storedName);
|
|
240
|
+
}, [sessionStorageKey, nameStorageKey, setSessionId, setName]);
|
|
241
|
+
react.useEffect(() => {
|
|
242
|
+
const recompute = () => setActive(document.visibilityState === "visible" || document.hasFocus());
|
|
243
|
+
recompute();
|
|
244
|
+
document.addEventListener("visibilitychange", recompute);
|
|
245
|
+
window.addEventListener("focus", recompute);
|
|
246
|
+
window.addEventListener("blur", recompute);
|
|
247
|
+
return () => {
|
|
248
|
+
document.removeEventListener("visibilitychange", recompute);
|
|
249
|
+
window.removeEventListener("focus", recompute);
|
|
250
|
+
window.removeEventListener("blur", recompute);
|
|
251
|
+
};
|
|
252
|
+
}, []);
|
|
253
|
+
const poll = react.useCallback(async () => {
|
|
254
|
+
const sessionId = getStoredSessionId(sessionStorageKey);
|
|
255
|
+
if (!sessionId) return;
|
|
256
|
+
try {
|
|
257
|
+
const response = await fetch(
|
|
258
|
+
`${basePath}/poll?sessionId=${encodeURIComponent(sessionId)}&since=${cursorRef.current}`,
|
|
259
|
+
{ cache: "no-store" }
|
|
260
|
+
);
|
|
261
|
+
if (!response.ok) return;
|
|
262
|
+
const data = await response.json();
|
|
263
|
+
if (data.messages.length === 0) return;
|
|
264
|
+
lastMessageAtRef.current = Date.now();
|
|
265
|
+
setMessages((current) => mergeMessages(current, data.messages));
|
|
266
|
+
cursorRef.current = data.cursor;
|
|
267
|
+
setCursor(data.cursor);
|
|
268
|
+
const fromOwner = data.messages.filter(
|
|
269
|
+
(message) => message.from === "owner"
|
|
270
|
+
).length;
|
|
271
|
+
const isHydrating = !hasHydratedRef.current;
|
|
272
|
+
hasHydratedRef.current = true;
|
|
273
|
+
if (fromOwner > 0 && !isHydrating && chime) playChime();
|
|
274
|
+
if (fromOwner > 0 && !open) setUnread((count) => count + fromOwner);
|
|
275
|
+
} catch {
|
|
276
|
+
}
|
|
277
|
+
}, [basePath, chime, open, sessionStorageKey, setCursor, setMessages, setUnread]);
|
|
278
|
+
react.useEffect(() => {
|
|
279
|
+
if (!active) return;
|
|
280
|
+
if (!open) {
|
|
281
|
+
void poll();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
let timer = null;
|
|
285
|
+
let cancelled = false;
|
|
286
|
+
const tick = async () => {
|
|
287
|
+
if (cancelled) return;
|
|
288
|
+
await poll();
|
|
289
|
+
if (cancelled) return;
|
|
290
|
+
const since = lastMessageAtRef.current || Date.now();
|
|
291
|
+
const delay = Date.now() - since > IDLE_BEFORE_SLOW_MS ? SLOW_INTERVAL_MS : FAST_INTERVAL_MS;
|
|
292
|
+
timer = setTimeout(tick, delay);
|
|
293
|
+
};
|
|
294
|
+
lastMessageAtRef.current = Date.now();
|
|
295
|
+
void tick();
|
|
296
|
+
return () => {
|
|
297
|
+
cancelled = true;
|
|
298
|
+
if (timer) clearTimeout(timer);
|
|
299
|
+
};
|
|
300
|
+
}, [open, active, poll]);
|
|
301
|
+
react.useEffect(() => {
|
|
302
|
+
if (open) setUnread(0);
|
|
303
|
+
}, [open, setUnread]);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
exports.ChatClientProvider = ChatClientProvider;
|
|
307
|
+
exports.DEFAULT_CHAT_CLIENT_CONFIG = DEFAULT_CHAT_CLIENT_CONFIG;
|
|
308
|
+
exports.chatCursorAtom = chatCursorAtom;
|
|
309
|
+
exports.chatErrorAtom = chatErrorAtom;
|
|
310
|
+
exports.chatMessagesAtom = chatMessagesAtom;
|
|
311
|
+
exports.chatNameAtom = chatNameAtom;
|
|
312
|
+
exports.chatOpenAtom = chatOpenAtom;
|
|
313
|
+
exports.chatSessionIdAtom = chatSessionIdAtom;
|
|
314
|
+
exports.chatStatusAtom = chatStatusAtom;
|
|
315
|
+
exports.chatUnreadAtom = chatUnreadAtom;
|
|
316
|
+
exports.ensureSessionId = ensureSessionId;
|
|
317
|
+
exports.getStoredName = getStoredName;
|
|
318
|
+
exports.getStoredSessionId = getStoredSessionId;
|
|
319
|
+
exports.mergeMessages = mergeMessages;
|
|
320
|
+
exports.playChime = playChime;
|
|
321
|
+
exports.storeName = storeName;
|
|
322
|
+
exports.useChatActions = useChatActions;
|
|
323
|
+
exports.useChatConfig = useChatConfig;
|
|
324
|
+
exports.useChatSync = useChatSync;
|
|
325
|
+
//# sourceMappingURL=index.cjs.map
|
|
326
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/types.ts","../../src/react/config.tsx","../../src/react/atoms.ts","../../src/core/name.ts","../../src/react/client.ts","../../src/react/use-chat-actions.ts","../../src/react/chime.ts","../../src/react/use-chat-sync.ts"],"names":["createContext","useMemo","useContext","atom","useAtom","useAtomValue","useSetAtom","useCallback","useState","useRef","useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["export type ChatAuthor = 'visitor' | 'owner';\n\nexport interface ChatMessage {\n id: string;\n from: ChatAuthor;\n text: string;\n ts: number;\n}\n\nexport interface ChatSession {\n topicId: number;\n createdAt: number;\n msgCount: number;\n // Optional because sessions created before the name gate shipped have none.\n name?: string;\n}\n\nexport interface SendRequestBody {\n sessionId: string;\n text: string;\n name?: string;\n honeypot?: string;\n}\n\nexport interface SendResponseBody {\n message: ChatMessage;\n cursor: number;\n}\n\nexport interface PollResponseBody {\n messages: ChatMessage[];\n cursor: number;\n}\n\nexport type ChatErrorCode =\n | 'validation'\n | 'rate_limited'\n | 'store_unavailable'\n | 'telegram_unavailable';\n\nexport interface ChatErrorBody {\n code: ChatErrorCode;\n error: string;\n}\n\n// @deprecated Use DEFAULT_CHAT_LIMITS.maxMessageLength instead.\nexport const MAX_MESSAGE_LENGTH = 1000;\n// @deprecated Use DEFAULT_CHAT_LIMITS.maxMessagesPerSession instead.\nexport const MAX_MESSAGES_PER_SESSION = 40;\n// @deprecated Use DEFAULT_CHAT_LIMITS.sessionTtlSeconds instead.\nexport const SESSION_TTL_SECONDS = 604800; // 7 days\n// @deprecated Use DEFAULT_CHAT_LIMITS.ratePerMinute instead.\nexport const RATE_PER_MINUTE = 5;\n// @deprecated Use DEFAULT_CHAT_LIMITS.ratePerDay instead.\nexport const RATE_PER_DAY = 30;\n\nexport interface ChatLimits {\n maxMessageLength: number;\n maxMessagesPerSession: number;\n sessionTtlSeconds: number;\n ratePerMinute: number;\n ratePerDay: number;\n}\n\nexport const DEFAULT_CHAT_LIMITS: ChatLimits = {\n maxMessageLength: MAX_MESSAGE_LENGTH,\n maxMessagesPerSession: MAX_MESSAGES_PER_SESSION,\n sessionTtlSeconds: SESSION_TTL_SECONDS,\n ratePerMinute: RATE_PER_MINUTE,\n ratePerDay: RATE_PER_DAY,\n};\n","'use client';\n\nimport React, { createContext, useContext, useMemo } from 'react';\nimport { DEFAULT_CHAT_LIMITS } from '../core/types.js';\n\nexport type ChatClientEvent =\n | { type: 'chat.open' }\n | { type: 'chat.close' }\n | { type: 'chat.send.attempt' }\n | { type: 'chat.send.success' }\n | { type: 'chat.send.error'; code: string };\n\nexport interface ChatClientConfig {\n /** Base path for the three route files. Default: '/api/chat'. */\n basePath: string;\n /** localStorage key for the session id. Default: 'contact-chat-session'. */\n sessionStorageKey: string;\n /** localStorage key for the visitor's name. Default: 'contact-chat-name'. */\n nameStorageKey: string;\n /** Play a chime when an owner reply arrives. Default: true. */\n chime: boolean;\n /** Client-side max message length; matches the server's `ChatLimits.maxMessageLength`. Default: DEFAULT_CHAT_LIMITS.maxMessageLength. */\n maxMessageLength: number;\n /** Optional analytics callback fired on lifecycle events. */\n onEvent?: (event: ChatClientEvent) => void;\n}\n\nexport const DEFAULT_CHAT_CLIENT_CONFIG: ChatClientConfig = {\n basePath: '/api/chat',\n sessionStorageKey: 'contact-chat-session',\n nameStorageKey: 'contact-chat-name',\n chime: true,\n maxMessageLength: DEFAULT_CHAT_LIMITS.maxMessageLength,\n};\n\nconst Ctx = createContext<ChatClientConfig>(DEFAULT_CHAT_CLIENT_CONFIG);\n\nexport function ChatClientProvider({\n children,\n ...overrides\n}: Partial<ChatClientConfig> & { children: React.ReactNode }): React.JSX.Element {\n const merged = useMemo<ChatClientConfig>(\n () => ({ ...DEFAULT_CHAT_CLIENT_CONFIG, ...overrides }),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [\n overrides.basePath,\n overrides.sessionStorageKey,\n overrides.nameStorageKey,\n overrides.chime,\n overrides.maxMessageLength,\n overrides.onEvent,\n ]\n );\n\n return <Ctx.Provider value={merged}>{children}</Ctx.Provider>;\n}\n\nexport function useChatConfig(): ChatClientConfig {\n return useContext(Ctx);\n}\n","import { atom } from 'jotai';\nimport type { ChatMessage } from '../core/types.js';\n\nexport type ChatStatus = 'idle' | 'sending' | 'error' | 'unavailable';\n\nexport const chatOpenAtom = atom(false);\nexport const chatSessionIdAtom = atom<string | null>(null);\nexport const chatNameAtom = atom<string | null>(null);\nexport const chatMessagesAtom = atom<ChatMessage[]>([]);\nexport const chatCursorAtom = atom(0);\nexport const chatUnreadAtom = atom(0);\nexport const chatStatusAtom = atom<ChatStatus>('idle');\nexport const chatErrorAtom = atom<string | null>(null);\n","export const MAX_NAME_LENGTH = 60;\n\n/**\n * Normalises a visitor-supplied name for display in the owner's Telegram.\n * Returns '' when nothing usable remains — emptiness is the only failure.\n *\n * Not an injection defence: Telegram does not parse topic names as markup.\n * This exists so control characters and runaway length cannot reach the\n * owner's topic list.\n */\nexport function sanitizeName(raw: unknown): string {\n if (typeof raw !== 'string') return '';\n return raw\n // Strip control characters, including newlines and tabs.\n .replace(/[\\u0000-\\u001F\\u007F]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, MAX_NAME_LENGTH);\n}\n","import type { ChatMessage } from '../core/types.js';\n\nexport function getStoredName(nameStorageKey: string): string | null {\n if (typeof window === 'undefined') return null;\n return window.localStorage.getItem(nameStorageKey);\n}\n\nexport function storeName(nameStorageKey: string, name: string): void {\n window.localStorage.setItem(nameStorageKey, name);\n}\n\nexport function getStoredSessionId(sessionStorageKey: string): string | null {\n if (typeof window === 'undefined') return null;\n return window.localStorage.getItem(sessionStorageKey);\n}\n\nexport function ensureSessionId(sessionStorageKey: string): string {\n const existing = getStoredSessionId(sessionStorageKey);\n if (existing) return existing;\n const created = window.crypto.randomUUID();\n window.localStorage.setItem(sessionStorageKey, created);\n return created;\n}\n\n/**\n * Merges polled messages into the local list, deduplicating by id.\n *\n * Deduplication is what lets send() append its message optimistically without\n * touching the cursor. Advancing the cursor from the send response would skip\n * anything already sitting at a lower index — an owner reply that arrived\n * between polls would be lost. Instead the cursor advances only from polling,\n * and the visitor's own message is recognised by id when it comes back.\n *\n * Sorted by server timestamp so an owner reply that arrives at a lower index\n * still displays before the visitor's newer message.\n */\nexport function mergeMessages(\n current: ChatMessage[],\n incoming: ChatMessage[]\n): ChatMessage[] {\n const seen = new Set(current.map((message) => message.id));\n const fresh = incoming.filter((message) => !seen.has(message.id));\n if (fresh.length === 0) return current;\n return [...current, ...fresh].sort((a, b) => a.ts - b.ts);\n}\n","'use client';\n\nimport { useAtom, useAtomValue, useSetAtom } from 'jotai';\nimport { useCallback } from 'react';\nimport {\n chatErrorAtom,\n chatMessagesAtom,\n chatNameAtom,\n chatOpenAtom,\n chatSessionIdAtom,\n chatStatusAtom,\n chatUnreadAtom,\n} from './atoms.js';\nimport type { ChatErrorBody, SendResponseBody } from '../core/types.js';\nimport { sanitizeName } from '../core/name.js';\nimport {\n ensureSessionId,\n getStoredName,\n mergeMessages,\n storeName,\n} from './client.js';\nimport { useChatConfig } from './config.js';\n\n/**\n * Read chat state and act on it. Contains no effects, so it is safe to call\n * from any number of components. The polling lifecycle lives in useChatSync.\n */\nexport function useChatActions() {\n const { basePath, sessionStorageKey, nameStorageKey, maxMessageLength, onEvent } =\n useChatConfig();\n\n const [open, originalSetOpen] = useAtom(chatOpenAtom);\n const messages = useAtomValue(chatMessagesAtom);\n const setMessages = useSetAtom(chatMessagesAtom);\n const unread = useAtomValue(chatUnreadAtom);\n const [status, setStatus] = useAtom(chatStatusAtom);\n const [error, setError] = useAtom(chatErrorAtom);\n const [name, setNameAtomValue] = useAtom(chatNameAtom);\n const setSessionId = useSetAtom(chatSessionIdAtom);\n\n const setOpen = useCallback(\n (v: boolean | ((prev: boolean) => boolean)) => {\n const next = typeof v === 'function' ? v(open) : v;\n originalSetOpen(next);\n if (next === true) onEvent?.({ type: 'chat.open' });\n else if (next === false) onEvent?.({ type: 'chat.close' });\n },\n [open, originalSetOpen, onEvent]\n );\n\n /** Returns false when the name is unusable, so the gate can stay open. */\n const setName = useCallback(\n (raw: string): boolean => {\n const clean = sanitizeName(raw);\n if (!clean) return false;\n storeName(nameStorageKey, clean);\n setNameAtomValue(clean);\n return true;\n },\n [nameStorageKey, setNameAtomValue]\n );\n\n const send = useCallback(\n async (text: string) => {\n const trimmed = text.trim();\n if (trimmed.length === 0 || trimmed.length > maxMessageLength) return;\n\n const sessionId = ensureSessionId(sessionStorageKey);\n setSessionId(sessionId);\n setStatus('sending');\n setError(null);\n\n onEvent?.({ type: 'chat.send.attempt' });\n\n try {\n const response = await fetch(`${basePath}/send`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n sessionId,\n text: trimmed,\n name: getStoredName(nameStorageKey) ?? '',\n honeypot: '',\n }),\n });\n\n if (!response.ok) {\n const body = (await response.json()) as ChatErrorBody;\n setError(body.error);\n setStatus(\n body.code === 'store_unavailable' ? 'unavailable' : 'error'\n );\n onEvent?.({ type: 'chat.send.error', code: body.code });\n return;\n }\n\n const data = (await response.json()) as SendResponseBody;\n // Appended optimistically and deduplicated by id on the next poll. The\n // cursor is deliberately NOT advanced here — doing so would skip an\n // owner reply sitting at a lower index.\n setMessages((current) => mergeMessages(current, [data.message]));\n setStatus('idle');\n onEvent?.({ type: 'chat.send.success' });\n } catch {\n setError('Chat is unavailable right now.');\n setStatus('unavailable');\n onEvent?.({ type: 'chat.send.error', code: 'network' });\n }\n },\n [\n basePath,\n maxMessageLength,\n nameStorageKey,\n onEvent,\n sessionStorageKey,\n setError,\n setMessages,\n setSessionId,\n setStatus,\n ]\n );\n\n return {\n open,\n setOpen,\n messages,\n unread,\n status,\n error,\n name,\n setName,\n send,\n canSend: status !== 'sending',\n };\n}\n","let context: AudioContext | null = null;\n\nfunction getContext(): AudioContext | null {\n if (typeof window === 'undefined') return null;\n const Ctor =\n window.AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext })\n .webkitAudioContext;\n if (!Ctor) return null;\n if (!context) context = new Ctor();\n return context;\n}\n\n/**\n * Two-note arrival chime, synthesized rather than loaded from a file: no asset\n * to host or license, no third-party request, and it works offline.\n *\n * Autoplay policy is satisfied in practice — the visitor must click, name\n * themselves and send before any reply can arrive, so a gesture has occurred.\n *\n * Every failure is swallowed. Audio must never interfere with message delivery.\n */\nexport function playChime(): void {\n try {\n const audio = getContext();\n if (!audio) return;\n if (audio.state === 'suspended') void audio.resume();\n\n const startedAt = audio.currentTime;\n const notes = [\n { frequency: 880, offset: 0 },\n { frequency: 1320, offset: 0.1 },\n ];\n\n for (const { frequency, offset } of notes) {\n const oscillator = audio.createOscillator();\n const gain = audio.createGain();\n oscillator.type = 'sine';\n oscillator.frequency.value = frequency;\n\n const start = startedAt + offset;\n const end = start + 0.09;\n // Ramp from and to zero so neither edge clicks.\n gain.gain.setValueAtTime(0, start);\n gain.gain.linearRampToValueAtTime(0.15, start + 0.01);\n gain.gain.linearRampToValueAtTime(0, end);\n\n oscillator.connect(gain).connect(audio.destination);\n oscillator.start(start);\n oscillator.stop(end + 0.02);\n }\n } catch {\n // Deliberately silent.\n }\n}\n","'use client';\n\nimport { useAtom, useAtomValue, useSetAtom } from 'jotai';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport {\n chatCursorAtom,\n chatMessagesAtom,\n chatNameAtom,\n chatOpenAtom,\n chatSessionIdAtom,\n chatUnreadAtom,\n} from './atoms.js';\nimport type { PollResponseBody } from '../core/types.js';\nimport { playChime } from './chime.js';\nimport { getStoredName, getStoredSessionId, mergeMessages } from './client.js';\nimport { useChatConfig } from './config.js';\n\nconst FAST_INTERVAL_MS = 4000;\nconst SLOW_INTERVAL_MS = 15000;\nconst IDLE_BEFORE_SLOW_MS = 120000;\n\n/**\n * Owns the chat polling lifecycle. Mount this EXACTLY ONCE in the tree.\n *\n * Every effect here is instance-scoped, so mounting it from two components\n * would run two polling loops racing on the same cursor. Components that only\n * need to read state or send a message use useChatActions instead.\n */\nexport function useChatSync() {\n const { basePath, sessionStorageKey, nameStorageKey, chime } = useChatConfig();\n\n const open = useAtomValue(chatOpenAtom);\n const setSessionId = useSetAtom(chatSessionIdAtom);\n const setName = useSetAtom(chatNameAtom);\n const setMessages = useSetAtom(chatMessagesAtom);\n const [cursor, setCursor] = useAtom(chatCursorAtom);\n const setUnread = useSetAtom(chatUnreadAtom);\n\n const [active, setActive] = useState(true);\n const cursorRef = useRef(cursor);\n const lastMessageAtRef = useRef(0);\n const hasHydratedRef = useRef(false);\n\n useEffect(() => {\n cursorRef.current = cursor;\n }, [cursor]);\n\n // Load any existing session id and name client-side only. Reading\n // localStorage during atom initialisation would desync server and client\n // renders.\n useEffect(() => {\n const storedSession = getStoredSessionId(sessionStorageKey);\n if (storedSession) setSessionId(storedSession);\n const storedName = getStoredName(nameStorageKey);\n if (storedName) setName(storedName);\n }, [sessionStorageKey, nameStorageKey, setSessionId, setName]);\n\n /**\n * Tracks whether the visitor is plausibly looking at the page.\n *\n * visibilityState alone is not sufficient: some embeddings report 'hidden'\n * for a tab that genuinely has focus, which silently disabled polling\n * entirely and meant owner replies never arrived. Treating a focused\n * document as active fixes that while still standing down for a truly\n * backgrounded tab. focus/blur are listened to as well because hasFocus()\n * changing does not emit visibilitychange.\n */\n useEffect(() => {\n const recompute = () =>\n setActive(document.visibilityState === 'visible' || document.hasFocus());\n recompute();\n document.addEventListener('visibilitychange', recompute);\n window.addEventListener('focus', recompute);\n window.addEventListener('blur', recompute);\n return () => {\n document.removeEventListener('visibilitychange', recompute);\n window.removeEventListener('focus', recompute);\n window.removeEventListener('blur', recompute);\n };\n }, []);\n\n const poll = useCallback(async () => {\n const sessionId = getStoredSessionId(sessionStorageKey);\n if (!sessionId) return;\n\n try {\n const response = await fetch(\n `${basePath}/poll?sessionId=${encodeURIComponent(sessionId)}&since=${cursorRef.current}`,\n { cache: 'no-store' }\n );\n if (!response.ok) return;\n\n const data = (await response.json()) as PollResponseBody;\n if (data.messages.length === 0) return;\n\n lastMessageAtRef.current = Date.now();\n setMessages((current) => mergeMessages(current, data.messages));\n cursorRef.current = data.cursor;\n setCursor(data.cursor);\n\n const fromOwner = data.messages.filter(\n (message) => message.from === 'owner'\n ).length;\n\n // The first poll of a session replays the whole transcript from cursor 0,\n // so chiming here would fire for replies received on a previous visit.\n const isHydrating = !hasHydratedRef.current;\n hasHydratedRef.current = true;\n\n if (fromOwner > 0 && !isHydrating && chime) playChime();\n if (fromOwner > 0 && !open) setUnread((count) => count + fromOwner);\n } catch {\n // Silent: transient poll failures must not surface as errors.\n }\n }, [basePath, chime, open, sessionStorageKey, setCursor, setMessages, setUnread]);\n\n useEffect(() => {\n // Backgrounded tab: poll nothing at all. Becoming active again re-runs this\n // effect, which polls immediately.\n if (!active) return;\n\n if (!open) {\n // Panel closed: a single fetch, enough to light the unread badge.\n void poll();\n return;\n }\n\n let timer: ReturnType<typeof setTimeout> | null = null;\n let cancelled = false;\n\n const tick = async () => {\n if (cancelled) return;\n await poll();\n if (cancelled) return;\n\n const since = lastMessageAtRef.current || Date.now();\n const delay =\n Date.now() - since > IDLE_BEFORE_SLOW_MS\n ? SLOW_INTERVAL_MS\n : FAST_INTERVAL_MS;\n timer = setTimeout(tick, delay);\n };\n\n lastMessageAtRef.current = Date.now();\n void tick();\n\n return () => {\n cancelled = true;\n if (timer) clearTimeout(timer);\n };\n }, [open, active, poll]);\n\n // Opening the panel means the visitor has seen the replies.\n useEffect(() => {\n if (open) setUnread(0);\n }, [open, setUnread]);\n}\n"]}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import * as jotai from 'jotai';
|
|
3
|
+
import { C as ChatMessage } from '../types-BcoqxSLg.cjs';
|
|
4
|
+
|
|
5
|
+
type ChatClientEvent = {
|
|
6
|
+
type: 'chat.open';
|
|
7
|
+
} | {
|
|
8
|
+
type: 'chat.close';
|
|
9
|
+
} | {
|
|
10
|
+
type: 'chat.send.attempt';
|
|
11
|
+
} | {
|
|
12
|
+
type: 'chat.send.success';
|
|
13
|
+
} | {
|
|
14
|
+
type: 'chat.send.error';
|
|
15
|
+
code: string;
|
|
16
|
+
};
|
|
17
|
+
interface ChatClientConfig {
|
|
18
|
+
/** Base path for the three route files. Default: '/api/chat'. */
|
|
19
|
+
basePath: string;
|
|
20
|
+
/** localStorage key for the session id. Default: 'contact-chat-session'. */
|
|
21
|
+
sessionStorageKey: string;
|
|
22
|
+
/** localStorage key for the visitor's name. Default: 'contact-chat-name'. */
|
|
23
|
+
nameStorageKey: string;
|
|
24
|
+
/** Play a chime when an owner reply arrives. Default: true. */
|
|
25
|
+
chime: boolean;
|
|
26
|
+
/** Client-side max message length; matches the server's `ChatLimits.maxMessageLength`. Default: DEFAULT_CHAT_LIMITS.maxMessageLength. */
|
|
27
|
+
maxMessageLength: number;
|
|
28
|
+
/** Optional analytics callback fired on lifecycle events. */
|
|
29
|
+
onEvent?: (event: ChatClientEvent) => void;
|
|
30
|
+
}
|
|
31
|
+
declare const DEFAULT_CHAT_CLIENT_CONFIG: ChatClientConfig;
|
|
32
|
+
declare function ChatClientProvider({ children, ...overrides }: Partial<ChatClientConfig> & {
|
|
33
|
+
children: React.ReactNode;
|
|
34
|
+
}): React.JSX.Element;
|
|
35
|
+
declare function useChatConfig(): ChatClientConfig;
|
|
36
|
+
|
|
37
|
+
type ChatStatus = 'idle' | 'sending' | 'error' | 'unavailable';
|
|
38
|
+
declare const chatOpenAtom: jotai.PrimitiveAtom<boolean> & {
|
|
39
|
+
init: boolean;
|
|
40
|
+
};
|
|
41
|
+
declare const chatSessionIdAtom: jotai.PrimitiveAtom<string | null> & {
|
|
42
|
+
init: string | null;
|
|
43
|
+
};
|
|
44
|
+
declare const chatNameAtom: jotai.PrimitiveAtom<string | null> & {
|
|
45
|
+
init: string | null;
|
|
46
|
+
};
|
|
47
|
+
declare const chatMessagesAtom: jotai.PrimitiveAtom<ChatMessage[]> & {
|
|
48
|
+
init: ChatMessage[];
|
|
49
|
+
};
|
|
50
|
+
declare const chatCursorAtom: jotai.PrimitiveAtom<number> & {
|
|
51
|
+
init: number;
|
|
52
|
+
};
|
|
53
|
+
declare const chatUnreadAtom: jotai.PrimitiveAtom<number> & {
|
|
54
|
+
init: number;
|
|
55
|
+
};
|
|
56
|
+
declare const chatStatusAtom: jotai.PrimitiveAtom<ChatStatus> & {
|
|
57
|
+
init: ChatStatus;
|
|
58
|
+
};
|
|
59
|
+
declare const chatErrorAtom: jotai.PrimitiveAtom<string | null> & {
|
|
60
|
+
init: string | null;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Read chat state and act on it. Contains no effects, so it is safe to call
|
|
65
|
+
* from any number of components. The polling lifecycle lives in useChatSync.
|
|
66
|
+
*/
|
|
67
|
+
declare function useChatActions(): {
|
|
68
|
+
open: boolean;
|
|
69
|
+
setOpen: (v: boolean | ((prev: boolean) => boolean)) => void;
|
|
70
|
+
messages: ChatMessage[];
|
|
71
|
+
unread: number;
|
|
72
|
+
status: ChatStatus;
|
|
73
|
+
error: string | null;
|
|
74
|
+
name: string | null;
|
|
75
|
+
setName: (raw: string) => boolean;
|
|
76
|
+
send: (text: string) => Promise<void>;
|
|
77
|
+
canSend: boolean;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Owns the chat polling lifecycle. Mount this EXACTLY ONCE in the tree.
|
|
82
|
+
*
|
|
83
|
+
* Every effect here is instance-scoped, so mounting it from two components
|
|
84
|
+
* would run two polling loops racing on the same cursor. Components that only
|
|
85
|
+
* need to read state or send a message use useChatActions instead.
|
|
86
|
+
*/
|
|
87
|
+
declare function useChatSync(): void;
|
|
88
|
+
|
|
89
|
+
declare function getStoredName(nameStorageKey: string): string | null;
|
|
90
|
+
declare function storeName(nameStorageKey: string, name: string): void;
|
|
91
|
+
declare function getStoredSessionId(sessionStorageKey: string): string | null;
|
|
92
|
+
declare function ensureSessionId(sessionStorageKey: string): string;
|
|
93
|
+
/**
|
|
94
|
+
* Merges polled messages into the local list, deduplicating by id.
|
|
95
|
+
*
|
|
96
|
+
* Deduplication is what lets send() append its message optimistically without
|
|
97
|
+
* touching the cursor. Advancing the cursor from the send response would skip
|
|
98
|
+
* anything already sitting at a lower index — an owner reply that arrived
|
|
99
|
+
* between polls would be lost. Instead the cursor advances only from polling,
|
|
100
|
+
* and the visitor's own message is recognised by id when it comes back.
|
|
101
|
+
*
|
|
102
|
+
* Sorted by server timestamp so an owner reply that arrives at a lower index
|
|
103
|
+
* still displays before the visitor's newer message.
|
|
104
|
+
*/
|
|
105
|
+
declare function mergeMessages(current: ChatMessage[], incoming: ChatMessage[]): ChatMessage[];
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Two-note arrival chime, synthesized rather than loaded from a file: no asset
|
|
109
|
+
* to host or license, no third-party request, and it works offline.
|
|
110
|
+
*
|
|
111
|
+
* Autoplay policy is satisfied in practice — the visitor must click, name
|
|
112
|
+
* themselves and send before any reply can arrive, so a gesture has occurred.
|
|
113
|
+
*
|
|
114
|
+
* Every failure is swallowed. Audio must never interfere with message delivery.
|
|
115
|
+
*/
|
|
116
|
+
declare function playChime(): void;
|
|
117
|
+
|
|
118
|
+
export { type ChatClientConfig, type ChatClientEvent, ChatClientProvider, type ChatStatus, DEFAULT_CHAT_CLIENT_CONFIG, chatCursorAtom, chatErrorAtom, chatMessagesAtom, chatNameAtom, chatOpenAtom, chatSessionIdAtom, chatStatusAtom, chatUnreadAtom, ensureSessionId, getStoredName, getStoredSessionId, mergeMessages, playChime, storeName, useChatActions, useChatConfig, useChatSync };
|