diff --git a/README.md b/README.md index b5c24fc9..41617243 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ const reaction = await conversation.messages.addReaction(msgId, { Delete reaction: ```ts -await conversation.messages.removeReaction(msgId, type) +await conversation.messages.removeReaction(reactionId) ``` ### Reaction object @@ -169,7 +169,7 @@ conversation.messages.subscribe(({ type, message }) => { ```ts // Subscribe to all reactions -conversation.reactions.subscribe(({ type, reaction }) => { +conversation.messages.subscribeReactions(({ type, reaction }) => { switch (type) { case 'reaction.added': console.log(reaction); @@ -292,8 +292,7 @@ conversation.reactions.add(reactionType) Remove reaction ```ts -conversation.reactions.delete(reaction) -conversation.reactions.delete(reactionType) +conversation.reactions.delete(reactionId) ``` ## Typing indicator diff --git a/__mocks__/ably/promises/index.ts b/__mocks__/ably/promises/index.ts index a18358df..ce53c134 100644 --- a/__mocks__/ably/promises/index.ts +++ b/__mocks__/ably/promises/index.ts @@ -48,6 +48,7 @@ class MockRealtime { }; public auth: { clientId: string; + requestToken(): void; }; public connection: { id?: string; @@ -65,6 +66,7 @@ class MockRealtime { }; this.auth = { clientId: MOCK_CLIENT_ID, + requestToken: () => {}, }; this.connection = { id: '1', diff --git a/demo/api/conversations/.env.example b/demo/api/conversations/.env.example deleted file mode 100644 index 27c61dbc..00000000 --- a/demo/api/conversations/.env.example +++ /dev/null @@ -1 +0,0 @@ -ABLY_API_KEY= diff --git a/demo/api/conversations/controllers/conversationsController.ts b/demo/api/conversations/controllers/conversationsController.ts new file mode 100644 index 00000000..b876ec17 --- /dev/null +++ b/demo/api/conversations/controllers/conversationsController.ts @@ -0,0 +1,12 @@ +import { Request, Response } from 'express'; +import { createConversation, getConversation } from '../inMemoryDb'; + +export const handleCreateConversation = (req: Request, res: Response) => { + const conversationId = req.params.conversationId; + res.json(createConversation(conversationId)); +}; + +export const handleGetConversation = (req: Request, res: Response) => { + const conversationId = req.params.conversationId; + res.json(getConversation(conversationId)); +}; diff --git a/demo/api/conversations/controllers/messagesController.ts b/demo/api/conversations/controllers/messagesController.ts new file mode 100644 index 00000000..c063280a --- /dev/null +++ b/demo/api/conversations/controllers/messagesController.ts @@ -0,0 +1,62 @@ +import * as Ably from 'ably/promises'; +import { Request, Response } from 'express'; +import { createMessage, deleteMessage, editMessage, findMessages } from '../inMemoryDb'; + +export const handleCreateMessage = (req: Request, res: Response) => { + const conversationId = req.params.conversationId; + const ablyToken = req.headers.authorization.split(' ')[1]; + + const message = createMessage({ + ...JSON.parse(req.body), + client_id: req.headers['ably-clientid'] as string, + conversation_id: conversationId, + }); + + const client = new Ably.Rest(ablyToken); + + client.channels.get(`conversations:${conversationId}`).publish('message.created', message); + + res.json({ id: message.id }); + + res.status(201).end(); +}; + +export const handleQueryMessages = (req: Request, res: Response) => { + const conversationId = req.params.conversationId; + res.json(findMessages(conversationId, req.headers['ably-clientid'] as string)); +}; + +export const handleEditMessages = (req: Request, res: Response) => { + const conversationId = req.params.conversationId; + const ablyToken = req.headers.authorization.split(' ')[1]; + + const message = editMessage({ + id: req.params.messageId, + conversation_id: conversationId, + ...JSON.parse(req.body), + }); + + const client = new Ably.Rest(ablyToken); + + client.channels.get(`conversations:${conversationId}`).publish('message.updated', message); + + res.json({ id: message.id }); + + res.status(201).end(); +}; + +export const handleDeleteMessages = (req: Request, res: Response) => { + const conversationId = req.params.conversationId; + const ablyToken = req.headers.authorization.split(' ')[1]; + + const message = deleteMessage({ + id: req.params.messageId, + conversation_id: conversationId, + }); + + const client = new Ably.Rest(ablyToken); + + client.channels.get(`conversations:${conversationId}`).publish('message.deleted', message); + + res.status(201).end(); +}; diff --git a/demo/api/conversations/controllers/reactionsController.ts b/demo/api/conversations/controllers/reactionsController.ts new file mode 100644 index 00000000..f2ca6b8c --- /dev/null +++ b/demo/api/conversations/controllers/reactionsController.ts @@ -0,0 +1,34 @@ +import { Request, Response } from 'express'; +import * as Ably from 'ably/promises'; +import { addReaction, deleteReaction } from '../inMemoryDb'; + +export const handleAddReaction = (req: Request, res: Response) => { + const conversationId = req.params.conversationId; + const ablyToken = req.headers.authorization.split(' ')[1]; + + const reaction = addReaction({ + message_id: req.params.messageId, + conversation_id: conversationId, + client_id: req.headers['ably-clientid'] as string, + ...JSON.parse(req.body), + }); + + const client = new Ably.Rest(ablyToken); + + client.channels.get(`conversations:${conversationId}`).publish('reaction.added', reaction); + + res.status(201).end(); +}; + +export const handleDeleteReaction = (req: Request, res: Response) => { + const reactionId = req.params.reactionId; + const ablyToken = req.headers.authorization.split(' ')[1]; + + const reaction = deleteReaction(reactionId); + + const client = new Ably.Rest(ablyToken); + + client.channels.get(`conversations:${reaction.conversation_id}`).publish('reaction.deleted', reaction); + + res.status(201).end(); +}; diff --git a/demo/api/conversations/inMemoryDb.ts b/demo/api/conversations/inMemoryDb.ts new file mode 100644 index 00000000..6a8b9032 --- /dev/null +++ b/demo/api/conversations/inMemoryDb.ts @@ -0,0 +1,138 @@ +import { ulid } from 'ulidx'; + +export interface Conversation { + id: string; + application_id: string; + ttl: number | null; + created_at: number; +} + +export interface Message { + id: string; + client_id: string; + conversation_id: string; + content: string; + reactions: { + counts: Record; + latest: Reaction[]; + mine: Reaction[]; + }; + created_at: number; + updated_at: number | null; + deleted_at: number | null; +} + +export interface Reaction { + id: string; + message_id: string; + conversation_id: string; + type: string; + client_id: string; + updated_at: number | null; + deleted_at: number | null; +} + +const conversations: Conversation[] = []; +const conversationIdToMessages: Record = {}; +const reactions: Reaction[] = []; + +export const createConversation = (id: string): Conversation => { + const existing = conversations.find((conv) => conv.id === id); + if (existing) return existing; + const conversation = { + id, + application_id: 'demo', + ttl: null, + created_at: Date.now(), + }; + conversationIdToMessages[id] = []; + conversations.push(conversation); + return conversation; +}; + +createConversation('conversation1'); + +export const getConversation = (id: string): Conversation => { + return conversations.find((conv) => conv.id === id); +}; + +export const findMessages = (conversationId: string, clientId: string) => + enrichMessagesWithReactions(conversationIdToMessages[conversationId], clientId); + +export const createMessage = (message: Pick) => { + const created: Message = { + ...message, + id: ulid(), + reactions: { + counts: {}, + latest: [], + mine: [], + }, + created_at: Date.now(), + updated_at: null, + deleted_at: null, + }; + conversationIdToMessages[created.conversation_id].push(created); + return created; +}; + +export const editMessage = (message: Pick) => { + const edited = conversationIdToMessages[message.conversation_id].find(({ id }) => message.id === id); + edited.content = message.content; + return edited; +}; + +export const deleteMessage = (message: Pick) => { + const deletedIndex = conversationIdToMessages[message.conversation_id].findIndex(({ id }) => message.id === id); + const deleted = conversationIdToMessages[message.conversation_id][deletedIndex]; + conversationIdToMessages[message.conversation_id].splice(deletedIndex, 1); + return deleted; +}; + +export const addReaction = ( + reaction: Pick, +) => { + const created: Reaction = { + ...reaction, + id: ulid(), + updated_at: null, + deleted_at: null, + }; + reactions.push(created); + return created; +}; + +export const deleteReaction = (reactionId: string) => { + const deletedIndex = reactions.findIndex((reaction) => reaction.id === reactionId); + const deleted = reactions[deletedIndex]; + reactions.splice(deletedIndex, 1); + return deleted; +}; + +const enrichMessageWithReactions = (message: Message, clientId: string): Message => { + const messageReactions = reactions.filter((reaction) => reaction.message_id === message.id); + const mine = messageReactions.filter((reaction) => reaction.client_id === clientId); + const counts = messageReactions.reduce( + (acc, reaction) => { + if (acc[reaction.type]) { + acc[reaction.type]++; + } else { + acc[reaction.type] = 1; + } + return acc; + }, + {} as Record, + ); + return { + ...message, + reactions: { + counts, + latest: messageReactions, + mine, + }, + }; +}; + +const enrichMessagesWithReactions = (messages: Message[], clientId: string) => { + return messages.map((message) => enrichMessageWithReactions(message, clientId)); +}; diff --git a/demo/api/conversations/index.ts b/demo/api/conversations/index.ts index 2f9116f0..4aa3287f 100644 --- a/demo/api/conversations/index.ts +++ b/demo/api/conversations/index.ts @@ -1,68 +1,12 @@ import * as dotenv from 'dotenv'; -import * as Ably from 'ably/promises'; -import { HandlerEvent } from '@netlify/functions'; -import { ulid } from 'ulidx'; +import express from 'express'; +import serverless from 'serverless-http'; +import { router } from './routes'; dotenv.config(); -const messages = []; +const api = express(); -export async function handler(event: HandlerEvent) { - if (!process.env.ABLY_API_KEY) { - console.error(` -Missing ABLY_API_KEY environment variable. -If you're running locally, please ensure you have a ./.env file with a value for ABLY_API_KEY=your-key. -If you're running in Netlify, make sure you've configured env variable ABLY_API_KEY. +api.use('/api/conversations/v1', router); -Please see README.md for more details on configuring your Ably API Key.`); - - return { - statusCode: 500, - headers: { 'content-type': 'application/json' }, - body: JSON.stringify('ABLY_API_KEY is not set'), - }; - } - - if (/\/api\/conversations\/v1\/conversations\/(\w+)\/messages/.test(event.path) && event.httpMethod === 'POST') { - const conversationId = /\/api\/conversations\/v1\/conversations\/(\w+)\/messages/.exec(event.path)[1]; - const message = { - id: ulid(), - ...JSON.parse(event.body), - client_id: event.headers['ably-clientid'], - conversation_id: conversationId, - reactions: { - counts: {}, - latest: [], - mine: [], - }, - created_at: Date.now(), - updated_at: null, - deleted_at: null, - }; - messages.push(message); - - const client = new Ably.Rest(process.env.ABLY_API_KEY); - - client.channels.get(`conversations:${conversationId}`).publish('message.created', message); - - return { - statusCode: 201, - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ id: message.id }), - }; - } - - const getMessagesRegEx = /\/api\/conversations\/v1\/conversations\/(\w+)\/messages/; - if (getMessagesRegEx.test(event.path) && event.httpMethod === 'GET') { - return { - statusCode: 200, - headers: { 'content-type': 'application/json' }, - body: JSON.stringify(messages), - }; - } - - return { - statusCode: 404, - body: 'Not Found', - }; -} +export const handler = serverless(api); diff --git a/demo/api/conversations/package-lock.json b/demo/api/conversations/package-lock.json index 58a0c743..1595e9d3 100644 --- a/demo/api/conversations/package-lock.json +++ b/demo/api/conversations/package-lock.json @@ -10,10 +10,13 @@ "license": "MIT", "dependencies": { "@netlify/functions": "^1.4.0", + "@types/express": "^4.17.21", "@types/node": "^18.3.0", "ably": "^1.2.36", "dotenv": "^16.0.3", + "express": "^4.18.2", "nanoid": "^5.0.4", + "serverless-http": "^3.2.0", "typescript": "^4.9.5", "ulidx": "^2.2.1" } @@ -60,6 +63,15 @@ "node": ">=10" } }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, "node_modules/@types/cacheable-request": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz", @@ -71,11 +83,46 @@ "@types/responselike": "*" } }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.41", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", + "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, "node_modules/@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", @@ -84,12 +131,27 @@ "@types/node": "*" } }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, "node_modules/@types/node": { "version": "18.13.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.13.0.tgz", "integrity": "sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==", "license": "MIT" }, + "node_modules/@types/qs": { + "version": "6.9.11", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", + "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + }, "node_modules/@types/responselike": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", @@ -98,6 +160,25 @@ "@types/node": "*" } }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, "node_modules/ably": { "version": "1.2.36", "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.36.tgz", @@ -112,6 +193,23 @@ "node": ">=5.10.x" } }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, "node_modules/async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", @@ -125,6 +223,29 @@ "node": ">= 0.4" } }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/bops": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/bops/-/bops-1.0.1.tgz", @@ -134,6 +255,14 @@ "to-utf8": "0.0.1" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -159,6 +288,19 @@ "node": ">=8" } }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/clone-response": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", @@ -170,6 +312,46 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -203,6 +385,36 @@ "node": ">=10" } }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/dotenv": { "version": "16.0.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", @@ -212,6 +424,19 @@ "node": ">=12" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -220,6 +445,115 @@ "once": "^1.4.0" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -234,6 +568,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/got": { "version": "11.8.5", "resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz", @@ -258,11 +603,70 @@ "url": "https://github.com/sindresorhus/got?sponsor=1" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", @@ -275,6 +679,30 @@ "node": ">=10.19.0" } }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -306,6 +734,57 @@ "node": ">=8" } }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -314,6 +793,11 @@ "node": ">=4" } }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/nanoid": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.4.tgz", @@ -331,6 +815,14 @@ "node": "^18 || >=20" } }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", @@ -342,6 +834,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -358,6 +869,31 @@ "node": ">=8" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -367,6 +903,20 @@ "once": "^1.3.1" } }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", @@ -378,6 +928,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", @@ -394,11 +966,145 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serverless-http": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/serverless-http/-/serverless-http-3.2.0.tgz", + "integrity": "sha512-QvSyZXljRLIGqwcJ4xsKJXwkZnAVkse1OajepxfjkBXV0BMvRS5R546Z4kCBI8IygDzkQY0foNPC/rnipaE9pQ==", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/to-utf8": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/to-utf8/-/to-utf8-0.0.1.tgz", "integrity": "sha512-zks18/TWT1iHO3v0vFp5qLKOG27m67ycq/Y7a7cTiRuUNlc4gf3HGnkRgMv0NyhnfTamtkYBJl+YeD1/j07gBQ==" }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", @@ -423,6 +1129,30 @@ "node": ">=16" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -467,6 +1197,15 @@ "defer-to-connect": "^2.0.0" } }, + "@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, "@types/cacheable-request": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz", @@ -478,11 +1217,46 @@ "@types/responselike": "*" } }, + "@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "requires": { + "@types/node": "*" + } + }, + "@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.41", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", + "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "@types/http-cache-semantics": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" }, + "@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, "@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", @@ -491,11 +1265,26 @@ "@types/node": "*" } }, + "@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, "@types/node": { "version": "18.13.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.13.0.tgz", "integrity": "sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==" }, + "@types/qs": { + "version": "6.9.11", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", + "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==" + }, + "@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + }, "@types/responselike": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", @@ -504,6 +1293,25 @@ "@types/node": "*" } }, + "@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "@types/serve-static": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "requires": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, "ably": { "version": "1.2.36", "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.36.tgz", @@ -514,6 +1322,20 @@ "ws": "^5.1" } }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, "async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", @@ -524,6 +1346,25 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.0.2.tgz", "integrity": "sha512-ZXBDPMt/v/8fsIqn+Z5VwrhdR6jVka0bYobHdGia0Nxi7BJ9i/Uvml3AocHIBtIIBhZjBw5MR0aR4ROs/8+SNg==" }, + "body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, "bops": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/bops/-/bops-1.0.1.tgz", @@ -533,6 +1374,11 @@ "to-utf8": "0.0.1" } }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, "cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -552,6 +1398,16 @@ "responselike": "^2.0.0" } }, + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, "clone-response": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", @@ -560,6 +1416,37 @@ "mimic-response": "^1.0.0" } }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, "decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -580,11 +1467,41 @@ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==" }, + "define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "requires": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, "dotenv": { "version": "16.0.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==" }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -593,6 +1510,94 @@ "once": "^1.4.0" } }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "requires": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, "get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -601,6 +1606,14 @@ "pump": "^3.0.0" } }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, "got": { "version": "11.8.5", "resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz", @@ -619,11 +1632,49 @@ "responselike": "^2.0.0" } }, + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "requires": { + "get-intrinsic": "^1.2.2" + } + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "requires": { + "function-bind": "^1.1.2" + } + }, "http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, "http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", @@ -633,6 +1684,24 @@ "resolve-alpn": "^1.0.0" } }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, "is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -661,21 +1730,77 @@ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "nanoid": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.4.tgz", "integrity": "sha512-vAjmBf13gsmhXSgBrtIclinISzFFy22WwCYoyilZlsrRXNIHSwgFQ1bEdjRwMT3aoadeIF6HMuDRlOxzfXV8ig==" }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, "normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" }, + "object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -689,6 +1814,25 @@ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==" }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -698,11 +1842,35 @@ "once": "^1.3.1" } }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } + }, "quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, "resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", @@ -716,11 +1884,109 @@ "lowercase-keys": "^2.0.0" } }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "serverless-http": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/serverless-http/-/serverless-http-3.2.0.tgz", + "integrity": "sha512-QvSyZXljRLIGqwcJ4xsKJXwkZnAVkse1OajepxfjkBXV0BMvRS5R546Z4kCBI8IygDzkQY0foNPC/rnipaE9pQ==" + }, + "set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "requires": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, "to-utf8": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/to-utf8/-/to-utf8-0.0.1.tgz", "integrity": "sha512-zks18/TWT1iHO3v0vFp5qLKOG27m67ycq/Y7a7cTiRuUNlc4gf3HGnkRgMv0NyhnfTamtkYBJl+YeD1/j07gBQ==" }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, "typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", @@ -734,6 +2000,21 @@ "layerr": "^2.0.1" } }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/demo/api/conversations/package.json b/demo/api/conversations/package.json index 49682543..31f3dc05 100644 --- a/demo/api/conversations/package.json +++ b/demo/api/conversations/package.json @@ -9,10 +9,13 @@ "license": "MIT", "dependencies": { "@netlify/functions": "^1.4.0", + "@types/express": "^4.17.21", "@types/node": "^18.3.0", "ably": "^1.2.36", "dotenv": "^16.0.3", + "express": "^4.18.2", "nanoid": "^5.0.4", + "serverless-http": "^3.2.0", "typescript": "^4.9.5", "ulidx": "^2.2.1" } diff --git a/demo/api/conversations/routes.ts b/demo/api/conversations/routes.ts new file mode 100644 index 00000000..44ca80fe --- /dev/null +++ b/demo/api/conversations/routes.ts @@ -0,0 +1,35 @@ +import { Router } from 'express'; +import { + handleCreateMessage, + handleDeleteMessages, + handleEditMessages, + handleQueryMessages, +} from './controllers/messagesController'; +import { handleCreateConversation, handleGetConversation } from './controllers/conversationsController'; +import { handleAddReaction, handleDeleteReaction } from './controllers/reactionsController'; + +const router = Router(); + +// Conversations + +router.post('/conversations/:conversationId', handleCreateConversation); + +router.get('/conversations/:conversationId', handleGetConversation); + +// Messages + +router.post('/conversations/:conversationId/messages', handleCreateMessage); + +router.get('/conversations/:conversationId/messages', handleQueryMessages); + +router.post('/conversations/:conversationId/messages/:messageId', handleEditMessages); + +router.delete('/conversations/:conversationId/messages/:messageId', handleDeleteMessages); + +// Reactions + +router.post('/conversations/:conversationId/messages/:messageId/reactions', handleAddReaction); + +router.delete('/reactions/:reactionId', handleDeleteReaction); + +export { router }; diff --git a/demo/netlify.toml b/demo/netlify.toml index ea3c9b53..2b67e431 100644 --- a/demo/netlify.toml +++ b/demo/netlify.toml @@ -4,6 +4,8 @@ [functions] directory = "api" + external_node_modules = ["express"] + node_bundler = "esbuild" [[redirects]] from = "/api/*" diff --git a/demo/src/components/Message/Message.tsx b/demo/src/components/Message/Message.tsx index 7e766120..7c697068 100644 --- a/demo/src/components/Message/Message.tsx +++ b/demo/src/components/Message/Message.tsx @@ -1,29 +1,36 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, useCallback } from 'react'; import clsx from 'clsx'; interface MessageProps { + id: string; self?: boolean; children?: ReactNode | undefined; + onMessageClick?(id: string): void; } -export const Message: React.FC = ({ self = false, children }) => { +export const Message: React.FC = ({ id, self = false, children, onMessageClick }) => { + const handleMessageClick = useCallback(() => { + onMessageClick?.(id); + }, [id, onMessageClick]); + return ( -
+
-
- - {children} - +
+ {children}
diff --git a/demo/src/components/MessageInput/MessageInput.tsx b/demo/src/components/MessageInput/MessageInput.tsx index c847d2d2..c2d2d41f 100644 --- a/demo/src/components/MessageInput/MessageInput.tsx +++ b/demo/src/components/MessageInput/MessageInput.tsx @@ -1,20 +1,22 @@ -import { useState, ChangeEventHandler, FormEventHandler } from 'react'; +import { FC, ChangeEventHandler, FormEventHandler } from 'react'; interface MessageInputProps { + disabled: boolean; + value: string; + onValueChange(text: string): void; onSend(text: string): void; } -export const MessageInput: React.FC = ({ onSend }) => { - const [value, setValue] = useState(''); +export const MessageInput: FC = ({ value, disabled, onValueChange, onSend }) => { const handleValueChange: ChangeEventHandler = ({ target }) => { - setValue(target.value); + onValueChange(target.value); }; const handleFormSubmit: FormEventHandler = (event) => { event.preventDefault(); event.stopPropagation(); onSend(value); - setValue(''); + onValueChange(''); }; return ( @@ -26,11 +28,13 @@ export const MessageInput: React.FC = ({ onSend }) => { type="text" value={value} onChange={handleValueChange} + disabled={disabled} placeholder="Type.." className="w-full focus:outline-none focus:placeholder-gray-400 text-gray-600 placeholder-gray-600 pl-2 bg-gray-200 rounded-md py-1" />
+ )} + {!selectedMessage.reactions.mine.length && ( + + )} + {!!selectedMessage.reactions.mine.length && ( + + )} +
+ )} + {loading &&
loading...
} + {!loading && ( +
+ {messages.map((msg) => ( + +
+
{msg.content}
+ {!!msg.reactions.counts.like && ( +
{msg.reactions.counts.like} ❤️
+ )} +
+
+ ))} +
+ )}
- +
diff --git a/demo/src/containers/ConversationContext/ConversationProvider.tsx b/demo/src/containers/ConversationContext/ConversationProvider.tsx index 773beccb..55903314 100644 --- a/demo/src/containers/ConversationContext/ConversationProvider.tsx +++ b/demo/src/containers/ConversationContext/ConversationProvider.tsx @@ -15,5 +15,6 @@ export const ConversationProvider: FC = ({ client, co }), [client, conversationId], ); + return {children}; }; diff --git a/demo/src/hooks/useConversation.ts b/demo/src/hooks/useConversation.ts new file mode 100644 index 00000000..29c10724 --- /dev/null +++ b/demo/src/hooks/useConversation.ts @@ -0,0 +1,13 @@ +import { useContext } from 'react'; +import { ConversationContext } from '../containers/ConversationContext'; + +export const useConversation = () => { + const context = useContext(ConversationContext); + + if (!context) throw Error('Client is not setup!'); + + return { + conversation: context.conversation, + clientId: context.client.clientId, + }; +}; diff --git a/demo/src/hooks/useMessages.ts b/demo/src/hooks/useMessages.ts index a57c4bc0..45cab8a6 100644 --- a/demo/src/hooks/useMessages.ts +++ b/demo/src/hooks/useMessages.ts @@ -1,46 +1,140 @@ -import { Message, MessageEvents, type MessageListener } from '@ably-labs/chat'; -import { useCallback, useContext, useEffect, useState } from 'react'; -import { ConversationContext } from '../containers/ConversationContext'; +import { Message, MessageEvents, ReactionEvents, type MessageListener, type ReactionListener } from '@ably-labs/chat'; +import { useCallback, useEffect, useState } from 'react'; +import { useConversation } from './useConversation'; export const useMessages = () => { const [messages, setMessages] = useState([]); - const context = useContext(ConversationContext); + const [loading, setLoading] = useState(false); + const { clientId, conversation } = useConversation(); const sendMessage = useCallback( (text: string) => { - if (!context?.conversation) throw Error('Client is not setup!'); - context.conversation.messages.send(text); + conversation.messages.send(text); }, - [context?.conversation], + [conversation], ); - useEffect(() => { - if (!context) throw Error('Client is not setup!'); + const editMessage = useCallback( + (messageId: string, text: string) => { + conversation.messages.edit(messageId, text); + }, + [conversation], + ); + + const deleteMessage = useCallback( + (messageId: string) => { + conversation.messages.delete(messageId); + }, + [conversation], + ); - const handler: MessageListener = ({ message }) => { + const addReaction = useCallback( + (messageId: string, type: string) => { + conversation.messages.addReaction(messageId, type); + }, + [conversation], + ); + + const removeReaction = useCallback( + (reactionId: string) => { + conversation.messages.removeReaction(reactionId); + }, + [conversation], + ); + + useEffect(() => { + setLoading(true); + const handleAdd: MessageListener = ({ message }) => { setMessages((prevMessage) => [...prevMessage, message]); }; - context.conversation.messages.subscribe(MessageEvents.created, handler); + const handleUpdate: MessageListener = ({ message: updated }) => { + setMessages((prevMessage) => + prevMessage.map((message) => + message.id !== updated.id ? message : { ...updated, reactions: message.reactions }, + ), + ); + }; + const handleDelete: MessageListener = ({ message }) => { + setMessages((prevMessage) => prevMessage.filter(({ id }) => id !== message.id)); + }; + const handleReactionAdd: ReactionListener = ({ reaction }) => { + setMessages((prevMessage) => + prevMessage.map((message) => + message.id !== reaction.message_id + ? message + : { + ...message, + reactions: { + mine: + reaction.client_id === clientId ? [...message.reactions.mine, reaction] : message.reactions.mine, + latest: [...message.reactions.latest, reaction], + counts: { + ...message.reactions.counts, + [reaction.type]: (message.reactions.counts[reaction.type] ?? 0) + 1, + }, + }, + }, + ), + ); + }; + const handleReactionDelete: ReactionListener = ({ reaction }) => { + setMessages((prevMessage) => + prevMessage.map((message) => + message.id !== reaction.message_id + ? message + : { + ...message, + reactions: { + mine: + reaction.client_id === clientId + ? message.reactions.mine.filter(({ id }) => id !== reaction.id) + : message.reactions.mine, + latest: message.reactions.latest.filter(({ id }) => id !== reaction.id), + counts: { + ...message.reactions.counts, + [reaction.type]: message.reactions.counts[reaction.type] - 1, + }, + }, + }, + ), + ); + }; + + conversation.messages.subscribe(MessageEvents.created, handleAdd); + conversation.messages.subscribe(MessageEvents.updated, handleUpdate); + conversation.messages.subscribe(MessageEvents.deleted, handleDelete); + conversation.messages.subscribeReactions(ReactionEvents.added, handleReactionAdd); + conversation.messages.subscribeReactions(ReactionEvents.deleted, handleReactionDelete); let mounted = true; const initMessages = async () => { - const lastMessages = await context.conversation.messages.query({ limit: 10 }); - if (mounted) setMessages((prevMessages) => [...lastMessages, ...prevMessages]); + const lastMessages = await conversation.messages.query({ limit: 10 }); + if (mounted) { + setLoading(false); + setMessages((prevMessages) => [...lastMessages, ...prevMessages]); + } }; setMessages([]); initMessages(); return () => { mounted = false; - context.conversation.messages.unsubscribe(MessageEvents.created, handler); + conversation.messages.unsubscribe(MessageEvents.created, handleAdd); + conversation.messages.unsubscribe(MessageEvents.updated, handleUpdate); + conversation.messages.unsubscribe(MessageEvents.deleted, handleDelete); + conversation.messages.unsubscribeReactions(ReactionEvents.added, handleReactionAdd); + conversation.messages.unsubscribeReactions(ReactionEvents.deleted, handleReactionDelete); }; - }, [context]); - - if (!context) throw Error('Client is not setup!'); + }, [clientId, conversation]); return { + loading, messages, - clientId: context.client.clientId, + editMessage, sendMessage, + deleteMessage, + addReaction, + removeReaction, + clientId, }; }; diff --git a/src/Chat.test.ts b/src/Chat.test.ts deleted file mode 100644 index fd87ddb2..00000000 --- a/src/Chat.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { it, describe, expect } from 'vitest'; - -describe('Chat SDK', () => { - it('dummy test', () => { - expect(true).toBe(true); - }); -}); diff --git a/src/ChatApi.ts b/src/ChatApi.ts index 5931d4d9..b2856f5d 100644 --- a/src/ChatApi.ts +++ b/src/ChatApi.ts @@ -1,5 +1,7 @@ import { Conversation, Message } from './entities.js'; -import { ErrorInfo } from 'ably'; +import { ErrorInfo, Types } from 'ably'; +import AuthPromise = Types.AuthPromise; +import TokenDetails = Types.TokenDetails; export interface CreateConversationRequest { ttl: number; @@ -24,41 +26,31 @@ export interface UpdateMessageResponse { id: string; } +export interface AddReactionResponse { + id: string; +} + /** * Chat SDK Backend */ export class ChatApi { private readonly baseUrl = '/api/conversations'; + private readonly auth: AuthPromise; + private tokenDetails: TokenDetails | undefined; - private readonly clientId: string; - - constructor(clientId: string) { - this.clientId = clientId; + constructor(auth: AuthPromise) { + this.auth = auth; } async getConversation(conversationId: string): Promise { - const response = await fetch(`${this.baseUrl}/v1/conversations/${conversationId}`, { - headers: { - 'ably-clientId': this.clientId, - }, - }); - if (!response.ok) throw new ErrorInfo(response.statusText, response.status, 4000); - return response.json(); + return this.makeAuthorisedRequest(`v1/conversations/${conversationId}`, 'GET'); } async createConversation( conversationId: string, body?: CreateConversationRequest, ): Promise { - const response = await fetch(`${this.baseUrl}/v1/conversations/${conversationId}`, { - method: 'POST', - headers: { - 'ably-clientId': this.clientId, - }, - body: body ? JSON.stringify(body) : undefined, - }); - if (!response.ok) throw new ErrorInfo(response.statusText, response.status, 4000); - return response.json(); + return this.makeAuthorisedRequest(`v1/conversations/${conversationId}`, 'POST', body); } async getMessages(conversationId: string, params: GetMessagesQueryParams): Promise { @@ -66,37 +58,57 @@ export class ChatApi { ...params, limit: params.limit.toString(), }).toString(); + return this.makeAuthorisedRequest(`v1/conversations/${conversationId}/messages?${queryString}`, 'GET'); + } - const response = await fetch(`${this.baseUrl}/v1/conversations/${conversationId}/messages?${queryString}`, { - headers: { - 'ably-clientId': this.clientId, - }, + async sendMessage(conversationId: string, text: string): Promise { + return this.makeAuthorisedRequest(`v1/conversations/${conversationId}/messages`, 'POST', { content: text }); + } + + async editMessage(conversationId: string, messageId: string, text: string): Promise { + return this.makeAuthorisedRequest(`v1/conversations/${conversationId}/messages/${messageId}`, 'POST', { + content: text, }); - if (!response.ok) throw new ErrorInfo(response.statusText, response.status, 4000); - return response.json(); } - async sendMessage(conversationId: string, text: string): Promise { - const response = await fetch(`${this.baseUrl}/v1/conversations/${conversationId}/messages`, { - method: 'POST', - headers: { - 'ably-clientId': this.clientId, - }, - body: JSON.stringify({ content: text }), + async deleteMessage(conversationId: string, messageId: string): Promise { + return this.makeAuthorisedRequest(`v1/conversations/${conversationId}/messages/${messageId}`, 'DELETE'); + } + + async addMessageReaction(conversationId: string, messageId: string, type: string): Promise { + return this.makeAuthorisedRequest(`v1/conversations/${conversationId}/messages/${messageId}/reactions`, 'POST', { + type, }); - if (!response.ok) throw new ErrorInfo(response.statusText, response.status, 4000); - return response.json(); } - async editMessage(conversationId: string, messageId: string, text: string): Promise { - const response = await fetch(`${this.baseUrl}/v1/conversations/${conversationId}/messages/${messageId}`, { - method: 'POST', + async deleteMessageReaction(reactionId: string): Promise { + return this.makeAuthorisedRequest(`v1/reactions/${reactionId}`, 'DELETE'); + } + + private async makeAuthorisedRequest( + url: string, + method: 'POST' | 'GET' | ' PUT' | 'DELETE', + body?: REQ, + ): Promise { + const tokenDetails = await this.getTokenDetails(); + const response = await fetch(`${this.baseUrl}/${url}`, { + method, headers: { - 'ably-clientId': this.clientId, + 'ably-clientId': tokenDetails.clientId as string, + authorization: `Bearer ${tokenDetails.token}`, }, - body: JSON.stringify({ content: text }), + body: body ? JSON.stringify(body) : undefined, }); if (!response.ok) throw new ErrorInfo(response.statusText, response.status, 4000); return response.json(); } + + private async getTokenDetails(): Promise { + if (this.tokenDetails && this.tokenDetails.expires > Date.now()) { + return this.tokenDetails; + } + const newTokenDetails = await this.auth.requestToken(); + this.tokenDetails = newTokenDetails; + return newTokenDetails; + } } diff --git a/src/Conversation.ts b/src/Conversation.ts index 10a51c57..898b172d 100644 --- a/src/Conversation.ts +++ b/src/Conversation.ts @@ -21,6 +21,9 @@ export class Conversation { await this.chatApi.createConversation(this.conversationId); } + get members() { + return this.channel.presence; + async delete() { await this.chatApi.deleteConversation(this.conversationId); } diff --git a/src/Conversations.ts b/src/Conversations.ts index ca73b410..9332edb5 100644 --- a/src/Conversations.ts +++ b/src/Conversations.ts @@ -10,7 +10,7 @@ export class Conversations { constructor(realtime: Realtime) { this.realtime = realtime; - this.chatApi = new ChatApi((realtime as any).options.clientId); + this.chatApi = new ChatApi(realtime.auth); } get(conversationId: string): Conversation { diff --git a/src/Messages.test.ts b/src/Messages.test.ts index 233b33fc..d1be076a 100644 --- a/src/Messages.test.ts +++ b/src/Messages.test.ts @@ -14,7 +14,15 @@ vi.mock('ably/promises'); describe('Messages', () => { beforeEach((context) => { context.realtime = new Realtime({ clientId: 'clientId', key: 'key' }); - context.chatApi = new ChatApi('clientId'); + context.chatApi = new ChatApi(context.realtime.auth); + + vi.spyOn(context.realtime.auth, 'requestToken').mockResolvedValue({ + clientId: 'clientId', + token: 'token', + capability: '', + expires: -1, + issued: -1, + }); const channel = context.realtime.channels.get('conversationId'); vi.spyOn(channel, 'subscribe').mockImplementation( @@ -128,4 +136,170 @@ describe('Messages', () => { }); }); }); + + describe('deleting message', () => { + it('should delete message by message object', async (context) => { + const { chatApi, realtime } = context; + + vi.spyOn(chatApi, 'deleteMessage').mockImplementation(async (conversationId, messageId) => { + context.emulateBackendPublish({ + clientId: 'clientId', + data: { + id: messageId, + client_id: 'clientId', + content: 'text', + deleted_at: 1111, + }, + }); + }); + + const conversation = new Conversation('conversationId', realtime, chatApi); + const message = await conversation.messages.delete({ id: 'messageId', content: 'text' } as any); + + expect(message).toContain({ + id: 'messageId', + client_id: 'clientId', + content: 'text', + deleted_at: 1111, + }); + }); + + it('should delete message by messageId', async (context) => { + const { chatApi, realtime } = context; + vi.spyOn(chatApi, 'deleteMessage').mockResolvedValue(undefined); + + const conversation = new Conversation('conversationId', realtime, chatApi); + const messagePromise = conversation.messages.delete('messageId'); + + context.emulateBackendPublish({ + clientId: 'clientId', + data: { + id: 'messageId', + client_id: 'clientId', + content: 'text', + deleted_at: 1111, + }, + }); + + const message = await messagePromise; + + expect(message).toContain({ + id: 'messageId', + client_id: 'clientId', + content: 'text', + deleted_at: 1111, + }); + }); + }); + + describe('adding message reaction', () => { + it('should return reaction if chat backend request come before realtime', async (context) => { + const { chatApi, realtime } = context; + vi.spyOn(chatApi, 'addMessageReaction').mockResolvedValue({ id: 'reactionId' }); + + const conversation = new Conversation('conversationId', realtime, chatApi); + const reactionPromise = conversation.messages.addReaction('messageId', 'like'); + + context.emulateBackendPublish({ + clientId: 'clientId', + data: { + id: 'reactionId', + message_id: 'messageId', + type: 'like', + client_id: 'clientId', + }, + }); + + const reaction = await reactionPromise; + + expect(reaction).toContain({ + id: 'reactionId', + message_id: 'messageId', + type: 'like', + client_id: 'clientId', + }); + }); + + it('should return reaction if chat backend request come after realtime', async (context) => { + const { chatApi, realtime } = context; + + vi.spyOn(chatApi, 'addMessageReaction').mockImplementation(async (conversationId, messageId, type) => { + context.emulateBackendPublish({ + clientId: 'clientId', + data: { + id: 'reactionId', + message_id: messageId, + type, + client_id: 'clientId', + }, + }); + return { id: 'reactionId' }; + }); + + const conversation = new Conversation('conversationId', realtime, chatApi); + const reaction = await conversation.messages.addReaction('messageId', 'like'); + + expect(reaction).toContain({ + id: 'reactionId', + message_id: 'messageId', + type: 'like', + client_id: 'clientId', + }); + }); + }); + + describe('deleting message reaction', () => { + it('should return reaction if chat backend request come before realtime', async (context) => { + const { chatApi, realtime } = context; + vi.spyOn(chatApi, 'deleteMessageReaction').mockResolvedValue(undefined); + + const conversation = new Conversation('conversationId', realtime, chatApi); + const reactionPromise = conversation.messages.removeReaction('reactionId'); + + context.emulateBackendPublish({ + clientId: 'clientId', + data: { + id: 'reactionId', + message_id: 'messageId', + type: 'like', + client_id: 'clientId', + }, + }); + + const reaction = await reactionPromise; + + expect(reaction).toContain({ + id: 'reactionId', + message_id: 'messageId', + type: 'like', + client_id: 'clientId', + }); + }); + + it('should return reaction if chat backend request come after realtime', async (context) => { + const { chatApi, realtime } = context; + + vi.spyOn(chatApi, 'deleteMessageReaction').mockImplementation(async (reactionId) => { + context.emulateBackendPublish({ + clientId: 'clientId', + data: { + id: reactionId, + message_id: 'messageId', + type: 'like', + client_id: 'clientId', + }, + }); + }); + + const conversation = new Conversation('conversationId', realtime, chatApi); + const reaction = await conversation.messages.removeReaction('reactionId'); + + expect(reaction).toContain({ + id: 'reactionId', + message_id: 'messageId', + type: 'like', + client_id: 'clientId', + }); + }); + }); }); diff --git a/src/Messages.ts b/src/Messages.ts index 47a450e9..c632d152 100644 --- a/src/Messages.ts +++ b/src/Messages.ts @@ -1,8 +1,8 @@ import { Types } from 'ably/promises'; import { ChatApi } from './ChatApi.js'; -import { Message } from './entities.js'; +import { Message, Reaction } from './entities.js'; import RealtimeChannelPromise = Types.RealtimeChannelPromise; -import { MessageEvents } from './events.js'; +import { MessageEvents, ReactionEvents } from './events.js'; export const enum Direction { forwards = 'forwards', @@ -21,7 +21,13 @@ interface MessageListenerArgs { message: Message; } +interface ReactionListenerArgs { + type: ReactionEvents; + reaction: Reaction; +} + export type MessageListener = (args: MessageListenerArgs) => void; +export type ReactionListener = (args: ReactionListenerArgs) => void; type ChannelListener = Types.messageCallback; export class Messages { @@ -29,7 +35,7 @@ export class Messages { private readonly channel: RealtimeChannelPromise; private readonly chatApi: ChatApi; - private messageToChannelListener = new WeakMap(); + private channelListeners = new WeakMap(); constructor(conversationId: string, channel: RealtimeChannelPromise, chatApi: ChatApi) { this.conversationId = conversationId; @@ -43,79 +49,151 @@ export class Messages { } async send(text: string): Promise { - const createdMessages: Record = {}; + return this.makeMessageApiCallAndWaitForRealtimeResult(MessageEvents.created, async () => { + const { id } = await this.chatApi.sendMessage(this.conversationId, text); + return id; + }); + } + + async edit(messageId: string, text: string): Promise { + return this.makeMessageApiCallAndWaitForRealtimeResult(MessageEvents.deleted, async () => { + await this.chatApi.editMessage(this.conversationId, messageId, text); + return messageId; + }); + } + + async delete(message: Message): Promise; + async delete(messageId: string): Promise; + async delete(messageIdOrMessage: string | Message): Promise { + const messageId = typeof messageIdOrMessage === 'string' ? messageIdOrMessage : messageIdOrMessage.id; + + return this.makeMessageApiCallAndWaitForRealtimeResult(MessageEvents.deleted, async () => { + await this.chatApi.deleteMessage(this.conversationId, messageId); + return messageId; + }); + } + + async addReaction(messageId: string, reactionType: string) { + return this.makeReactionApiCallAndWaitForRealtimeResult(ReactionEvents.added, async () => { + const { id } = await this.chatApi.addMessageReaction(this.conversationId, messageId, reactionType); + return id; + }); + } + + async removeReaction(reactionId: string) { + return this.makeReactionApiCallAndWaitForRealtimeResult(ReactionEvents.deleted, async () => { + await this.chatApi.deleteMessageReaction(reactionId); + return reactionId; + }); + } + + async subscribe(event: MessageEvents, listener: MessageListener) { + const channelListener = ({ name, data }: Types.Message) => { + listener({ + type: name as MessageEvents, + message: data, + }); + }; + this.channelListeners.set(listener, channelListener); + return this.channel.subscribe(event, channelListener); + } + + unsubscribe(event: MessageEvents, listener: MessageListener) { + const channelListener = this.channelListeners.get(listener); + if (!channelListener) return; + this.channel.unsubscribe(event, channelListener); + } + + async subscribeReactions(event: ReactionEvents, listener: ReactionListener) { + const channelListener = ({ name, data }: Types.Message) => { + listener({ + type: name as ReactionEvents, + reaction: data, + }); + }; + this.channelListeners.set(listener, channelListener); + return this.channel.subscribe(event, channelListener); + } + + unsubscribeReactions(event: ReactionEvents, listener: ReactionListener) { + const channelListener = this.channelListeners.get(listener); + if (!channelListener) return; + this.channel.unsubscribe(event, channelListener); + } + + private async makeMessageApiCallAndWaitForRealtimeResult(event: MessageEvents, apiCall: () => Promise) { + const queuedMessages: Record = {}; let waitingMessageId: string | null = null; let resolver: ((message: Message) => void) | null = null; const waiter = ({ data }: Types.Message) => { const message: Message = data; - if (waitingMessageId == null) createdMessages[message.id] = message; - if (waitingMessageId == message.id) resolver?.(message); + if (waitingMessageId === null) { + queuedMessages[message.id] = message; + } else if (waitingMessageId === message.id) { + resolver?.(message); + resolver = null; + } }; - await this.channel.subscribe(MessageEvents.created, waiter); + await this.channel.subscribe(event, waiter); try { - const { id } = await this.chatApi.sendMessage(this.conversationId, text); - if (createdMessages[id]) { - this.channel.unsubscribe(MessageEvents.created, waiter); - return createdMessages[id]; + const messageId = await apiCall(); + if (queuedMessages[messageId]) { + this.channel.unsubscribe(event, waiter); + return queuedMessages[messageId]; } - waitingMessageId = id; + waitingMessageId = messageId; } catch (e) { - this.channel.unsubscribe(MessageEvents.created, waiter); + this.channel.unsubscribe(event, waiter); throw e; } - return new Promise((resolve) => { + return new Promise((resolve) => { resolver = (message) => { - this.channel.unsubscribe(MessageEvents.created, waiter); + this.channel.unsubscribe(event, waiter); resolve(message); }; }); } - async edit(messageId: string, text: string): Promise { - let resolver: ((message: Message) => void) | null = null; + private async makeReactionApiCallAndWaitForRealtimeResult(event: ReactionEvents, apiCall: () => Promise) { + const queuedReaction: Record = {}; + + let waitingReactionId: string | null = null; + let resolver: ((reaction: Reaction) => void) | null = null; + const waiter = ({ data }: Types.Message) => { - const message: Message = data; - if (messageId == message.id) resolver?.(message); + const reaction: Reaction = data; + if (waitingReactionId === null) { + queuedReaction[reaction.id] = reaction; + } else if (waitingReactionId === reaction.id) { + resolver?.(reaction); + resolver = null; + } }; - const promise: Promise = new Promise((resolve) => { - resolver = (message) => { - this.channel.unsubscribe(MessageEvents.updated, waiter); - resolve(message); - }; - }); - - await this.channel.subscribe(MessageEvents.updated, waiter); + await this.channel.subscribe(event, waiter); try { - await this.chatApi.editMessage(this.conversationId, messageId, text); + const reactionId = await apiCall(); + if (queuedReaction[reactionId]) { + this.channel.unsubscribe(event, waiter); + return queuedReaction[reactionId]; + } + waitingReactionId = reactionId; } catch (e) { - this.channel.unsubscribe(MessageEvents.updated, waiter); + this.channel.unsubscribe(event, waiter); throw e; } - return promise; - } - - async subscribe(event: MessageEvents, listener: MessageListener) { - const channelListener = ({ name, data }: Types.Message) => { - listener({ - type: name as MessageEvents, - message: data, - }); - }; - this.messageToChannelListener.set(listener, channelListener); - return this.channel.subscribe(event, channelListener); - } - - unsubscribe(event: MessageEvents, listener: MessageListener) { - const channelListener = this.messageToChannelListener.get(listener); - if (!channelListener) return; - this.channel.unsubscribe(event, channelListener); + return new Promise((resolve) => { + resolver = (reaction) => { + this.channel.unsubscribe(event, waiter); + resolve(reaction); + }; + }); } } diff --git a/src/events.ts b/src/events.ts index c70c96e7..74240ea7 100644 --- a/src/events.ts +++ b/src/events.ts @@ -3,3 +3,8 @@ export const enum MessageEvents { updated = 'message.updated', deleted = 'message.deleted', } + +export const enum ReactionEvents { + added = 'reaction.added', + deleted = 'reaction.deleted', +} diff --git a/src/index.ts b/src/index.ts index 4a2ff745..19b05d1c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ export { Chat } from './Chat.js'; -export { MessageEvents } from './events.js'; +export { MessageEvents, ReactionEvents } from './events.js'; export type { Message, Reaction, Conversation } from './entities.js'; export type { Conversation as ConversationController } from './Conversation.js'; -export type { MessageListener } from './Messages.js'; +export type { MessageListener, ReactionListener } from './Messages.js';