├── .dockerignore ├── .eslintrc.js ├── .github └── workflows │ ├── build.yml │ ├── codeql-analysis.yml │ ├── codestyle.yml │ ├── create-release-pr.yml │ ├── docker.yml │ ├── publish-packages.yml │ ├── release.yml │ ├── tests.yml │ ├── upload-webchat-production.yml │ └── upload-webchat-staging.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .yarn ├── plugins │ └── @yarnpkg │ │ ├── plugin-engines.cjs │ │ ├── plugin-interactive-tools.cjs │ │ ├── plugin-version.cjs │ │ └── plugin-workspace-tools.cjs ├── releases │ └── yarn-3.1.1.cjs └── sdks │ ├── eslint │ ├── lib │ │ └── api.js │ └── package.json │ ├── integrations.yml │ ├── prettier │ ├── index.js │ └── package.json │ └── typescript │ ├── lib │ ├── tsc.js │ ├── tsserver.js │ ├── tsserverlibrary.js │ └── typescript.js │ └── package.json ├── .yarnrc.yml ├── CHANGELOG.md ├── CODEOWNERS ├── Dockerfile ├── README.md ├── ROADMAP.md ├── Tiltfile ├── cypress.config.ts ├── docs ├── branches.md ├── channels │ └── v1 │ │ ├── changelog.md │ │ ├── messenger.md │ │ ├── readme.md │ │ ├── slack.md │ │ ├── smooch.md │ │ ├── teams.md │ │ ├── telegram.md │ │ ├── twilio.md │ │ └── vonage.md ├── configuration.md ├── database.drawio ├── database.md ├── database.png ├── docker.md ├── readme.md ├── release.md └── routes.md ├── misc ├── api.rest ├── benchmark.sh └── scripts │ ├── package.ts │ └── utils │ ├── exec.ts │ ├── logger.ts │ └── version.ts ├── package.json ├── packages ├── base │ ├── package.json │ ├── src │ │ ├── conversations.ts │ │ ├── emitter.ts │ │ ├── endpoint.ts │ │ ├── health.ts │ │ ├── index.ts │ │ ├── messages.ts │ │ ├── sync.ts │ │ ├── users.ts │ │ └── uuid.ts │ └── tsconfig.json ├── board │ ├── .parcelrc │ ├── package.json │ ├── src │ │ ├── app.tsx │ │ ├── chat │ │ │ ├── base │ │ │ │ └── system.ts │ │ │ ├── chat.ts │ │ │ ├── conversation │ │ │ │ ├── events.ts │ │ │ │ └── system.ts │ │ │ ├── index.ts │ │ │ ├── lang │ │ │ │ └── system.ts │ │ │ ├── locale │ │ │ │ └── system.ts │ │ │ ├── messages │ │ │ │ ├── events.ts │ │ │ │ └── system.ts │ │ │ ├── storage │ │ │ │ └── system.ts │ │ │ └── user │ │ │ │ ├── events.ts │ │ │ │ └── system.ts │ │ ├── index.html │ │ ├── linker.ts │ │ ├── skin │ │ │ ├── index.ts │ │ │ ├── lang │ │ │ │ ├── en.json │ │ │ │ ├── fr.json │ │ │ │ └── index.ts │ │ │ ├── render.ts │ │ │ └── ui.ts │ │ ├── style.css │ │ └── watcher.ts │ └── tsconfig.json ├── channels │ ├── README.md │ ├── example │ │ ├── app.ts │ │ ├── config.json │ │ ├── index.ts │ │ ├── payloads.json │ │ └── tsconfig.json │ ├── package.json │ ├── src │ │ ├── base │ │ │ ├── api.ts │ │ │ ├── channel.ts │ │ │ ├── config.ts │ │ │ ├── context.ts │ │ │ ├── emitter.ts │ │ │ ├── endpoint.ts │ │ │ ├── kvs.ts │ │ │ ├── logger.ts │ │ │ ├── meta.ts │ │ │ ├── renderer.ts │ │ │ ├── renderers │ │ │ │ ├── audio.ts │ │ │ │ ├── card.ts │ │ │ │ ├── carousel.ts │ │ │ │ ├── choices.ts │ │ │ │ ├── dropdown.ts │ │ │ │ ├── file.ts │ │ │ │ ├── image.ts │ │ │ │ ├── location.ts │ │ │ │ ├── text.ts │ │ │ │ └── video.ts │ │ │ ├── sender.ts │ │ │ ├── senders │ │ │ │ ├── common.ts │ │ │ │ └── typing.ts │ │ │ ├── service.ts │ │ │ └── stream.ts │ │ ├── content │ │ │ └── types.ts │ │ ├── index.ts │ │ ├── messenger │ │ │ ├── README.md │ │ │ ├── api.ts │ │ │ ├── channel.ts │ │ │ ├── config.ts │ │ │ ├── context.ts │ │ │ ├── messenger.ts │ │ │ ├── renderers │ │ │ │ ├── audio.ts │ │ │ │ ├── carousel.ts │ │ │ │ ├── choices.ts │ │ │ │ ├── file.ts │ │ │ │ ├── image.ts │ │ │ │ ├── index.ts │ │ │ │ ├── location.ts │ │ │ │ ├── text.ts │ │ │ │ └── video.ts │ │ │ ├── senders │ │ │ │ ├── common.ts │ │ │ │ ├── index.ts │ │ │ │ └── typing.ts │ │ │ ├── service.ts │ │ │ └── stream.ts │ │ ├── slack │ │ │ ├── README.md │ │ │ ├── api.ts │ │ │ ├── channel.ts │ │ │ ├── config.ts │ │ │ ├── context.ts │ │ │ ├── renderers │ │ │ │ ├── audio.ts │ │ │ │ ├── carousel.ts │ │ │ │ ├── choices.ts │ │ │ │ ├── dropdown.ts │ │ │ │ ├── file.ts │ │ │ │ ├── image.ts │ │ │ │ ├── index.ts │ │ │ │ ├── location.ts │ │ │ │ ├── text.ts │ │ │ │ └── video.ts │ │ │ ├── senders │ │ │ │ ├── common.ts │ │ │ │ └── index.ts │ │ │ ├── service.ts │ │ │ └── stream.ts │ │ ├── smooch │ │ │ ├── README.md │ │ │ ├── api.ts │ │ │ ├── channel.ts │ │ │ ├── config.ts │ │ │ ├── context.ts │ │ │ ├── renderers │ │ │ │ ├── audio.ts │ │ │ │ ├── carousel.ts │ │ │ │ ├── choices.ts │ │ │ │ ├── file.ts │ │ │ │ ├── image.ts │ │ │ │ ├── index.ts │ │ │ │ ├── location.ts │ │ │ │ ├── text.ts │ │ │ │ └── video.ts │ │ │ ├── senders │ │ │ │ ├── common.ts │ │ │ │ ├── index.ts │ │ │ │ └── typing.ts │ │ │ ├── service.ts │ │ │ ├── smooch.ts │ │ │ └── stream.ts │ │ ├── teams │ │ │ ├── README.md │ │ │ ├── api.ts │ │ │ ├── channel.ts │ │ │ ├── config.ts │ │ │ ├── context.ts │ │ │ ├── renderers │ │ │ │ ├── audio.ts │ │ │ │ ├── carousel.ts │ │ │ │ ├── choices.ts │ │ │ │ ├── dropdown.ts │ │ │ │ ├── file.ts │ │ │ │ ├── image.ts │ │ │ │ ├── index.ts │ │ │ │ ├── location.ts │ │ │ │ ├── text.ts │ │ │ │ └── video.ts │ │ │ ├── senders │ │ │ │ ├── common.ts │ │ │ │ ├── index.ts │ │ │ │ └── typing.ts │ │ │ ├── service.ts │ │ │ └── stream.ts │ │ ├── telegram │ │ │ ├── README.md │ │ │ ├── api.ts │ │ │ ├── channel.ts │ │ │ ├── config.ts │ │ │ ├── context.ts │ │ │ ├── renderers │ │ │ │ ├── audio.ts │ │ │ │ ├── carousel.ts │ │ │ │ ├── choices.ts │ │ │ │ ├── file.ts │ │ │ │ ├── image.ts │ │ │ │ ├── index.ts │ │ │ │ ├── location.ts │ │ │ │ ├── text.ts │ │ │ │ └── video.ts │ │ │ ├── senders │ │ │ │ ├── common.ts │ │ │ │ ├── index.ts │ │ │ │ └── typing.ts │ │ │ ├── service.ts │ │ │ └── stream.ts │ │ ├── twilio │ │ │ ├── README.md │ │ │ ├── api.ts │ │ │ ├── channel.ts │ │ │ ├── config.ts │ │ │ ├── context.ts │ │ │ ├── renderers │ │ │ │ ├── audio.ts │ │ │ │ ├── carousel.ts │ │ │ │ ├── choices.ts │ │ │ │ ├── file.ts │ │ │ │ ├── image.ts │ │ │ │ ├── index.ts │ │ │ │ ├── location.ts │ │ │ │ ├── text.ts │ │ │ │ └── video.ts │ │ │ ├── senders │ │ │ │ ├── common.ts │ │ │ │ └── index.ts │ │ │ ├── service.ts │ │ │ └── stream.ts │ │ ├── typings │ │ │ └── ext-name.d.ts │ │ └── vonage │ │ │ ├── README.md │ │ │ ├── api.ts │ │ │ ├── channel.ts │ │ │ ├── config.ts │ │ │ ├── context.ts │ │ │ ├── renderers │ │ │ ├── audio.ts │ │ │ ├── carousel.ts │ │ │ ├── choices.ts │ │ │ ├── dropdown.ts │ │ │ ├── file.ts │ │ │ ├── image.ts │ │ │ ├── index.ts │ │ │ ├── location.ts │ │ │ ├── template-media.ts │ │ │ ├── template.ts │ │ │ ├── text.ts │ │ │ └── video.ts │ │ │ ├── senders │ │ │ ├── common.ts │ │ │ └── index.ts │ │ │ ├── service.ts │ │ │ └── stream.ts │ └── tsconfig.json ├── client │ ├── README.md │ ├── package.json │ ├── src │ │ ├── api.ts │ │ ├── auth.ts │ │ ├── base.ts │ │ ├── channel.ts │ │ ├── client.ts │ │ ├── emitter.ts │ │ ├── errors.ts │ │ ├── events.ts │ │ ├── index.ts │ │ ├── logger.ts │ │ ├── options.ts │ │ └── schema.ts │ ├── test │ │ ├── e2e │ │ │ └── client.test.ts │ │ └── tsconfig.json │ └── tsconfig.json ├── components │ ├── README.md │ ├── package.json │ ├── src │ │ ├── base │ │ │ ├── button.tsx │ │ │ ├── error-boundary.tsx │ │ │ └── file-input.tsx │ │ ├── content-typings.ts │ │ ├── css │ │ │ ├── botpress-default.css │ │ │ ├── slick-theme.css │ │ │ ├── slick.css │ │ │ └── theme-light.css │ │ ├── global.d.ts │ │ ├── index.tsx │ │ ├── react-text-format.d.ts │ │ ├── renderer │ │ │ ├── carousel.tsx │ │ │ ├── choice.tsx │ │ │ ├── custom.tsx │ │ │ ├── dropdown.tsx │ │ │ ├── file.tsx │ │ │ ├── index.tsx │ │ │ ├── keyboard.tsx │ │ │ ├── location.tsx │ │ │ ├── login.tsx │ │ │ ├── single-choice.tsx │ │ │ ├── text.tsx │ │ │ ├── typing.tsx │ │ │ └── voice.tsx │ │ ├── typings.ts │ │ └── utils.ts │ ├── story │ │ ├── audio.stories.tsx │ │ ├── card.stories.tsx │ │ ├── carousel.stories.tsx │ │ ├── chat.stories.tsx │ │ ├── choice.stories.tsx │ │ ├── config │ │ │ ├── custom.css │ │ │ ├── main.ts │ │ │ └── preview.ts │ │ ├── dropdown.stories.tsx │ │ ├── file.stories.tsx │ │ ├── login.stories.tsx │ │ ├── text.stories.tsx │ │ ├── tsconfig.json │ │ ├── video.stories.tsx │ │ └── voice.stories.tsx │ ├── test │ │ ├── jest.setup.ts │ │ ├── tsconfig.json │ │ └── unit │ │ │ ├── carousel.test.tsx │ │ │ ├── choice.test.tsx │ │ │ ├── file.test.tsx │ │ │ ├── index.test.tsx │ │ │ ├── login.test.tsx │ │ │ ├── mocks │ │ │ └── matchMedia.mock.ts │ │ │ ├── renderer.test.tsx │ │ │ ├── test-events.json │ │ │ ├── text.test.tsx │ │ │ ├── utils.test.tsx │ │ │ └── voice.test.tsx │ └── tsconfig.json ├── engine │ ├── package.json │ ├── src │ │ ├── barrier │ │ │ ├── barrier.ts │ │ │ └── service.ts │ │ ├── base │ │ │ ├── errors.ts │ │ │ ├── service.ts │ │ │ └── table.ts │ │ ├── batching │ │ │ ├── batcher.ts │ │ │ └── service.ts │ │ ├── caching │ │ │ ├── cache.ts │ │ │ ├── cache2D.ts │ │ │ └── service.ts │ │ ├── crypto │ │ │ └── service.ts │ │ ├── database │ │ │ └── service.ts │ │ ├── dispatch │ │ │ ├── dispatcher.ts │ │ │ └── service.ts │ │ ├── distributed │ │ │ ├── base │ │ │ │ └── subservice.ts │ │ │ ├── local │ │ │ │ └── subservice.ts │ │ │ ├── redis │ │ │ │ ├── ping.ts │ │ │ │ └── subservice.ts │ │ │ ├── service.ts │ │ │ └── types.ts │ │ ├── engine.ts │ │ ├── global.ts │ │ ├── index.ts │ │ ├── kvs │ │ │ ├── service.ts │ │ │ └── table.ts │ │ ├── logger │ │ │ ├── service.ts │ │ │ └── types.ts │ │ ├── meta │ │ │ ├── service.ts │ │ │ ├── table.ts │ │ │ └── types.ts │ │ └── migration │ │ │ ├── migration.ts │ │ │ └── service.ts │ ├── test │ │ ├── tsconfig.json │ │ └── unit │ │ │ ├── barrier.test.ts │ │ │ ├── batcher.test.ts │ │ │ ├── database.test.ts │ │ │ └── logger.test.ts │ └── tsconfig.json ├── example │ ├── api.rest │ ├── package.json │ ├── src │ │ ├── api.ts │ │ ├── app.ts │ │ ├── global.ts │ │ ├── houses │ │ │ ├── api.ts │ │ │ ├── schema.ts │ │ │ ├── service.ts │ │ │ ├── table.ts │ │ │ └── types.ts │ │ ├── index.ts │ │ ├── migrations │ │ │ ├── 0.0.1-init.ts │ │ │ └── index.ts │ │ ├── rewire.ts │ │ ├── socket.ts │ │ └── stream.ts │ └── tsconfig.json ├── framework │ ├── package.json │ ├── src │ │ ├── base │ │ │ ├── api-manager.ts │ │ │ ├── auth │ │ │ │ ├── admin.ts │ │ │ │ ├── auth.ts │ │ │ │ ├── base.ts │ │ │ │ ├── client.ts │ │ │ │ └── public.ts │ │ │ └── schema.ts │ │ ├── client-tokens │ │ │ ├── service.ts │ │ │ ├── table.ts │ │ │ └── types.ts │ │ ├── clients │ │ │ ├── api.ts │ │ │ ├── schema.ts │ │ │ ├── service.ts │ │ │ ├── table.ts │ │ │ └── types.ts │ │ ├── entry.ts │ │ ├── framework.ts │ │ ├── global.ts │ │ ├── index.ts │ │ ├── launcher.ts │ │ ├── routes.ts │ │ ├── start.ts │ │ └── starter.ts │ └── tsconfig.json ├── inject │ ├── .parcelrc │ ├── README.md │ ├── jest.inject.config.ts │ ├── package.json │ ├── src │ │ ├── index.html │ │ ├── index.tsx │ │ ├── inject.css │ │ ├── inject.test.ts │ │ ├── inject.ts │ │ └── themes │ │ │ └── theme-light.css │ ├── test │ │ ├── cssmock.js │ │ ├── serve.ts │ │ ├── tsconfig.json │ │ └── webchat.html │ └── tsconfig.json ├── server │ ├── package.json │ ├── src │ │ ├── api.ts │ │ ├── app.ts │ │ ├── base │ │ │ ├── source.ts │ │ │ └── streamer.ts │ │ ├── channels │ │ │ ├── api.ts │ │ │ ├── service.ts │ │ │ └── table.ts │ │ ├── clients │ │ │ ├── api.ts │ │ │ └── schema.ts │ │ ├── conduits │ │ │ ├── events.ts │ │ │ ├── service.ts │ │ │ ├── table.ts │ │ │ └── types.ts │ │ ├── conversations │ │ │ ├── api.ts │ │ │ ├── events.ts │ │ │ ├── schema.ts │ │ │ ├── service.ts │ │ │ ├── socket.ts │ │ │ ├── stream.ts │ │ │ └── table.ts │ │ ├── converse │ │ │ ├── dispatch.ts │ │ │ ├── service.ts │ │ │ └── types.ts │ │ ├── global.ts │ │ ├── health │ │ │ ├── api.ts │ │ │ ├── events.ts │ │ │ ├── listener.ts │ │ │ ├── service.ts │ │ │ ├── stream.ts │ │ │ └── table.ts │ │ ├── index.ts │ │ ├── instances │ │ │ ├── clearing │ │ │ │ └── service.ts │ │ │ ├── invalidation │ │ │ │ └── service.ts │ │ │ ├── lifetime │ │ │ │ ├── dispatch.ts │ │ │ │ ├── events.ts │ │ │ │ └── service.ts │ │ │ ├── messaging │ │ │ │ ├── queue.ts │ │ │ │ └── service.ts │ │ │ ├── monitoring │ │ │ │ └── service.ts │ │ │ ├── sandbox │ │ │ │ └── service.ts │ │ │ └── service.ts │ │ ├── interceptor.ts │ │ ├── mapping │ │ │ ├── api.ts │ │ │ ├── convmap │ │ │ │ ├── service.ts │ │ │ │ ├── table.ts │ │ │ │ └── types.ts │ │ │ ├── identities │ │ │ │ ├── service.ts │ │ │ │ ├── table.ts │ │ │ │ └── types.ts │ │ │ ├── sandboxmap │ │ │ │ ├── service.ts │ │ │ │ ├── table.ts │ │ │ │ └── types.ts │ │ │ ├── schema.ts │ │ │ ├── senders │ │ │ │ ├── service.ts │ │ │ │ ├── table.ts │ │ │ │ └── types.ts │ │ │ ├── service.ts │ │ │ ├── threads │ │ │ │ ├── service.ts │ │ │ │ ├── table.ts │ │ │ │ └── types.ts │ │ │ ├── tunnels │ │ │ │ ├── service.ts │ │ │ │ ├── table.ts │ │ │ │ └── types.ts │ │ │ ├── types.ts │ │ │ └── usermap │ │ │ │ ├── service.ts │ │ │ │ ├── table.ts │ │ │ │ └── types.ts │ │ ├── messages │ │ │ ├── api.ts │ │ │ ├── events.ts │ │ │ ├── schema.ts │ │ │ ├── service.ts │ │ │ ├── socket.ts │ │ │ ├── stream.ts │ │ │ └── table.ts │ │ ├── metrics │ │ │ └── service.ts │ │ ├── migrations │ │ │ ├── 0.0.1-init.ts │ │ │ ├── 0.1.19-status.ts │ │ │ ├── 0.1.20-fix-client-schema.ts │ │ │ ├── 0.1.21-client-tokens.ts │ │ │ ├── 1.0.2-channel-versions.ts │ │ │ ├── 1.1.0-user-tokens.ts │ │ │ ├── 1.1.5-a-provisions.ts │ │ │ ├── 1.1.5-b-move-provider-id.ts │ │ │ ├── 1.1.7-custom-channels.ts │ │ │ ├── 1.1.8-user-data.ts │ │ │ └── index.ts │ │ ├── providers │ │ │ ├── events.ts │ │ │ ├── service.ts │ │ │ ├── table.ts │ │ │ └── types.ts │ │ ├── provisions │ │ │ ├── service.ts │ │ │ ├── table.ts │ │ │ └── types.ts │ │ ├── rewire.ts │ │ ├── socket.ts │ │ ├── socket │ │ │ ├── events.ts │ │ │ ├── manager.ts │ │ │ ├── schema.ts │ │ │ └── service.ts │ │ ├── status │ │ │ ├── service.ts │ │ │ ├── table.ts │ │ │ └── types.ts │ │ ├── stream.ts │ │ ├── sync │ │ │ ├── api.ts │ │ │ ├── schema.ts │ │ │ └── service.ts │ │ ├── user-tokens │ │ │ ├── api.ts │ │ │ ├── schema.ts │ │ │ ├── service.ts │ │ │ ├── table.ts │ │ │ └── types.ts │ │ ├── users │ │ │ ├── api.ts │ │ │ ├── events.ts │ │ │ ├── schema.ts │ │ │ ├── service.ts │ │ │ ├── socket.ts │ │ │ ├── stream.ts │ │ │ └── table.ts │ │ └── webhooks │ │ │ ├── service.ts │ │ │ ├── table.ts │ │ │ └── types.ts │ ├── test │ │ ├── integration │ │ │ ├── client-tokens.test.ts │ │ │ ├── clients.test.ts │ │ │ ├── conduits.test.ts │ │ │ ├── conversations.test.ts │ │ │ ├── health.test.ts │ │ │ ├── mapping.test.ts │ │ │ ├── providers.test.ts │ │ │ ├── provisions.test.ts │ │ │ ├── status.test.ts │ │ │ └── user-tokens.test.ts │ │ ├── migration │ │ │ ├── global-diff.test.ts │ │ │ ├── migration-cli.test.ts │ │ │ ├── migrations-diff.test.ts │ │ │ └── utils │ │ │ │ ├── database.ts │ │ │ │ ├── diff.ts │ │ │ │ ├── error.ts │ │ │ │ ├── seed.ts │ │ │ │ ├── semver.ts │ │ │ │ └── server.ts │ │ ├── security │ │ │ ├── api.test.ts │ │ │ └── mocha-froth.ts │ │ ├── tsconfig.json │ │ ├── unit │ │ │ ├── linked-queue.test.ts │ │ │ ├── socket.test.ts │ │ │ └── sync.test.ts │ │ └── utils │ │ │ └── index.ts │ └── tsconfig.json ├── socket │ ├── package.json │ ├── src │ │ ├── com.ts │ │ ├── emitter.ts │ │ ├── index.ts │ │ └── socket.ts │ ├── test │ │ ├── e2e │ │ │ └── socket.test.ts │ │ └── tsconfig.json │ └── tsconfig.json └── webchat │ ├── .parcelrc │ ├── README.md │ ├── assets │ ├── fonts │ │ ├── roboto.css │ │ └── roboto │ │ │ ├── roboto.woff2 │ │ │ └── roboto500.woff2 │ └── notification.mp3 │ ├── package.json │ ├── src │ ├── components │ │ ├── Composer.tsx │ │ ├── ConditionalWrap.tsx │ │ ├── Container.tsx │ │ ├── ConversationList.tsx │ │ ├── Footer.tsx │ │ ├── Header.tsx │ │ ├── VoiceRecorder.tsx │ │ ├── common │ │ │ ├── Avatar │ │ │ │ └── index.tsx │ │ │ ├── BotInfo │ │ │ │ ├── index.tsx │ │ │ │ └── style.scss │ │ │ ├── ConfirmDialog │ │ │ │ ├── index.tsx │ │ │ │ └── style.module.scss │ │ │ ├── Dialog │ │ │ │ ├── index.tsx │ │ │ │ └── style.module.scss │ │ │ ├── ToolTip │ │ │ │ ├── index.tsx │ │ │ │ ├── style.module.scss │ │ │ │ └── utils.ts │ │ │ └── variables.scss │ │ └── messages │ │ │ ├── InlineFeedback.tsx │ │ │ ├── Message.tsx │ │ │ ├── MessageGroup.tsx │ │ │ └── MessageList.tsx │ ├── core │ │ ├── api.tsx │ │ ├── constants.ts │ │ └── socket.tsx │ ├── declaration.ts │ ├── fonts │ │ └── roboto.tsx │ ├── globals.ts │ ├── icons │ │ ├── Add.tsx │ │ ├── Cancel.tsx │ │ ├── Chat.tsx │ │ ├── Close.tsx │ │ ├── Delete.tsx │ │ ├── Download.tsx │ │ ├── Email.tsx │ │ ├── Information.tsx │ │ ├── List.tsx │ │ ├── Microphone.tsx │ │ ├── Phone.tsx │ │ ├── Reload.tsx │ │ ├── ThumbsDown.tsx │ │ ├── ThumbsUp.tsx │ │ └── Website.tsx │ ├── index.tsx │ ├── main.tsx │ ├── store │ │ ├── composer.ts │ │ ├── index.ts │ │ └── view.ts │ ├── translations │ │ ├── ar.json │ │ ├── de.json │ │ ├── en.json │ │ ├── es.json │ │ ├── fr.json │ │ ├── index.tsx │ │ ├── it.json │ │ ├── pt.json │ │ ├── ru.json │ │ └── uk.json │ ├── typings.ts │ └── utils │ │ ├── analytics.ts │ │ ├── index.ts │ │ ├── storage.ts │ │ └── webchatEvents.ts │ └── tsconfig.json ├── test ├── README.md ├── cypress │ ├── fixtures │ │ └── chat.json │ ├── support │ │ ├── commands.ts │ │ ├── cypress.d.ts │ │ ├── index.ts │ │ └── tasks.ts │ ├── test.config.ts │ ├── tsconfig.json │ └── webchat.cy.ts ├── jest.e2e.config.ts ├── jest.e2e.setup.ts ├── jest.e2e.teardown.ts ├── jest.integration.config.ts ├── jest.integration.setup.ts ├── jest.integration.teardown.ts ├── jest.migration.config.ts ├── jest.security.config.ts ├── jest.unit.config.ts └── setup │ ├── database.ts │ ├── docker-compose.yml │ └── server.ts ├── tsconfig.json ├── tsconfig.packages.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/*.md 3 | *.log 4 | **/*.env 5 | **/.parcel-cache 6 | **/.cache 7 | **/test 8 | **/dist 9 | **/story 10 | 11 | .git 12 | .github 13 | .vscode 14 | dist 15 | bin 16 | docs 17 | misc 18 | .editorconfig 19 | .eslintrc.js 20 | .gitignore 21 | .prettierrc 22 | .nvmrc 23 | Dockerfile 24 | 25 | # Yarn 3 files 26 | .pnp.* 27 | .yarn 28 | !.yarn/releases 29 | !.yarn/plugins 30 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | on: 3 | push: 4 | branches: 5 | - master 6 | tags: 7 | - v* 8 | release: 9 | types: [published] 10 | workflow_dispatch: {} 11 | 12 | permissions: 13 | id-token: write 14 | contents: read 15 | 16 | jobs: 17 | messaging: 18 | name: Publish Messaging Docker Image 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@master 23 | - uses: botpress/gh-actions/build/docker@v2 24 | with: 25 | repository: messaging 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | bin/ 4 | *.log 5 | .parcel-cache 6 | parcel-bundle-reports 7 | .cache 8 | coverage/ 9 | storybook-static/ 10 | build/ 11 | .pnp.* 12 | .yarn/* 13 | !.yarn/patches 14 | !.yarn/plugins 15 | !.yarn/releases 16 | !.yarn/sdks 17 | !.yarn/versions 18 | .test-data 19 | 20 | test/cypress/videos 21 | test/cypress/screenshots 22 | test/cypress/downloads 23 | 24 | .DS_Store 25 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.14.0 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "semi": false, 6 | "bracketSpacing": true, 7 | "requirePragma": false 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["arcanis.vscode-zipfs", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "humao.rest-client"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll": "explicit" 4 | }, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.formatOnSave": true, 7 | "editor.tabSize": 2, 8 | "eslint.nodePath": ".yarn/sdks", 9 | "files.eol": "\n", 10 | "prettier.prettierPath": ".yarn/sdks/prettier/index.js", 11 | "search.exclude": { 12 | "**/.pnp.*": true, 13 | "**/.yarn": true 14 | }, 15 | "typescript.enablePromptUseWorkspaceTsdk": true, 16 | "typescript.tsdk": ".yarn/sdks/typescript/lib", 17 | "files.associations": { 18 | ".parcelrc": "json" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "type": "shell", 7 | "command": "yarn build", 8 | "group": { 9 | "kind": "build", 10 | "isDefault": true 11 | }, 12 | "presentation": { 13 | "echo": false, 14 | "reveal": "silent", 15 | "focus": false, 16 | "panel": "shared", 17 | "showReuseMessage": false, 18 | "clear": true 19 | }, 20 | "problemMatcher": [] 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/lib/api.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require eslint 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real eslint your application uses 20 | module.exports = absRequire(`eslint`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint", 3 | "version": "7.32.0-sdk", 4 | "main": "./lib/api.js", 5 | "type": "commonjs" 6 | } 7 | -------------------------------------------------------------------------------- /.yarn/sdks/integrations.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by @yarnpkg/sdks. 2 | # Manual changes might be lost! 3 | 4 | integrations: 5 | - vscode 6 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require prettier/index.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real prettier/index.js your application uses 20 | module.exports = absRequire(`prettier/index.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prettier", 3 | "version": "2.5.1-sdk", 4 | "main": "./index.js", 5 | "type": "commonjs" 6 | } 7 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/tsc.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/lib/tsc.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/lib/tsc.js your application uses 20 | module.exports = absRequire(`typescript/lib/tsc.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/typescript.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/lib/typescript.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/lib/typescript.js your application uses 20 | module.exports = absRequire(`typescript/lib/typescript.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "version": "4.5.4-sdk", 4 | "main": "./lib/typescript.js", 5 | "type": "commonjs" 6 | } 7 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 5 | spec: "@yarnpkg/plugin-workspace-tools" 6 | - path: .yarn/plugins/@yarnpkg/plugin-version.cjs 7 | spec: "@yarnpkg/plugin-version" 8 | - path: .yarn/plugins/@yarnpkg/plugin-engines.cjs 9 | spec: "https://raw.githubusercontent.com/devoto13/yarn-plugin-engines/main/bundles/%40yarnpkg/plugin-engines.js" 10 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 11 | spec: "@yarnpkg/plugin-interactive-tools" 12 | 13 | yarnPath: .yarn/releases/yarn-3.1.1.cjs 14 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | import { sendMessage } from './test/cypress/support/tasks' 3 | import { testConfig } from './test/cypress/test.config' 4 | 5 | export default defineConfig({ 6 | e2e: { 7 | baseUrl: testConfig.baseUrl, 8 | downloadsFolder: 'test/cypress/downloads', 9 | fixturesFolder: 'test/cypress/fixtures', 10 | screenshotsFolder: 'test/cypress/screenshots', 11 | videosFolder: 'test/cypress/videos', 12 | supportFile: 'test/cypress/support/index.ts', 13 | specPattern: '**/*.cy.ts', 14 | setupNodeEvents(on, config) { 15 | on('task', { 16 | sendMessage 17 | }) 18 | } 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /docs/branches.md: -------------------------------------------------------------------------------- 1 | # Useful Branches 2 | 3 | We use a trunk based development flow when working with Git 4 | 5 | ## Branches 6 | 7 | ### master 8 | 9 | `master` is our main "production" branch and all PR should end up being merged into this branch. 10 | 11 | ### v1 12 | 13 | The `v1` branch is there to allow making changes to legacy packages (e.g. legacy channels). 14 | 15 | ### main_backup 16 | 17 | `main_backup` is a experimentation that prototyped having all the services (messaging, studio, runtime, webchat) in a single monorepo. This **should only** be seen as an example rather than a working system. You can refer to it as it uses a proper structure and nice tooling (Yarn v3, Parcel, reusable test suites, reusable and optimized CI, etc.) for this kind of project. 18 | -------------------------------------------------------------------------------- /docs/channels/v1/readme.md: -------------------------------------------------------------------------------- 1 | # Botpress Messaging Channels Version 1.0.0 2 | 3 | - [Changelog](./changelog.md) 4 | 5 | ## Channels 6 | 7 | - [Facebook Messenger](./messenger.md) 8 | - [Slack](./slack.md) 9 | - [Smooch](./smooch.md) 10 | - [Teams](./teams.md) 11 | - [Telegram](./telegram.md) 12 | - [Twilio](./twilio.md) 13 | - [Vonage](./vonage.md) 14 | 15 | ## Development 16 | 17 | To make change to any of the supported channels, please follow the documentation [here](../../../packages/channels/README.md) 18 | 19 | -------------------------------------------------------------------------------- /docs/database.md: -------------------------------------------------------------------------------- 1 | # Database Diagram (Outdated) 2 | 3 | _Note: this diagram is missing lots of tables and might not reflect the true database diagram when locally inspecting tour SQLite database_ 4 | 5 | ![Database Diagram](./database.png 'Diagram') 6 | 7 | [database.drawio](./database.drawio) 8 | -------------------------------------------------------------------------------- /docs/database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/botpress/messaging/e7d8faf55f0925c79f7ff56dd23f17831d2116ab/docs/database.png -------------------------------------------------------------------------------- /docs/docker.md: -------------------------------------------------------------------------------- 1 | # Docker 2 | 3 | ## Minimal Configuration 4 | 5 | Build local docker image 6 | 7 | ``` 8 | sudo docker build . -t messaging 9 | ``` 10 | 11 | Run docker container with minimal configurations 12 | 13 | ``` 14 | docker run -d \ 15 | --name messaging \ 16 | -p 3100:3100 \ 17 | -v messaging_data:/messaging/data \ 18 | --env EXTERNAL_URL=https://your-external-url.com \ 19 | messaging:latest 20 | ``` 21 | 22 | Public docker image incoming 23 | 24 | ## Recommended Configuration 25 | 26 | It's highly recommended to you set an encryption key using `--env ENCRYPTION_KEY=myKey` 27 | 28 | Refer to the [configurations doc](./configuration.md) to see what format is accepted for encryption keys 29 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # Botpress Messaging Documentation 2 | 3 | ## Release 4 | 5 | - [Release Process](./release.md) 6 | 7 | ## Branches 8 | 9 | - [Useful branches](./branches.md) 10 | 11 | ## Configs 12 | 13 | - [Configurations](./configuration.md) 14 | 15 | ## API 16 | 17 | - [Routes](./routes.md) 18 | 19 | ## Channels 20 | 21 | - [Version 1.0.0](./channels/v1/readme.md) 22 | 23 | ## Docker 24 | 25 | - [Docker](./docker.md) 26 | 27 | ## Database 28 | 29 | - [Database Diagram](./database.md) 30 | -------------------------------------------------------------------------------- /misc/benchmark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # To run this : 4 | # yarn global add loadtest 5 | 6 | clientId=YOUR_CLIENT_ID 7 | clientToken=YOUR_CLIENT_TOKEN 8 | conversationId=YOUR_CONVERSATION_ID 9 | authorId=YOUR_AUTHOR_ID 10 | 11 | yarn dlx loadtest http://localhost:3100/api/v1/messages \ 12 | -m POST \ 13 | -T application/json \ 14 | -c 64 \ 15 | -P "{\"conversationId\":\"$conversationId\",\"authorId\":\"$authorId\",\"payload\":{\"type\":\"text\",\"text\":\"Hello this is a text message!\"}}" \ 16 | -H "x-bp-messaging-client-id:${clientId}" \ 17 | -H "x-bp-messaging-client-token:${clientToken}" 18 | -------------------------------------------------------------------------------- /misc/scripts/utils/exec.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process' 2 | 3 | export type Options = Parameters[1] 4 | 5 | export const execute = async (cmd: string, opts: Options, { silent } = { silent: false }) => { 6 | await new Promise((resolve, reject) => { 7 | const proc = exec(cmd, opts, (err) => (err ? reject(err) : resolve(undefined))) 8 | if (!silent) { 9 | proc.stdout?.pipe(process.stdout) 10 | proc.stderr?.pipe(process.stderr) 11 | } 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /misc/scripts/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import clc from 'cli-color' 2 | 3 | const logger = { 4 | info: (msg: string) => console.info(`${clc.green('[INFO]')} ${msg}`), 5 | warning: (msg: string) => console.info(`${clc.yellow('[WARNING]')} ${msg}`), 6 | error: (msg: string, err?: Error | string) => console.info(`${clc.red('[ERROR]')} ${msg}`, err) 7 | } 8 | 9 | export default logger 10 | -------------------------------------------------------------------------------- /misc/scripts/utils/version.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | export const getProjectVersion = () => { 4 | return require(path.join(__dirname, '../../../package.json')).version 5 | } 6 | 7 | export const formatVersion = (version: string): string => { 8 | return version.replace(/\./g, '_') 9 | } 10 | -------------------------------------------------------------------------------- /packages/base/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@botpress/messaging-base", 3 | "version": "1.2.0", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "source": "src/index.ts", 7 | "license": "AGPL-3.0", 8 | "scripts": { 9 | "build": "yarn && yarn run -T tsc --build", 10 | "watch": "yarn && yarn run -T tsc --build --watch", 11 | "prepublish": "yarn run -T rimraf dist && yarn --immutable && yarn run -T tsc --build && yarn run -T rimraf dist/.tsbuildinfo" 12 | }, 13 | "files": [ 14 | "dist" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/base/src/conversations.ts: -------------------------------------------------------------------------------- 1 | import { uuid } from './uuid' 2 | 3 | export interface Conversation { 4 | id: uuid 5 | clientId: uuid 6 | userId: uuid 7 | createdOn: Date 8 | } 9 | -------------------------------------------------------------------------------- /packages/base/src/emitter.ts: -------------------------------------------------------------------------------- 1 | export class Emitter { 2 | private listeners: { [eventId: number]: ((arg: any) => Promise)[] } = {} 3 | 4 | public on(event: K, listener: (arg: T[K]) => Promise, pushBack: boolean = false) { 5 | const listeners = this.listeners[event as number] 6 | if (!listeners) { 7 | this.listeners[event as number] = [listener] 8 | } else if (!pushBack) { 9 | listeners.push(listener) 10 | } else { 11 | listeners.unshift(listener) 12 | } 13 | } 14 | 15 | public async emit(event: K, arg: T[K]): Promise { 16 | const listeners = this.listeners[event as number] 17 | if (listeners?.length) { 18 | for (const listener of listeners) { 19 | await listener(arg) 20 | } 21 | return true 22 | } else { 23 | return false 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/base/src/endpoint.ts: -------------------------------------------------------------------------------- 1 | export interface Endpoint { 2 | channel: string | { name: string; version: string } 3 | identity: string 4 | sender: string 5 | thread: string 6 | } 7 | -------------------------------------------------------------------------------- /packages/base/src/health.ts: -------------------------------------------------------------------------------- 1 | import { uuid } from './uuid' 2 | 3 | export enum HealthEventType { 4 | Create = 'create', 5 | Configure = 'configure', 6 | Start = 'start', 7 | StartFailure = 'start-failure', 8 | Initialize = 'initialize', 9 | InitializeFailure = 'initialize-failure', 10 | Sleep = 'sleep', 11 | Delete = 'delete' 12 | } 13 | 14 | export interface HealthEvent { 15 | id: uuid 16 | conduitId: uuid 17 | time: Date 18 | type: HealthEventType 19 | data?: any 20 | } 21 | 22 | export interface HealthReport { 23 | channels: { 24 | [channel: string]: { 25 | events: HealthReportEvent[] 26 | } 27 | } 28 | } 29 | 30 | export type HealthReportEvent = Omit 31 | -------------------------------------------------------------------------------- /packages/base/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './conversations' 2 | export * from './emitter' 3 | export * from './endpoint' 4 | export * from './health' 5 | export * from './messages' 6 | export * from './sync' 7 | export * from './users' 8 | export * from './uuid' 9 | -------------------------------------------------------------------------------- /packages/base/src/messages.ts: -------------------------------------------------------------------------------- 1 | import { uuid } from './uuid' 2 | 3 | export interface Message { 4 | id: uuid 5 | conversationId: uuid 6 | authorId: uuid | undefined 7 | sentOn: Date 8 | payload: any 9 | } 10 | -------------------------------------------------------------------------------- /packages/base/src/sync.ts: -------------------------------------------------------------------------------- 1 | export interface SyncRequest { 2 | channels?: SyncChannels 3 | webhooks?: Omit[] 4 | } 5 | 6 | export interface SyncResult { 7 | webhooks: SyncWebhook[] 8 | } 9 | 10 | export interface SyncSandboxRequest { 11 | name: string 12 | channels?: SyncChannels 13 | } 14 | 15 | export interface SyncChannels { 16 | [channel: string]: any 17 | } 18 | 19 | export interface SyncWebhook { 20 | url: string 21 | token?: string 22 | } 23 | -------------------------------------------------------------------------------- /packages/base/src/users.ts: -------------------------------------------------------------------------------- 1 | import { uuid } from './uuid' 2 | 3 | export interface User { 4 | id: uuid 5 | clientId: uuid 6 | data?: Record 7 | } 8 | -------------------------------------------------------------------------------- /packages/base/src/uuid.ts: -------------------------------------------------------------------------------- 1 | export type uuid = string 2 | -------------------------------------------------------------------------------- /packages/base/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // Manage references here 3 | "references": [], 4 | 5 | // Defaults (don't change this) 6 | "extends": "../../tsconfig.packages.json", 7 | "compilerOptions": { 8 | "rootDir": "src", 9 | "outDir": "dist", 10 | "tsBuildInfoFile": "dist/.tsbuildinfo", 11 | 12 | // Specific settings for npm package 13 | "target": "es6", 14 | "declaration": true, 15 | "sourceMap": false 16 | }, 17 | "exclude": ["dist", "node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /packages/board/.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@parcel/config-default", 3 | "transformers": { 4 | "*.{ts,tsx}": [ 5 | "@parcel/transformer-typescript-tsc" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/board/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@botpress/messaging-board", 3 | "version": "0.1.0", 4 | "source": "src/index.html", 5 | "license": "AGPL-3.0", 6 | "scripts": { 7 | "build": "yarn && yarn run -T parcel build", 8 | "watch": "yarn && yarn run -T parcel watch", 9 | "dev": "yarn && yarn run -T parcel" 10 | }, 11 | "devDependencies": { 12 | "@parcel/config-default": "^2.2.1", 13 | "@parcel/transformer-typescript-tsc": "^2.2.1", 14 | "@types/react": "^17.0.38", 15 | "@types/react-dom": "^17.0.11", 16 | "typescript": ">=3.0.0" 17 | }, 18 | "dependencies": { 19 | "@botpress/messaging-base": "1.2.0", 20 | "@botpress/messaging-client": "1.2.1", 21 | "@botpress/messaging-socket": "1.3.0", 22 | "@botpress/webchat": "0.5.2", 23 | "react": "^17.0.2", 24 | "react-dom": "^17.0.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/board/src/app.tsx: -------------------------------------------------------------------------------- 1 | import { ExposedWebChat, Config } from '@botpress/webchat' 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | 5 | // Uncomment this for the test webchat 6 | /* 7 | import { BoardLinker } from './linker' 8 | 9 | new BoardLinker( 10 | document.getElementById('board-linker')!, 11 | document.getElementById('webchat')!, 12 | document.getElementById('board-watcher')! 13 | ) 14 | */ 15 | 16 | const config: Partial = { 17 | messagingUrl: 'http://localhost:3100', 18 | clientId: 'c28082df-0582-4ace-94cf-e6e293593b63' 19 | } 20 | 21 | ReactDOM.render(, document.getElementById('oldwebchat')) 22 | -------------------------------------------------------------------------------- /packages/board/src/chat/base/system.ts: -------------------------------------------------------------------------------- 1 | export class WebchatSystem { 2 | async setup() {} 3 | } 4 | -------------------------------------------------------------------------------- /packages/board/src/chat/conversation/events.ts: -------------------------------------------------------------------------------- 1 | import { Conversation, Emitter, uuid } from '@botpress/messaging-base' 2 | 3 | export enum ConversationEvents { 4 | Choose = 'choose', 5 | Set = 'set' 6 | } 7 | 8 | export interface ConversationSetEvent { 9 | previous: Conversation | undefined 10 | value: Conversation | undefined 11 | } 12 | 13 | export interface ConversationChooseEvent { 14 | choice: uuid | undefined 15 | } 16 | 17 | export class ConversationEmitter extends Emitter<{ 18 | [ConversationEvents.Set]: ConversationSetEvent 19 | [ConversationEvents.Choose]: ConversationChooseEvent 20 | }> {} 21 | 22 | export type ConversationWatcher = Omit 23 | -------------------------------------------------------------------------------- /packages/board/src/chat/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base/system' 2 | export * from './conversation/events' 3 | export * from './conversation/system' 4 | export * from './lang/system' 5 | export * from './locale/system' 6 | export * from './messages/events' 7 | export * from './messages/system' 8 | export * from './storage/system' 9 | export * from './user/events' 10 | export * from './user/system' 11 | export * from './chat' 12 | -------------------------------------------------------------------------------- /packages/board/src/chat/locale/system.ts: -------------------------------------------------------------------------------- 1 | import { WebchatSystem } from '../base/system' 2 | 3 | export class WebchateLocale extends WebchatSystem { 4 | public current!: string 5 | 6 | constructor() { 7 | super() 8 | this.current = navigator.language 9 | } 10 | 11 | getFamily() { 12 | return this.current?.split('-')[0] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/board/src/chat/messages/events.ts: -------------------------------------------------------------------------------- 1 | import { Emitter, Message } from '@botpress/messaging-base' 2 | 3 | export enum MessagesEvents { 4 | Receive = 'receive', 5 | Send = 'send' 6 | } 7 | 8 | export class MessagesEmitter extends Emitter<{ 9 | [MessagesEvents.Receive]: Message[] 10 | [MessagesEvents.Send]: any 11 | }> {} 12 | 13 | export type MessagesWatcher = Omit 14 | -------------------------------------------------------------------------------- /packages/board/src/chat/storage/system.ts: -------------------------------------------------------------------------------- 1 | import { WebchatSystem } from '../base/system' 2 | 3 | export class WebchatStorage extends WebchatSystem { 4 | public get(key: string): T | undefined { 5 | const stored = localStorage.getItem(this.getKey(key)) 6 | if (!stored) { 7 | return undefined 8 | } 9 | 10 | try { 11 | const val = JSON.parse(stored) 12 | return val 13 | } catch { 14 | return undefined 15 | } 16 | } 17 | 18 | public set(key: string, object: T) { 19 | localStorage.setItem(this.getKey(key), JSON.stringify(object)) 20 | } 21 | 22 | private getKey(key: string) { 23 | return `bp-chat-${key}` 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/board/src/chat/user/events.ts: -------------------------------------------------------------------------------- 1 | import { Emitter } from '@botpress/messaging-base' 2 | import { UserCredentials } from '@botpress/messaging-socket' 3 | 4 | export enum UserEvents { 5 | Choose = 'choose', 6 | Set = 'set' 7 | } 8 | 9 | export interface UserSetEvent { 10 | previous: UserCredentials | undefined 11 | value: UserCredentials | undefined 12 | } 13 | 14 | export interface UserChooseEvent { 15 | choice: UserCredentials | undefined 16 | } 17 | 18 | export class UserEmitter extends Emitter<{ 19 | [UserEvents.Set]: UserSetEvent 20 | [UserEvents.Choose]: UserChooseEvent 21 | }> {} 22 | 23 | export type UserWatcher = Omit 24 | -------------------------------------------------------------------------------- /packages/board/src/skin/index.ts: -------------------------------------------------------------------------------- 1 | export * from './render' 2 | export * from './ui' 3 | -------------------------------------------------------------------------------- /packages/board/src/skin/lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "webchat": { 3 | "typeMessage": "Type a message", 4 | "send": "Send" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/board/src/skin/lang/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "webchat": { 3 | "typeMessage": "Entrer un message", 4 | "send": "Envoyer" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/board/src/skin/lang/index.ts: -------------------------------------------------------------------------------- 1 | import en from './en.json' 2 | import fr from './fr.json' 3 | 4 | export default { fr, en } 5 | -------------------------------------------------------------------------------- /packages/board/src/skin/ui.ts: -------------------------------------------------------------------------------- 1 | export const element = ( 2 | type: K, 3 | parent: N, 4 | construct?: (node: HTMLElementTagNameMap[K]) => void 5 | ) => { 6 | const node = document.createElement(type) 7 | 8 | try { 9 | construct?.(node) 10 | } catch (e) { 11 | node.appendChild(document.createTextNode(e)) 12 | } 13 | 14 | parent.appendChild(node) 15 | return node 16 | } 17 | 18 | export const text = (data: string | undefined, parent: N) => { 19 | const text = document.createTextNode(data || '') 20 | parent.appendChild(text) 21 | return text 22 | } 23 | -------------------------------------------------------------------------------- /packages/board/src/style.css: -------------------------------------------------------------------------------- 1 | .bp-send-button { 2 | margin-left: 5px; 3 | } 4 | 5 | .bp-messages-section { 6 | overflow: auto; 7 | height: 400px; 8 | margin-top: 24px; 9 | margin-bottom: 24px; 10 | } 11 | 12 | .bp-messages-section table { 13 | margin-top: 0; 14 | margin-bottom: 0; 15 | } 16 | 17 | /* Copied from simple css source. Forces dark theme colors */ 18 | :root { 19 | --bg: #212121; 20 | --accent-bg: #2b2b2b; 21 | --text: #dcdcdc; 22 | --text-light: #ababab; 23 | --border: #666; 24 | --accent: #ffb300; 25 | --accent-light: #ffecb3; 26 | --code: #f06292; 27 | --preformatted: #ccc; 28 | --disabled: #111; 29 | } 30 | -------------------------------------------------------------------------------- /packages/board/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // Manage references here 3 | "references": [ 4 | { 5 | "path": "../socket" 6 | }, 7 | { 8 | "path": "../client" 9 | }, 10 | { 11 | "path": "../webchat" 12 | } 13 | ], 14 | 15 | // Defaults (don't change this) 16 | "extends": "../../tsconfig.packages.json", 17 | "compilerOptions": { 18 | "rootDir": "src", 19 | "outDir": "dist", 20 | "tsBuildInfoFile": "dist/.tsbuildinfo", 21 | 22 | // Specific settings for frontend 23 | "target": "es6", 24 | "lib": ["dom", "es2021"], 25 | "jsx": "react", 26 | "resolveJsonModule": true 27 | }, 28 | "include": ["src", "src/**/*.json"], 29 | "exclude": ["dist", "node_modules"] 30 | } 31 | -------------------------------------------------------------------------------- /packages/channels/README.md: -------------------------------------------------------------------------------- 1 | # Botpress Messaging Channels Version 1.0.0 2 | 3 | ## Supported Channels 4 | 5 | - Facebook Messenger 6 | - Slack 7 | - Smooch 8 | - Teams 9 | - Telegram 10 | - Twilio 11 | - Vonage 12 | 13 | ## Development 14 | 15 | _Note: this documentation is for the **channel v1+ only**. For the doc on **legacy channel**, please checkout the `v1` branch and make your changes there._ 16 | 17 | **Steps:** 18 | 19 | 1. Make changes to one of the channel 20 | 1. Test those changes locally 21 | 1. When ready to deploy the new version of the channels, bump the version of this package to something higher than `v1`. 22 | 1. Use `yarn` to publish the updated package to NPM. See doc [here](../../docs/release.md) for reference. 23 | -------------------------------------------------------------------------------- /packages/channels/example/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "externalUrl": "", 3 | "scopes": { 4 | "CLIENT_ID": { 5 | "telegram": { 6 | "botToken": "" 7 | }, 8 | "twilio": { 9 | "accountSID": "", 10 | "authToken": "" 11 | }, 12 | "smooch": { 13 | "keyId": "", 14 | "secret": "" 15 | }, 16 | "teams": { 17 | "appId": "", 18 | "appPassword": "" 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/channels/example/index.ts: -------------------------------------------------------------------------------- 1 | import clc from 'cli-color' 2 | import express, { Router } from 'express' 3 | import { App } from './app' 4 | import config from './config.json' 5 | 6 | const setup = async () => { 7 | console.info('====================\n' + ` ${clc.magentaBright('channels example')}\n` + '====================') 8 | 9 | const exp = express() 10 | exp.get('/', (req, res) => { 11 | res.sendStatus(200) 12 | }) 13 | 14 | const router = Router() 15 | const app = new App(router, config) 16 | await app.setup() 17 | 18 | const port = 3100 19 | exp.use('/webhooks/v1', router) 20 | exp.listen(port) 21 | 22 | console.info(`${clc.cyan('url')} ${config.externalUrl}`) 23 | console.info(`${clc.cyan('port')} ${port}`) 24 | } 25 | 26 | void setup() 27 | -------------------------------------------------------------------------------- /packages/channels/example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.packages.json", 3 | "compilerOptions": { 4 | "rootDir": ".." 5 | }, 6 | "include": ["../test/**/*", "../src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/channels/src/base/config.ts: -------------------------------------------------------------------------------- 1 | export interface ChannelConfig {} 2 | -------------------------------------------------------------------------------- /packages/channels/src/base/context.ts: -------------------------------------------------------------------------------- 1 | import { Endpoint } from './endpoint' 2 | import { Logger } from './logger' 3 | import { ChannelState } from './service' 4 | 5 | export type ChannelContext> = { 6 | scope: string 7 | state: TState 8 | handlers: number 9 | payload: any 10 | logger?: Logger 11 | } & Endpoint 12 | 13 | export interface IndexChoiceOption { 14 | type: IndexChoiceType 15 | title: string 16 | value?: string 17 | } 18 | 19 | export enum IndexChoiceType { 20 | OpenUrl = 'open_url', 21 | PostBack = 'postback', 22 | SaySomething = 'say_something', 23 | QuickReply = 'quick_reply' 24 | } 25 | -------------------------------------------------------------------------------- /packages/channels/src/base/emitter.ts: -------------------------------------------------------------------------------- 1 | export class Emitter { 2 | private listeners: { [eventId: number]: (((arg: any) => Promise) | ((arg: any) => void))[] } = {} 3 | 4 | public on(event: K, listener: ((arg: T[K]) => Promise) | ((arg: T[K]) => void)) { 5 | const listeners = this.listeners[event as number] 6 | if (!listeners) { 7 | this.listeners[event as number] = [listener] 8 | } else { 9 | listeners.push(listener) 10 | } 11 | } 12 | 13 | protected async emit(event: K, arg: T[K]): Promise { 14 | const listeners = this.listeners[event as number] 15 | if (listeners?.length) { 16 | for (const listener of listeners) { 17 | await listener(arg) 18 | } 19 | return true 20 | } else { 21 | return false 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/channels/src/base/endpoint.ts: -------------------------------------------------------------------------------- 1 | export interface Endpoint { 2 | identity: string 3 | sender: string 4 | thread: string 5 | } 6 | -------------------------------------------------------------------------------- /packages/channels/src/base/kvs.ts: -------------------------------------------------------------------------------- 1 | export interface Kvs { 2 | get(key: string): Promise 3 | set(key: string, value: any): Promise 4 | } 5 | -------------------------------------------------------------------------------- /packages/channels/src/base/logger.ts: -------------------------------------------------------------------------------- 1 | export interface Logger { 2 | info(message: string, data?: any): void 3 | debug(message: string, data?: any): void 4 | warn(message: string, data?: any): void 5 | error(error: Error | undefined | unknown, message?: string, data?: any): void 6 | } 7 | -------------------------------------------------------------------------------- /packages/channels/src/base/meta.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | 3 | export interface ChannelMeta { 4 | id: string 5 | name: string 6 | version: string 7 | initiable: boolean 8 | lazy: boolean 9 | schema: { [field: string]: Joi.Schema } 10 | } 11 | -------------------------------------------------------------------------------- /packages/channels/src/base/renderer.ts: -------------------------------------------------------------------------------- 1 | import { ChannelContext } from './context' 2 | 3 | export interface ChannelRenderer> { 4 | priority: number 5 | 6 | handles(context: TContext): boolean 7 | render(context: TContext): void 8 | } 9 | -------------------------------------------------------------------------------- /packages/channels/src/base/renderers/audio.ts: -------------------------------------------------------------------------------- 1 | import { ChannelRenderer } from '../../base/renderer' 2 | import { AudioContent } from '../../content/types' 3 | import { ChannelContext } from '../context' 4 | 5 | export abstract class AudioRenderer implements ChannelRenderer { 6 | get priority(): number { 7 | return 0 8 | } 9 | 10 | handles(context: ChannelContext): boolean { 11 | const payload = context.payload as AudioContent 12 | return !!payload.audio 13 | } 14 | 15 | render(context: ChannelContext) { 16 | const payload = context.payload as AudioContent 17 | this.renderAudio(context, payload) 18 | } 19 | 20 | abstract renderAudio(context: ChannelContext, payload: AudioContent): void 21 | } 22 | -------------------------------------------------------------------------------- /packages/channels/src/base/renderers/card.ts: -------------------------------------------------------------------------------- 1 | import { ChannelRenderer } from '../../base/renderer' 2 | import { CardContent } from '../../content/types' 3 | import { ChannelContext } from '../context' 4 | 5 | export class CardToCarouselRenderer implements ChannelRenderer> { 6 | get priority(): number { 7 | return -1 8 | } 9 | 10 | handles(context: ChannelContext): boolean { 11 | return context.payload.type === 'card' 12 | } 13 | 14 | render(context: ChannelContext) { 15 | const payload = context.payload as CardContent 16 | 17 | // we convert our card to a carousel 18 | context.payload = context.payload = { 19 | type: 'carousel', 20 | items: [payload] 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/channels/src/base/renderers/choices.ts: -------------------------------------------------------------------------------- 1 | import { ChannelRenderer } from '../../base/renderer' 2 | import { ChoiceContent } from '../../content/types' 3 | import { ChannelContext } from '../context' 4 | 5 | export abstract class ChoicesRenderer implements ChannelRenderer { 6 | get priority(): number { 7 | return 1 8 | } 9 | 10 | handles(context: ChannelContext): boolean { 11 | const payload = context.payload as ChoiceContent 12 | return !!payload.choices?.length 13 | } 14 | 15 | render(context: ChannelContext) { 16 | const payload = context.payload as ChoiceContent 17 | this.renderChoice(context, payload) 18 | } 19 | 20 | abstract renderChoice(context: ChannelContext, payload: ChoiceContent): void 21 | } 22 | -------------------------------------------------------------------------------- /packages/channels/src/base/renderers/dropdown.ts: -------------------------------------------------------------------------------- 1 | import { ChannelRenderer } from '../../base/renderer' 2 | import { ChannelContext } from '../context' 3 | 4 | export class DropdownToChoicesRenderer implements ChannelRenderer> { 5 | get priority(): number { 6 | return -1 7 | } 8 | 9 | handles(context: ChannelContext): boolean { 10 | return !!context.payload.options?.length 11 | } 12 | 13 | render(context: ChannelContext) { 14 | const payload = context.payload // as DropdownContent 15 | 16 | // we convert our dropdown to choices 17 | context.payload = { 18 | type: 'single-choice', 19 | text: payload.message, 20 | choices: payload.options.map((x: any) => ({ title: x.label, value: x.value })) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/channels/src/base/renderers/file.ts: -------------------------------------------------------------------------------- 1 | import { ChannelRenderer } from '../../base/renderer' 2 | import { FileContent } from '../../content/types' 3 | import { ChannelContext } from '../context' 4 | 5 | export abstract class FileRenderer implements ChannelRenderer { 6 | get priority(): number { 7 | return 0 8 | } 9 | 10 | handles(context: ChannelContext): boolean { 11 | const payload = context.payload as FileContent 12 | return !!payload.file 13 | } 14 | 15 | render(context: ChannelContext) { 16 | const payload = context.payload as FileContent 17 | this.renderFile(context, payload) 18 | } 19 | 20 | abstract renderFile(context: ChannelContext, payload: FileContent): void 21 | } 22 | -------------------------------------------------------------------------------- /packages/channels/src/base/renderers/image.ts: -------------------------------------------------------------------------------- 1 | import { ChannelRenderer } from '../../base/renderer' 2 | import { ImageContent } from '../../content/types' 3 | import { ChannelContext } from '../context' 4 | 5 | export abstract class ImageRenderer implements ChannelRenderer { 6 | get priority(): number { 7 | return 0 8 | } 9 | 10 | handles(context: ChannelContext): boolean { 11 | const payload = context.payload as ImageContent 12 | return !!payload.image 13 | } 14 | 15 | render(context: ChannelContext) { 16 | const payload = context.payload as ImageContent 17 | this.renderImage(context, payload) 18 | } 19 | 20 | abstract renderImage(context: ChannelContext, payload: ImageContent): void 21 | } 22 | -------------------------------------------------------------------------------- /packages/channels/src/base/renderers/location.ts: -------------------------------------------------------------------------------- 1 | import { ChannelRenderer } from '../../base/renderer' 2 | import { LocationContent } from '../../content/types' 3 | import { ChannelContext } from '../context' 4 | 5 | export abstract class LocationRenderer implements ChannelRenderer { 6 | get priority(): number { 7 | return 0 8 | } 9 | 10 | handles(context: ChannelContext): boolean { 11 | const payload = context.payload as LocationContent 12 | return !!payload.latitude && !!payload.longitude 13 | } 14 | 15 | render(context: ChannelContext) { 16 | const payload = context.payload as LocationContent 17 | this.renderLocation(context, payload) 18 | } 19 | 20 | abstract renderLocation(context: ChannelContext, payload: LocationContent): void 21 | } 22 | -------------------------------------------------------------------------------- /packages/channels/src/base/renderers/text.ts: -------------------------------------------------------------------------------- 1 | import { ChannelRenderer } from '../../base/renderer' 2 | import { TextContent } from '../../content/types' 3 | import { ChannelContext } from '../context' 4 | 5 | export abstract class TextRenderer implements ChannelRenderer { 6 | get priority(): number { 7 | return 0 8 | } 9 | 10 | handles(context: ChannelContext): boolean { 11 | const payload = context.payload as TextContent 12 | return !!payload.text 13 | } 14 | 15 | render(context: ChannelContext) { 16 | const payload = context.payload as TextContent 17 | this.renderText(context, payload) 18 | } 19 | 20 | abstract renderText(context: ChannelContext, payload: TextContent): void 21 | } 22 | -------------------------------------------------------------------------------- /packages/channels/src/base/renderers/video.ts: -------------------------------------------------------------------------------- 1 | import { VideoContent } from '../../content/types' 2 | import { ChannelContext } from '../context' 3 | import { ChannelRenderer } from '../renderer' 4 | 5 | export abstract class VideoRenderer implements ChannelRenderer { 6 | get priority(): number { 7 | return 0 8 | } 9 | 10 | handles(context: ChannelContext): boolean { 11 | const payload = context.payload as VideoContent 12 | return !!payload.video 13 | } 14 | 15 | render(context: ChannelContext) { 16 | const payload = context.payload as VideoContent 17 | this.renderVideo(context, payload) 18 | } 19 | 20 | abstract renderVideo(context: ChannelContext, payload: VideoContent): void 21 | } 22 | -------------------------------------------------------------------------------- /packages/channels/src/base/sender.ts: -------------------------------------------------------------------------------- 1 | import { ChannelContext } from './context' 2 | 3 | export interface ChannelSender> { 4 | priority: number 5 | 6 | handles(context: TContext): boolean 7 | send(context: TContext): Promise 8 | } 9 | -------------------------------------------------------------------------------- /packages/channels/src/base/senders/common.ts: -------------------------------------------------------------------------------- 1 | import { ChannelSender } from '../../base/sender' 2 | import { ChannelContext } from '../context' 3 | 4 | export abstract class CommonSender implements ChannelSender { 5 | get priority(): number { 6 | return 0 7 | } 8 | 9 | handles(context: ChannelContext): boolean { 10 | return context.handlers > 0 11 | } 12 | 13 | async send(context: ChannelContext) {} 14 | } 15 | -------------------------------------------------------------------------------- /packages/channels/src/base/senders/typing.ts: -------------------------------------------------------------------------------- 1 | import { ChannelSender } from '../../base/sender' 2 | import { ChannelContext } from '../context' 3 | 4 | export class TypingSender implements ChannelSender { 5 | get priority(): number { 6 | return -1 7 | } 8 | 9 | handles(context: ChannelContext): boolean { 10 | const typing = context.payload.typing 11 | return context.handlers > 0 && typing === true 12 | } 13 | 14 | async send(context: ChannelContext): Promise { 15 | await this.sendIndicator(context) 16 | 17 | const delay = context.payload.delay ?? 1000 18 | await new Promise((resolve) => setTimeout(resolve, delay)) 19 | 20 | await this.stopIndicator(context) 21 | } 22 | 23 | async sendIndicator(context: ChannelContext): Promise {} 24 | async stopIndicator(context: ChannelContext): Promise {} 25 | } 26 | -------------------------------------------------------------------------------- /packages/channels/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base/channel' 2 | export * from './base/endpoint' 3 | export * from './messenger/channel' 4 | export * from './slack/channel' 5 | export * from './smooch/channel' 6 | export * from './teams/channel' 7 | export * from './telegram/channel' 8 | export * from './twilio/channel' 9 | export * from './vonage/channel' 10 | -------------------------------------------------------------------------------- /packages/channels/src/messenger/channel.ts: -------------------------------------------------------------------------------- 1 | import { ChannelTemplate } from '../base/channel' 2 | import { MessengerApi } from './api' 3 | import { MessengerConfig, MessengerConfigSchema } from './config' 4 | import { MessengerService } from './service' 5 | import { MessengerStream } from './stream' 6 | 7 | export class MessengerChannel extends ChannelTemplate< 8 | MessengerConfig, 9 | MessengerService, 10 | MessengerApi, 11 | MessengerStream 12 | > { 13 | get meta() { 14 | return { 15 | id: 'aa88f73d-a9fb-456f-b0d0-5c0031e4aa34', 16 | name: 'messenger', 17 | version: '1.0.0', 18 | schema: MessengerConfigSchema, 19 | initiable: true, 20 | lazy: true 21 | } 22 | } 23 | 24 | constructor() { 25 | const service = new MessengerService() 26 | super(service, new MessengerApi(service), new MessengerStream(service)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/channels/src/messenger/config.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | import { ChannelConfig } from '../base/config' 3 | 4 | export interface MessengerConfig extends ChannelConfig { 5 | appId: string 6 | appSecret: string 7 | verifyToken: string 8 | pageId: string 9 | accessToken: string 10 | } 11 | 12 | export const MessengerConfigSchema = { 13 | appId: Joi.string().required(), 14 | appSecret: Joi.string().required(), 15 | verifyToken: Joi.string().required(), 16 | pageId: Joi.string().required(), 17 | accessToken: Joi.string().required() 18 | } 19 | -------------------------------------------------------------------------------- /packages/channels/src/messenger/context.ts: -------------------------------------------------------------------------------- 1 | import { ChannelContext } from '../base/context' 2 | import { MessengerState } from './service' 3 | import { MessengerStream } from './stream' 4 | 5 | export type MessengerContext = ChannelContext & { 6 | messages: any[] 7 | stream: MessengerStream 8 | } 9 | -------------------------------------------------------------------------------- /packages/channels/src/messenger/renderers/audio.ts: -------------------------------------------------------------------------------- 1 | import { AudioRenderer } from '../../base/renderers/audio' 2 | import { AudioContent } from '../../content/types' 3 | import { MessengerContext } from '../context' 4 | 5 | export class MessengerAudioRenderer extends AudioRenderer { 6 | renderAudio(context: MessengerContext, payload: AudioContent) { 7 | context.messages.push({ 8 | attachment: { 9 | type: 'audio', 10 | payload: { 11 | is_reusable: true, 12 | url: payload.audio 13 | } 14 | } 15 | }) 16 | 17 | if (payload.title?.length) { 18 | context.messages.push({ text: payload.title }) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/channels/src/messenger/renderers/choices.ts: -------------------------------------------------------------------------------- 1 | import { ChoicesRenderer } from '../../base/renderers/choices' 2 | import { ChoiceContent } from '../../content/types' 3 | import { MessengerContext } from '../context' 4 | 5 | export class MessengerChoicesRenderer extends ChoicesRenderer { 6 | renderChoice(context: MessengerContext, payload: ChoiceContent): void { 7 | const message = context.messages[0] 8 | 9 | message.quick_replies = payload.choices.map((c) => ({ 10 | content_type: 'text', 11 | title: c.title, 12 | payload: c.value 13 | })) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/channels/src/messenger/renderers/file.ts: -------------------------------------------------------------------------------- 1 | import { FileRenderer } from '../../base/renderers/file' 2 | import { FileContent } from '../../content/types' 3 | import { MessengerContext } from '../context' 4 | 5 | export class MessengerFileRenderer extends FileRenderer { 6 | renderFile(context: MessengerContext, payload: FileContent) { 7 | context.messages.push({ text: `${payload.title ? `${payload.title}\n` : payload.title}${payload.file}` }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/channels/src/messenger/renderers/image.ts: -------------------------------------------------------------------------------- 1 | import { ImageRenderer } from '../../base/renderers/image' 2 | import { ImageContent } from '../../content/types' 3 | import { MessengerContext } from '../context' 4 | 5 | export class MessengerImageRenderer extends ImageRenderer { 6 | renderImage(context: MessengerContext, payload: ImageContent): void { 7 | context.messages.push({ 8 | attachment: { 9 | type: 'image', 10 | payload: { 11 | is_reusable: true, 12 | url: payload.image 13 | } 14 | } 15 | }) 16 | 17 | if (payload.title?.length) { 18 | // TODO: could maybe use the media templat instead? 19 | context.messages.push({ text: payload.title }) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/channels/src/messenger/renderers/index.ts: -------------------------------------------------------------------------------- 1 | import { MessengerAudioRenderer } from './audio' 2 | import { MessengerCarouselRenderer } from './carousel' 3 | import { MessengerChoicesRenderer } from './choices' 4 | import { MessengerFileRenderer } from './file' 5 | import { MessengerImageRenderer } from './image' 6 | import { MessengerLocationRenderer } from './location' 7 | import { MessengerTextRenderer } from './text' 8 | import { MessengerVideoRenderer } from './video' 9 | 10 | export const MessengerRenderers = [ 11 | new MessengerTextRenderer(), 12 | new MessengerImageRenderer(), 13 | new MessengerCarouselRenderer(), 14 | new MessengerChoicesRenderer(), 15 | new MessengerFileRenderer(), 16 | new MessengerAudioRenderer(), 17 | new MessengerVideoRenderer(), 18 | new MessengerLocationRenderer() 19 | ] 20 | -------------------------------------------------------------------------------- /packages/channels/src/messenger/renderers/location.ts: -------------------------------------------------------------------------------- 1 | import { LocationRenderer } from '../../base/renderers/location' 2 | import { LocationContent } from '../../content/types' 3 | import { MessengerContext } from '../context' 4 | 5 | export class MessengerLocationRenderer extends LocationRenderer { 6 | renderLocation(context: MessengerContext, payload: LocationContent) { 7 | const googleMapsLink = `https://www.google.com/maps/search/?api=1&query=${payload.latitude},${payload.longitude}` 8 | 9 | context.messages.push({ 10 | text: `${payload.title}${payload.address ? `\n${payload.address}` : ''}\n${googleMapsLink}` 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/channels/src/messenger/renderers/text.ts: -------------------------------------------------------------------------------- 1 | import { TextRenderer } from '../../base/renderers/text' 2 | import { TextContent } from '../../content/types' 3 | import { MessengerContext } from '../context' 4 | 5 | export class MessengerTextRenderer extends TextRenderer { 6 | renderText(context: MessengerContext, payload: TextContent): void { 7 | context.messages.push({ text: payload.text }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/channels/src/messenger/renderers/video.ts: -------------------------------------------------------------------------------- 1 | import { VideoRenderer } from '../../base/renderers/video' 2 | import { VideoContent } from '../../content/types' 3 | import { MessengerContext } from '../context' 4 | 5 | export class MessengerVideoRenderer extends VideoRenderer { 6 | renderVideo(context: MessengerContext, payload: VideoContent) { 7 | context.messages.push({ 8 | attachment: { 9 | type: 'video', 10 | payload: { 11 | is_reusable: true, 12 | url: payload.video 13 | } 14 | } 15 | }) 16 | 17 | if (payload.title?.length) { 18 | context.messages.push({ text: payload.title }) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/channels/src/messenger/senders/common.ts: -------------------------------------------------------------------------------- 1 | import { CommonSender } from '../../base/senders/common' 2 | import { MessengerContext } from '../context' 3 | 4 | export class MessengerCommonSender extends CommonSender { 5 | async send(context: MessengerContext) { 6 | for (const message of context.messages) { 7 | await context.stream.sendMessage(context.scope, context, message) 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/channels/src/messenger/senders/index.ts: -------------------------------------------------------------------------------- 1 | import { MessengerCommonSender } from './common' 2 | import { MessengerTypingSender } from './typing' 3 | 4 | export const MessengerSenders = [new MessengerTypingSender(), new MessengerCommonSender()] 5 | -------------------------------------------------------------------------------- /packages/channels/src/messenger/senders/typing.ts: -------------------------------------------------------------------------------- 1 | import { TypingSender } from '../../base/senders/typing' 2 | import { MessengerContext } from '../context' 3 | 4 | export class MessengerTypingSender extends TypingSender { 5 | async sendIndicator(context: MessengerContext) { 6 | await context.stream.sendAction(context.scope, context, 'typing_on') 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/channels/src/messenger/service.ts: -------------------------------------------------------------------------------- 1 | import { ChannelService, ChannelState } from '../base/service' 2 | import { MessengerConfig } from './config' 3 | 4 | export interface MessengerState extends ChannelState {} 5 | 6 | export class MessengerService extends ChannelService { 7 | async create(scope: string, config: MessengerConfig) { 8 | return { 9 | config 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/channels/src/slack/channel.ts: -------------------------------------------------------------------------------- 1 | import { ChannelTemplate } from '../base/channel' 2 | import { SlackApi } from './api' 3 | import { SlackConfig, SlackConfigSchema } from './config' 4 | import { SlackService } from './service' 5 | import { SlackStream } from './stream' 6 | 7 | export class SlackChannel extends ChannelTemplate { 8 | get meta() { 9 | return { 10 | id: 'a3551758-a03f-4a68-97f2-4536a8805b52', 11 | name: 'slack', 12 | version: '1.0.0', 13 | schema: SlackConfigSchema, 14 | initiable: false, 15 | lazy: true 16 | } 17 | } 18 | 19 | constructor() { 20 | const service = new SlackService() 21 | super(service, new SlackApi(service), new SlackStream(service)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/channels/src/slack/config.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | import { ChannelConfig } from '../base/config' 3 | 4 | export interface SlackConfig extends ChannelConfig { 5 | botToken: string 6 | signingSecret: string 7 | } 8 | 9 | export const SlackConfigSchema = { 10 | botToken: Joi.string().required(), 11 | signingSecret: Joi.string().required() 12 | } 13 | -------------------------------------------------------------------------------- /packages/channels/src/slack/context.ts: -------------------------------------------------------------------------------- 1 | import { Block, KnownBlock } from '@slack/bolt' 2 | import { ChatPostMessageArguments } from '@slack/web-api' 3 | import { ChannelContext } from '../base/context' 4 | import { SlackState } from './service' 5 | 6 | export type SlackContext = ChannelContext & { 7 | message: ChatPostMessageArguments & { blocks: (KnownBlock | Block)[] } 8 | } 9 | -------------------------------------------------------------------------------- /packages/channels/src/slack/renderers/audio.ts: -------------------------------------------------------------------------------- 1 | import { AudioRenderer } from '../../base/renderers/audio' 2 | import { AudioContent } from '../../content/types' 3 | import { SlackContext } from '../context' 4 | 5 | export class SlackAudioRenderer extends AudioRenderer { 6 | renderAudio(context: SlackContext, payload: AudioContent) { 7 | context.message.blocks.push({ 8 | type: 'section', 9 | text: { type: 'mrkdwn', text: `<${payload.audio}|${payload.title || payload.audio}>` } 10 | }) 11 | 12 | context.message.text = payload.title || payload.audio 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/channels/src/slack/renderers/choices.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid' 2 | import { ChoicesRenderer } from '../../base/renderers/choices' 3 | import { ChoiceContent } from '../../content/types' 4 | import { SlackContext } from '../context' 5 | 6 | export const QUICK_REPLY_PREFIX = 'quick_reply::' 7 | 8 | export class SlackChoicesRenderer extends ChoicesRenderer { 9 | renderChoice(context: SlackContext, payload: ChoiceContent) { 10 | context.message.blocks.push({ 11 | type: 'actions', 12 | elements: payload.choices.map((x) => ({ 13 | type: 'button', 14 | action_id: `${QUICK_REPLY_PREFIX}${uuidv4()}`, 15 | text: { 16 | type: 'plain_text', 17 | text: x.title 18 | }, 19 | value: x.value 20 | })) 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/channels/src/slack/renderers/file.ts: -------------------------------------------------------------------------------- 1 | import { FileRenderer } from '../../base/renderers/file' 2 | import { FileContent } from '../../content/types' 3 | import { SlackContext } from '../context' 4 | 5 | export class SlackFileRenderer extends FileRenderer { 6 | renderFile(context: SlackContext, payload: FileContent) { 7 | context.message.blocks.push({ 8 | type: 'section', 9 | text: { type: 'mrkdwn', text: `<${payload.file}|${payload.title || payload.file}>` } 10 | }) 11 | 12 | context.message.text = payload.title || payload.file 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/channels/src/slack/renderers/image.ts: -------------------------------------------------------------------------------- 1 | import { ImageRenderer } from '../../base/renderers/image' 2 | import { ImageContent } from '../../content/types' 3 | import { SlackContext } from '../context' 4 | 5 | export class SlackImageRenderer extends ImageRenderer { 6 | renderImage(context: SlackContext, payload: ImageContent) { 7 | context.message.blocks.push({ 8 | type: 'image', 9 | title: payload.title 10 | ? { 11 | type: 'plain_text', 12 | text: payload.title 13 | } 14 | : undefined, 15 | image_url: payload.image, 16 | alt_text: 'image' 17 | }) 18 | 19 | if (payload.title) { 20 | context.message.text = payload.title 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/channels/src/slack/renderers/index.ts: -------------------------------------------------------------------------------- 1 | import { SlackAudioRenderer } from './audio' 2 | import { SlackCarouselRenderer } from './carousel' 3 | import { SlackChoicesRenderer } from './choices' 4 | import { SlackDropdownRenderer } from './dropdown' 5 | import { SlackFileRenderer } from './file' 6 | import { SlackImageRenderer } from './image' 7 | import { SlackLocationRenderer } from './location' 8 | import { SlackTextRenderer } from './text' 9 | import { SlackVideoRenderer } from './video' 10 | 11 | export const SlackRenderers = [ 12 | new SlackDropdownRenderer(), 13 | new SlackTextRenderer(), 14 | new SlackImageRenderer(), 15 | new SlackCarouselRenderer(), 16 | new SlackChoicesRenderer(), 17 | new SlackFileRenderer(), 18 | new SlackAudioRenderer(), 19 | new SlackVideoRenderer(), 20 | new SlackLocationRenderer() 21 | ] 22 | -------------------------------------------------------------------------------- /packages/channels/src/slack/renderers/location.ts: -------------------------------------------------------------------------------- 1 | import { LocationRenderer } from '../../base/renderers/location' 2 | import { LocationContent } from '../../content/types' 3 | import { SlackContext } from '../context' 4 | 5 | export class SlackLocationRenderer extends LocationRenderer { 6 | renderLocation(context: SlackContext, payload: LocationContent) { 7 | const googleMapsLink = `https://www.google.com/maps/search/?api=1&query=${payload.latitude},${payload.longitude}` 8 | 9 | context.message.blocks.push({ 10 | type: 'section', 11 | text: { 12 | type: 'mrkdwn', 13 | text: `${payload.title ? `${payload.title}\n` : ''}<${googleMapsLink}|${payload.address || googleMapsLink}>` 14 | } 15 | }) 16 | 17 | context.message.text = payload.title || payload.address || googleMapsLink 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/channels/src/slack/renderers/text.ts: -------------------------------------------------------------------------------- 1 | import { TextRenderer } from '../../base/renderers/text' 2 | import { TextContent } from '../../content/types' 3 | import { SlackContext } from '../context' 4 | 5 | export class SlackTextRenderer extends TextRenderer { 6 | renderText(context: SlackContext, payload: TextContent) { 7 | context.message.blocks.push({ 8 | type: 'section', 9 | text: { type: payload.markdown ? 'mrkdwn' : 'plain_text', text: payload.text } 10 | }) 11 | 12 | context.message.text = payload.text 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/channels/src/slack/renderers/video.ts: -------------------------------------------------------------------------------- 1 | import { VideoRenderer } from '../../base/renderers/video' 2 | import { VideoContent } from '../../content/types' 3 | import { SlackContext } from '../context' 4 | 5 | export class SlackVideoRenderer extends VideoRenderer { 6 | renderVideo(context: SlackContext, payload: VideoContent) { 7 | context.message.blocks.push({ 8 | type: 'section', 9 | text: { type: 'mrkdwn', text: `<${payload.video}|${payload.title || payload.video}>` } 10 | }) 11 | 12 | context.message.text = payload.title || payload.video 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/channels/src/slack/senders/common.ts: -------------------------------------------------------------------------------- 1 | import { CommonSender } from '../../base/senders/common' 2 | import { SlackContext } from '../context' 3 | 4 | export class SlackCommonSender extends CommonSender { 5 | async send(context: SlackContext) { 6 | await context.state.app.client.chat.postMessage({ 7 | ...context.message, 8 | text: context.message.text || context.payload.type 9 | }) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/channels/src/slack/senders/index.ts: -------------------------------------------------------------------------------- 1 | import { SlackCommonSender } from './common' 2 | 3 | export const SlackSenders = [new SlackCommonSender()] 4 | -------------------------------------------------------------------------------- /packages/channels/src/slack/service.ts: -------------------------------------------------------------------------------- 1 | import { App } from '@slack/bolt' 2 | import { ChannelService, ChannelState } from '../base/service' 3 | import { SlackConfig } from './config' 4 | 5 | export interface SlackState extends ChannelState { 6 | app: App 7 | } 8 | 9 | export class SlackService extends ChannelService { 10 | async create(scope: string, config: SlackConfig) { 11 | return { 12 | config, 13 | app: new App({ 14 | signingSecret: config.signingSecret, 15 | token: config.botToken, 16 | tokenVerificationEnabled: false, 17 | // We send a fake receiver here because we implement a receiver 18 | // that works completely different than what bolt expects 19 | receiver: { init: () => {} } as any 20 | }) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/channels/src/smooch/channel.ts: -------------------------------------------------------------------------------- 1 | import { ChannelTemplate } from '../base/channel' 2 | import { SmoochApi } from './api' 3 | import { SmoochConfig, SmoochConfigSchema } from './config' 4 | import { SmoochService } from './service' 5 | import { SmoochStream } from './stream' 6 | 7 | export class SmoochChannel extends ChannelTemplate { 8 | get meta() { 9 | return { 10 | id: '82c7a7ee-f1c9-4fb6-8306-18f03d6aadc9', 11 | name: 'smooch', 12 | version: '1.0.0', 13 | schema: SmoochConfigSchema, 14 | initiable: true, 15 | lazy: true 16 | } 17 | } 18 | 19 | constructor() { 20 | const service = new SmoochService() 21 | super(service, new SmoochApi(service), new SmoochStream(service)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/channels/src/smooch/config.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | import { ChannelConfig } from '../base/config' 3 | 4 | export interface SmoochConfig extends ChannelConfig { 5 | appId: string 6 | keyId: string 7 | keySecret: string 8 | webhookSecret: string 9 | } 10 | 11 | export const SmoochConfigSchema = { 12 | appId: Joi.string().required(), 13 | keyId: Joi.string().required(), 14 | keySecret: Joi.string().required(), 15 | webhookSecret: Joi.string().required() 16 | } 17 | -------------------------------------------------------------------------------- /packages/channels/src/smooch/context.ts: -------------------------------------------------------------------------------- 1 | import { ChannelContext } from '../base/context' 2 | import { SmoochState } from './service' 3 | import { SmoochContent } from './smooch' 4 | 5 | export type SmoochContext = ChannelContext & { 6 | messages: SmoochContent[] 7 | } 8 | -------------------------------------------------------------------------------- /packages/channels/src/smooch/renderers/audio.ts: -------------------------------------------------------------------------------- 1 | import { AudioRenderer } from '../../base/renderers/audio' 2 | import { AudioContent } from '../../content/types' 3 | import { SmoochContext } from '../context' 4 | 5 | export class SmoochAudioRenderer extends AudioRenderer { 6 | renderAudio(context: SmoochContext, payload: AudioContent) { 7 | context.messages.push({ type: 'file', text: payload.title, mediaUrl: payload.audio }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/channels/src/smooch/renderers/choices.ts: -------------------------------------------------------------------------------- 1 | import { ChoicesRenderer } from '../../base/renderers/choices' 2 | import { ChoiceContent } from '../../content/types' 3 | import { SmoochContext } from '../context' 4 | 5 | export class SmoochChoicesRenderer extends ChoicesRenderer { 6 | renderChoice(context: SmoochContext, payload: ChoiceContent): void { 7 | const message = context.messages[0] 8 | message.actions = payload.choices.map((r) => ({ type: 'reply', text: r.title, payload: r.value })) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/channels/src/smooch/renderers/file.ts: -------------------------------------------------------------------------------- 1 | import { FileRenderer } from '../../base/renderers/file' 2 | import { FileContent } from '../../content/types' 3 | import { SmoochContext } from '../context' 4 | 5 | export class SmoochFileRenderer extends FileRenderer { 6 | renderFile(context: SmoochContext, payload: FileContent) { 7 | context.messages.push({ type: 'file', text: payload.title, mediaUrl: payload.file }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/channels/src/smooch/renderers/image.ts: -------------------------------------------------------------------------------- 1 | import { ImageRenderer } from '../../base/renderers/image' 2 | import { ImageContent } from '../../content/types' 3 | import { SmoochContext } from '../context' 4 | 5 | export class SmoochImageRenderer extends ImageRenderer { 6 | renderImage(context: SmoochContext, payload: ImageContent): void { 7 | context.messages.push({ type: 'image', mediaUrl: payload.image, text: payload.title }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/channels/src/smooch/renderers/index.ts: -------------------------------------------------------------------------------- 1 | import { SmoochAudioRenderer } from './audio' 2 | import { SmoochCarouselRenderer } from './carousel' 3 | import { SmoochChoicesRenderer } from './choices' 4 | import { SmoochFileRenderer } from './file' 5 | import { SmoochImageRenderer } from './image' 6 | import { SmoochLocationRenderer } from './location' 7 | import { SmoochTextRenderer } from './text' 8 | import { SmoochVideoRenderer } from './video' 9 | 10 | export const SmoochRenderers = [ 11 | new SmoochTextRenderer(), 12 | new SmoochImageRenderer(), 13 | new SmoochChoicesRenderer(), 14 | new SmoochCarouselRenderer(), 15 | new SmoochFileRenderer(), 16 | new SmoochAudioRenderer(), 17 | new SmoochVideoRenderer(), 18 | new SmoochLocationRenderer() 19 | ] 20 | -------------------------------------------------------------------------------- /packages/channels/src/smooch/renderers/location.ts: -------------------------------------------------------------------------------- 1 | import { LocationRenderer } from '../../base/renderers/location' 2 | import { LocationContent } from '../../content/types' 3 | import { SmoochContext } from '../context' 4 | 5 | export class SmoochLocationRenderer extends LocationRenderer { 6 | renderLocation(context: SmoochContext, payload: LocationContent) { 7 | context.messages.push({ 8 | type: 'location', 9 | coordinates: { 10 | lat: payload.latitude, 11 | long: payload.longitude 12 | }, 13 | location: { 14 | address: payload.address, 15 | name: payload.title 16 | } 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/channels/src/smooch/renderers/text.ts: -------------------------------------------------------------------------------- 1 | import { TextRenderer } from '../../base/renderers/text' 2 | import { TextContent } from '../../content/types' 3 | import { SmoochContext } from '../context' 4 | 5 | export class SmoochTextRenderer extends TextRenderer { 6 | renderText(context: SmoochContext, payload: TextContent): void { 7 | context.messages.push({ 8 | type: 'text', 9 | text: payload.text 10 | }) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/channels/src/smooch/renderers/video.ts: -------------------------------------------------------------------------------- 1 | import { VideoRenderer } from '../../base/renderers/video' 2 | import { VideoContent } from '../../content/types' 3 | import { SmoochContext } from '../context' 4 | 5 | export class SmoochVideoRenderer extends VideoRenderer { 6 | renderVideo(context: SmoochContext, payload: VideoContent) { 7 | context.messages.push({ type: 'file', text: payload.title, mediaUrl: payload.video }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/channels/src/smooch/senders/common.ts: -------------------------------------------------------------------------------- 1 | import { CommonSender } from '../../base/senders/common' 2 | import { SmoochContext } from '../context' 3 | const SunshineConversationsClient = require('sunshine-conversations-client') 4 | 5 | export class SmoochCommonSender extends CommonSender { 6 | async send(context: SmoochContext) { 7 | for (const message of context.messages) { 8 | const data = new SunshineConversationsClient.MessagePost() 9 | data.author = { 10 | type: 'business' 11 | } 12 | data.content = message 13 | 14 | await context.state.smooch.messages.postMessage(context.state.config.appId, context.thread, data) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/channels/src/smooch/senders/index.ts: -------------------------------------------------------------------------------- 1 | import { SmoochCommonSender } from './common' 2 | import { SmoochTypingSender } from './typing' 3 | 4 | export const SmoochSenders = [new SmoochTypingSender(), new SmoochCommonSender()] 5 | -------------------------------------------------------------------------------- /packages/channels/src/smooch/senders/typing.ts: -------------------------------------------------------------------------------- 1 | import { TypingSender } from '../../base/senders/typing' 2 | import { SmoochContext } from '../context' 3 | const SunshineConversationsClient = require('sunshine-conversations-client') 4 | 5 | export class SmoochTypingSender extends TypingSender { 6 | async sendIndicator(context: SmoochContext) { 7 | const data = new SunshineConversationsClient.ActivityPost() 8 | data.author = { 9 | type: 'business' 10 | } 11 | data.type = 'typing:start' 12 | 13 | await context.state.smooch.activity.postActivity(context.state.config.appId, context.thread, data) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/channels/src/teams/channel.ts: -------------------------------------------------------------------------------- 1 | import { ChannelTemplate } from '../base/channel' 2 | import { TeamsApi } from './api' 3 | import { TeamsConfig, TeamsConfigSchema } from './config' 4 | import { TeamsService } from './service' 5 | import { TeamsStream } from './stream' 6 | 7 | export class TeamsChannel extends ChannelTemplate { 8 | get meta() { 9 | return { 10 | id: '39525c14-738f-4db8-b73b-1f0edb36ad7c', 11 | name: 'teams', 12 | version: '1.0.0', 13 | schema: TeamsConfigSchema, 14 | initiable: false, 15 | lazy: true 16 | } 17 | } 18 | 19 | constructor() { 20 | const service = new TeamsService() 21 | super(service, new TeamsApi(service), new TeamsStream(service)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/channels/src/teams/config.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | import { ChannelConfig } from '../base/config' 3 | 4 | export interface TeamsConfig extends ChannelConfig { 5 | appId: string 6 | appPassword: string 7 | tenantId: string 8 | } 9 | 10 | export const TeamsConfigSchema = { 11 | appId: Joi.string().required(), 12 | appPassword: Joi.string().required(), 13 | tenantId: Joi.string().optional() 14 | } 15 | -------------------------------------------------------------------------------- /packages/channels/src/teams/context.ts: -------------------------------------------------------------------------------- 1 | import { Activity, ConversationReference } from 'botbuilder' 2 | import { ChannelContext } from '../base/context' 3 | import { TeamsState } from './service' 4 | 5 | export type TeamsContext = ChannelContext & { 6 | messages: Partial[] 7 | convoRef: Partial 8 | } 9 | -------------------------------------------------------------------------------- /packages/channels/src/teams/renderers/audio.ts: -------------------------------------------------------------------------------- 1 | import { AudioRenderer } from '../../base/renderers/audio' 2 | import { AudioContent } from '../../content/types' 3 | import { TeamsContext } from '../context' 4 | 5 | export class TeamsAudioRenderer extends AudioRenderer { 6 | renderAudio(context: TeamsContext, payload: AudioContent) { 7 | context.messages.push({ 8 | text: `${payload.title ? `${payload.title} ` : ''}${payload.audio}` 9 | }) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/channels/src/teams/renderers/file.ts: -------------------------------------------------------------------------------- 1 | import { FileRenderer } from '../../base/renderers/file' 2 | import { FileContent } from '../../content/types' 3 | import { TeamsContext } from '../context' 4 | 5 | export class TeamsFileRenderer extends FileRenderer { 6 | renderFile(context: TeamsContext, payload: FileContent) { 7 | context.messages.push({ 8 | text: `${payload.title ? `${payload.title} ` : ''}${payload.file}` 9 | }) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/channels/src/teams/renderers/image.ts: -------------------------------------------------------------------------------- 1 | import { CardFactory } from 'botbuilder' 2 | import { ImageRenderer } from '../../base/renderers/image' 3 | import { ImageContent } from '../../content/types' 4 | import { TeamsContext } from '../context' 5 | 6 | export class TeamsImageRenderer extends ImageRenderer { 7 | renderImage(context: TeamsContext, payload: ImageContent) { 8 | context.messages.push({ 9 | attachments: [CardFactory.heroCard(payload.title!, CardFactory.images([payload.image]))] 10 | }) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/channels/src/teams/renderers/index.ts: -------------------------------------------------------------------------------- 1 | import { TeamsAudioRenderer } from './audio' 2 | import { TeamsCarouselRenderer } from './carousel' 3 | import { TeamsChoicesRenderer } from './choices' 4 | import { TeamsDropdownRenderer } from './dropdown' 5 | import { TeamsFileRenderer } from './file' 6 | import { TeamsImageRenderer } from './image' 7 | import { TeamsLocationRenderer } from './location' 8 | import { TeamsTextRenderer } from './text' 9 | import { TeamsVideoRenderer } from './video' 10 | 11 | export const TeamsRenderers = [ 12 | new TeamsTextRenderer(), 13 | new TeamsImageRenderer(), 14 | new TeamsCarouselRenderer(), 15 | new TeamsDropdownRenderer(), 16 | new TeamsChoicesRenderer(), 17 | new TeamsFileRenderer(), 18 | new TeamsAudioRenderer(), 19 | new TeamsVideoRenderer(), 20 | new TeamsLocationRenderer() 21 | ] 22 | -------------------------------------------------------------------------------- /packages/channels/src/teams/renderers/location.ts: -------------------------------------------------------------------------------- 1 | import { LocationRenderer } from '../../base/renderers/location' 2 | import { LocationContent } from '../../content/types' 3 | import { TeamsContext } from '../context' 4 | 5 | export class TeamsLocationRenderer extends LocationRenderer { 6 | renderLocation(context: TeamsContext, payload: LocationContent) { 7 | const googleMapsLink = `https://www.google.com/maps/search/?api=1&query=${payload.latitude},${payload.longitude}` 8 | 9 | context.messages.push({ 10 | text: `${payload.title}${payload.address ? ` ${payload.address}` : ' '} ${googleMapsLink}` 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/channels/src/teams/renderers/text.ts: -------------------------------------------------------------------------------- 1 | import { TextRenderer } from '../../base/renderers/text' 2 | import { TextContent } from '../../content/types' 3 | import { TeamsContext } from '../context' 4 | 5 | export class TeamsTextRenderer extends TextRenderer { 6 | renderText(context: TeamsContext, payload: TextContent) { 7 | context.messages.push({ text: payload.text }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/channels/src/teams/renderers/video.ts: -------------------------------------------------------------------------------- 1 | import { VideoRenderer } from '../../base/renderers/video' 2 | import { VideoContent } from '../../content/types' 3 | import { TeamsContext } from '../context' 4 | 5 | export class TeamsVideoRenderer extends VideoRenderer { 6 | renderVideo(context: TeamsContext, payload: VideoContent) { 7 | context.messages.push({ 8 | text: `${payload.title ? `${payload.title} ` : ''}${payload.video}` 9 | }) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/channels/src/teams/senders/common.ts: -------------------------------------------------------------------------------- 1 | import { TurnContext } from 'botbuilder' 2 | import { CommonSender } from '../../base/senders/common' 3 | import { TeamsContext } from '../context' 4 | 5 | export class TeamsCommonSender extends CommonSender { 6 | async send(context: TeamsContext) { 7 | for (const message of context.messages) { 8 | await context.state.adapter.continueConversation(context.convoRef, async (turnContext: TurnContext) => { 9 | await turnContext.sendActivity(message) 10 | }) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/channels/src/teams/senders/index.ts: -------------------------------------------------------------------------------- 1 | import { TeamsCommonSender } from './common' 2 | import { TeamsTypingSender } from './typing' 3 | 4 | export const TeamsSenders = [new TeamsTypingSender(), new TeamsCommonSender()] 5 | -------------------------------------------------------------------------------- /packages/channels/src/teams/senders/typing.ts: -------------------------------------------------------------------------------- 1 | import { TurnContext } from 'botbuilder' 2 | import { TypingSender } from '../../base/senders/typing' 3 | import { TeamsContext } from '../context' 4 | 5 | export class TeamsTypingSender extends TypingSender { 6 | async sendIndicator(context: TeamsContext) { 7 | await context.state.adapter.continueConversation(context.convoRef, async (turnContext: TurnContext) => { 8 | await turnContext.sendActivity({ type: 'typing' }) 9 | }) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/channels/src/telegram/channel.ts: -------------------------------------------------------------------------------- 1 | import { ChannelTemplate } from '../base/channel' 2 | import { TelegramApi } from './api' 3 | import { TelegramConfig, TelegramConfigSchema } from './config' 4 | import { TelegramService } from './service' 5 | import { TelegramStream } from './stream' 6 | 7 | export class TelegramChannel extends ChannelTemplate { 8 | get meta() { 9 | return { 10 | id: 'e578723f-ab57-463c-bc13-b483db9bf547', 11 | name: 'telegram', 12 | version: '1.0.0', 13 | schema: TelegramConfigSchema, 14 | initiable: true, 15 | lazy: true 16 | } 17 | } 18 | 19 | constructor() { 20 | const service = new TelegramService() 21 | super(service, new TelegramApi(service), new TelegramStream(service)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/channels/src/telegram/config.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | 3 | export interface TelegramConfig { 4 | botToken: string 5 | } 6 | 7 | export const TelegramConfigSchema = { 8 | botToken: Joi.string().required() 9 | } 10 | -------------------------------------------------------------------------------- /packages/channels/src/telegram/context.ts: -------------------------------------------------------------------------------- 1 | import { ChatAction, InputFile } from 'telegraf/typings/core/types/typegram' 2 | import { ChannelContext } from '../base/context' 3 | import { TelegramState } from './service' 4 | 5 | export type TelegramContext = ChannelContext & { 6 | messages: TelegramMessage[] 7 | } 8 | 9 | export interface TelegramMessage { 10 | text?: string 11 | animation?: string 12 | photo?: InputFile 13 | markdown?: boolean 14 | action?: ChatAction 15 | document?: InputFile 16 | audio?: InputFile 17 | video?: InputFile 18 | location?: { 19 | latitude: number 20 | longitude: number 21 | } 22 | extra?: any 23 | } 24 | -------------------------------------------------------------------------------- /packages/channels/src/telegram/renderers/audio.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { AudioRenderer } from '../../base/renderers/audio' 3 | import { AudioContent } from '../../content/types' 4 | import { TelegramContext } from '../context' 5 | 6 | export class TelegramAudioRenderer extends AudioRenderer { 7 | renderAudio(context: TelegramContext, payload: AudioContent) { 8 | context.messages.push({ 9 | document: { url: payload.audio, filename: path.basename(payload.audio) }, 10 | extra: { caption: payload.title } 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/channels/src/telegram/renderers/choices.ts: -------------------------------------------------------------------------------- 1 | import { Markup } from 'telegraf' 2 | import { ChoicesRenderer } from '../../base/renderers/choices' 3 | import { ChoiceContent } from '../../content/types' 4 | import { TelegramContext } from '../context' 5 | 6 | export class TelegramChoicesRenderer extends ChoicesRenderer { 7 | renderChoice(context: TelegramContext, payload: ChoiceContent) { 8 | if (!context.messages.length) { 9 | context.messages.push({}) 10 | } 11 | 12 | const buttons = payload.choices.map((x) => Markup.button.callback(x.title, x.value)) 13 | context.messages[0].extra = { ...context.messages[0].extra, ...Markup.keyboard(buttons).oneTime() } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/channels/src/telegram/renderers/file.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { FileRenderer } from '../../base/renderers/file' 3 | import { FileContent } from '../../content/types' 4 | import { TelegramContext } from '../context' 5 | 6 | export class TelegramFileRenderer extends FileRenderer { 7 | renderFile(context: TelegramContext, payload: FileContent) { 8 | context.messages.push({ 9 | document: { url: payload.file, filename: path.basename(payload.file) }, 10 | extra: { caption: payload.title } 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/channels/src/telegram/renderers/image.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { ImageRenderer } from '../../base/renderers/image' 3 | import { ImageContent } from '../../content/types' 4 | import { TelegramContext } from '../context' 5 | 6 | export class TelegramImageRenderer extends ImageRenderer { 7 | renderImage(context: TelegramContext, payload: ImageContent) { 8 | if (payload.image.toLowerCase().endsWith('.gif')) { 9 | context.messages.push({ animation: payload.image, extra: { caption: payload.title } }) 10 | } else { 11 | context.messages.push({ 12 | photo: { url: payload.image, filename: path.basename(payload.image) }, 13 | extra: { caption: payload.title } 14 | }) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/channels/src/telegram/renderers/index.ts: -------------------------------------------------------------------------------- 1 | import { TelegramAudioRenderer } from './audio' 2 | import { TelegramCarouselRenderer } from './carousel' 3 | import { TelegramChoicesRenderer } from './choices' 4 | import { TelegramFileRenderer } from './file' 5 | import { TelegramImageRenderer } from './image' 6 | import { TelegramLocationRenderer } from './location' 7 | import { TelegramTextRenderer } from './text' 8 | import { TelegramVideoRenderer } from './video' 9 | 10 | export const TelegramRenderers = [ 11 | new TelegramTextRenderer(), 12 | new TelegramImageRenderer(), 13 | new TelegramCarouselRenderer(), 14 | new TelegramChoicesRenderer(), 15 | new TelegramFileRenderer(), 16 | new TelegramAudioRenderer(), 17 | new TelegramVideoRenderer(), 18 | new TelegramLocationRenderer() 19 | ] 20 | -------------------------------------------------------------------------------- /packages/channels/src/telegram/renderers/location.ts: -------------------------------------------------------------------------------- 1 | import { LocationRenderer } from '../../base/renderers/location' 2 | import { LocationContent } from '../../content/types' 3 | import { TelegramContext } from '../context' 4 | 5 | export class TelegramLocationRenderer extends LocationRenderer { 6 | renderLocation(context: TelegramContext, payload: LocationContent) { 7 | context.messages.push({ 8 | location: { latitude: payload.latitude, longitude: payload.longitude } 9 | // For some reason this does not work, so we need to send a seperate text message 10 | // extra: { caption: payload.title } 11 | }) 12 | 13 | let text = payload.title 14 | if (payload.address) { 15 | text = (text ? `*${text}*\n` : '') + payload.address 16 | } 17 | 18 | if (payload.title) { 19 | context.messages.push({ text, extra: { parse_mode: 'Markdown' } }) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/channels/src/telegram/renderers/text.ts: -------------------------------------------------------------------------------- 1 | import { TextRenderer } from '../../base/renderers/text' 2 | import { TextContent } from '../../content/types' 3 | import { TelegramContext } from '../context' 4 | 5 | export class TelegramTextRenderer extends TextRenderer { 6 | renderText(context: TelegramContext, payload: TextContent) { 7 | context.messages.push({ 8 | text: payload.text, 9 | markdown: payload.markdown, 10 | extra: payload.markdown ? { parse_mode: 'Markdown' } : {} 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/channels/src/telegram/renderers/video.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { VideoRenderer } from '../../base/renderers/video' 3 | import { VideoContent } from '../../content/types' 4 | import { TelegramContext } from '../context' 5 | 6 | export class TelegramVideoRenderer extends VideoRenderer { 7 | renderVideo(context: TelegramContext, payload: VideoContent) { 8 | context.messages.push({ 9 | document: { url: payload.video, filename: path.basename(payload.video) }, 10 | extra: { caption: payload.title } 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/channels/src/telegram/senders/index.ts: -------------------------------------------------------------------------------- 1 | import { TelegramCommonSender } from './common' 2 | import { TelegramTypingSender } from './typing' 3 | 4 | export const TelegramSenders = [new TelegramTypingSender(), new TelegramCommonSender()] 5 | -------------------------------------------------------------------------------- /packages/channels/src/telegram/senders/typing.ts: -------------------------------------------------------------------------------- 1 | import { TypingSender } from '../../base/senders/typing' 2 | import { TelegramContext } from '../context' 3 | 4 | export class TelegramTypingSender extends TypingSender { 5 | async sendIndicator(context: TelegramContext) { 6 | await context.state.telegraf.telegram.sendChatAction(context.thread, 'typing') 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/channels/src/telegram/service.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express' 2 | import { Telegraf } from 'telegraf' 3 | import { ChannelService, ChannelState } from '../base/service' 4 | import { TelegramConfig } from './config' 5 | 6 | export interface TelegramState extends ChannelState { 7 | telegraf: Telegraf 8 | callback?: (req: Request, res: Response) => any 9 | } 10 | 11 | export class TelegramService extends ChannelService { 12 | async create(scope: string, config: TelegramConfig) { 13 | const telegraf = new Telegraf(config.botToken) 14 | 15 | return { 16 | config, 17 | telegraf 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/channels/src/twilio/channel.ts: -------------------------------------------------------------------------------- 1 | import { ChannelTemplate } from '../base/channel' 2 | import { TwilioApi } from './api' 3 | import { TwilioConfig, TwilioConfigSchema } from './config' 4 | import { TwilioService } from './service' 5 | import { TwilioStream } from './stream' 6 | 7 | export class TwilioChannel extends ChannelTemplate { 8 | get meta() { 9 | return { 10 | id: 'a711e325-7e71-4955-a76c-b46e62cdebd7', 11 | name: 'twilio', 12 | version: '1.0.0', 13 | schema: TwilioConfigSchema, 14 | initiable: false, 15 | lazy: true 16 | } 17 | } 18 | 19 | constructor() { 20 | const service = new TwilioService() 21 | super(service, new TwilioApi(service), new TwilioStream(service)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/channels/src/twilio/config.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | import { ChannelConfig } from '../base/config' 3 | 4 | export interface TwilioConfig extends ChannelConfig { 5 | accountSID: string 6 | authToken: string 7 | messageDelay?: string 8 | retryMaxAttempts?: number 9 | retryDelay?: string 10 | } 11 | 12 | export const TwilioConfigSchema = { 13 | accountSID: Joi.string().regex(/^AC.*/).required(), 14 | authToken: Joi.string().required(), 15 | messageDelay: Joi.string().optional(), 16 | retryMaxAttempts: Joi.number().optional(), 17 | retryDelay: Joi.string().optional() 18 | } 19 | -------------------------------------------------------------------------------- /packages/channels/src/twilio/context.ts: -------------------------------------------------------------------------------- 1 | import { MessageListInstanceCreateOptions } from 'twilio/lib/rest/api/v2010/account/message' 2 | import { ChannelContext, IndexChoiceOption } from '../base/context' 3 | import { TwilioState } from './service' 4 | 5 | export type TwilioContext = ChannelContext & { 6 | messages: Partial[] 7 | prepareIndexResponse(scope: string, identity: string, sender: string, options: IndexChoiceOption[]): void 8 | } 9 | -------------------------------------------------------------------------------- /packages/channels/src/twilio/renderers/audio.ts: -------------------------------------------------------------------------------- 1 | import { AudioRenderer } from '../../base/renderers/audio' 2 | import { AudioContent } from '../../content/types' 3 | import { TwilioContext } from '../context' 4 | 5 | export class TwilioAudioRenderer extends AudioRenderer { 6 | renderAudio(context: TwilioContext, payload: AudioContent) { 7 | context.messages.push({ body: payload.title, mediaUrl: payload.audio }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/channels/src/twilio/renderers/choices.ts: -------------------------------------------------------------------------------- 1 | import { IndexChoiceType } from '../../base/context' 2 | import { ChoicesRenderer } from '../../base/renderers/choices' 3 | import { ChoiceContent, ChoiceOption } from '../../content/types' 4 | import { TwilioContext } from '../context' 5 | 6 | export class TwilioChoicesRenderer extends ChoicesRenderer { 7 | renderChoice(context: TwilioContext, payload: ChoiceContent) { 8 | if (!context.messages.length) { 9 | context.messages.push({}) 10 | } 11 | 12 | const message = context.messages[0] 13 | 14 | message.body = `${message.body || ''}\n\n${payload.choices 15 | .map(({ title }, idx) => `${idx + 1}. ${title}`) 16 | .join('\n')}` 17 | 18 | context.prepareIndexResponse( 19 | context.scope, 20 | context.identity, 21 | context.sender, 22 | payload.choices.map((x: ChoiceOption) => ({ ...x, type: IndexChoiceType.QuickReply })) 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/channels/src/twilio/renderers/file.ts: -------------------------------------------------------------------------------- 1 | import { FileRenderer } from '../../base/renderers/file' 2 | import { FileContent } from '../../content/types' 3 | import { TwilioContext } from '../context' 4 | 5 | export class TwilioFileRenderer extends FileRenderer { 6 | renderFile(context: TwilioContext, payload: FileContent) { 7 | context.messages.push({ body: `${payload.title ? `${payload.title}\n` : payload.title}${payload.file}` }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/channels/src/twilio/renderers/image.ts: -------------------------------------------------------------------------------- 1 | import { ImageRenderer } from '../../base/renderers/image' 2 | import { ImageContent } from '../../content/types' 3 | import { TwilioContext } from '../context' 4 | 5 | export class TwilioImageRenderer extends ImageRenderer { 6 | renderImage(context: TwilioContext, payload: ImageContent) { 7 | context.messages.push({ body: payload.title, mediaUrl: payload.image }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/channels/src/twilio/renderers/index.ts: -------------------------------------------------------------------------------- 1 | import { TwilioAudioRenderer } from './audio' 2 | import { TwilioCarouselRenderer } from './carousel' 3 | import { TwilioChoicesRenderer } from './choices' 4 | import { TwilioFileRenderer } from './file' 5 | import { TwilioImageRenderer } from './image' 6 | import { TwilioLocationRenderer } from './location' 7 | import { TwilioTextRenderer } from './text' 8 | import { TwilioVideoRenderer } from './video' 9 | 10 | export const TwilioRenderers = [ 11 | new TwilioTextRenderer(), 12 | new TwilioImageRenderer(), 13 | new TwilioCarouselRenderer(), 14 | new TwilioChoicesRenderer(), 15 | new TwilioFileRenderer(), 16 | new TwilioAudioRenderer(), 17 | new TwilioVideoRenderer(), 18 | new TwilioLocationRenderer() 19 | ] 20 | -------------------------------------------------------------------------------- /packages/channels/src/twilio/renderers/location.ts: -------------------------------------------------------------------------------- 1 | import { LocationRenderer } from '../../base/renderers/location' 2 | import { LocationContent } from '../../content/types' 3 | import { TwilioContext } from '../context' 4 | 5 | export class TwilioLocationRenderer extends LocationRenderer { 6 | renderLocation(context: TwilioContext, payload: LocationContent) { 7 | const googleMapsLink = `https://www.google.com/maps/search/?api=1&query=${payload.latitude},${payload.longitude}` 8 | 9 | context.messages.push({ 10 | body: `${payload.title}${payload.address ? `\n${payload.address}` : ''}\n${googleMapsLink}` 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/channels/src/twilio/renderers/text.ts: -------------------------------------------------------------------------------- 1 | import { TextRenderer } from '../../base/renderers/text' 2 | import { TextContent } from '../../content/types' 3 | import { TwilioContext } from '../context' 4 | 5 | export class TwilioTextRenderer extends TextRenderer { 6 | renderText(context: TwilioContext, payload: TextContent) { 7 | context.messages.push({ body: payload.text }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/channels/src/twilio/renderers/video.ts: -------------------------------------------------------------------------------- 1 | import { VideoRenderer } from '../../base/renderers/video' 2 | import { VideoContent } from '../../content/types' 3 | import { TwilioContext } from '../context' 4 | 5 | export class TwilioVideoRenderer extends VideoRenderer { 6 | renderVideo(context: TwilioContext, payload: VideoContent) { 7 | context.messages.push({ body: `${payload.title ? `${payload.title}\n` : payload.title}${payload.video}` }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/channels/src/twilio/senders/index.ts: -------------------------------------------------------------------------------- 1 | import { TwilioCommonSender } from './common' 2 | 3 | export const TwilioSenders = [new TwilioCommonSender()] 4 | -------------------------------------------------------------------------------- /packages/channels/src/twilio/service.ts: -------------------------------------------------------------------------------- 1 | import { Twilio } from 'twilio' 2 | import { ChannelService, ChannelState } from '../base/service' 3 | import { TwilioConfig } from './config' 4 | 5 | export interface TwilioState extends ChannelState { 6 | twilio: Twilio 7 | } 8 | 9 | export class TwilioService extends ChannelService { 10 | async create(scope: string, config: TwilioConfig) { 11 | return { 12 | config, 13 | twilio: new Twilio(config.accountSID, config.authToken) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/channels/src/typings/ext-name.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'ext-name' 2 | -------------------------------------------------------------------------------- /packages/channels/src/vonage/channel.ts: -------------------------------------------------------------------------------- 1 | import { ChannelTemplate } from '../base/channel' 2 | import { VonageApi } from './api' 3 | import { VonageConfig, VonageConfigSchema } from './config' 4 | import { VonageService } from './service' 5 | import { VonageStream } from './stream' 6 | 7 | export class VonageChannel extends ChannelTemplate { 8 | get meta() { 9 | return { 10 | id: 'd6073ed2-5603-4f5b-bcef-0a4bc75ef113', 11 | name: 'vonage', 12 | version: '1.0.0', 13 | schema: VonageConfigSchema, 14 | initiable: false, 15 | lazy: true 16 | } 17 | } 18 | 19 | constructor() { 20 | const service = new VonageService() 21 | super(service, new VonageApi(service), new VonageStream(service)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/channels/src/vonage/config.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | import { ChannelConfig } from '../base/config' 3 | 4 | export interface VonageConfig extends ChannelConfig { 5 | apiKey: string 6 | apiSecret: string 7 | signatureSecret: string 8 | useTestingApi?: boolean 9 | } 10 | 11 | export const VonageConfigSchema = { 12 | apiKey: Joi.string().required(), 13 | apiSecret: Joi.string().required(), 14 | signatureSecret: Joi.string().required(), 15 | useTestingApi: Joi.boolean().optional() 16 | } 17 | -------------------------------------------------------------------------------- /packages/channels/src/vonage/context.ts: -------------------------------------------------------------------------------- 1 | import { ChannelContext, IndexChoiceOption } from '../base/context' 2 | import { VonageState } from './service' 3 | 4 | export type VonageContext = ChannelContext & { 5 | messages: any[] 6 | prepareIndexResponse(scope: string, identity: string, sender: string, options: IndexChoiceOption[]): void 7 | } 8 | -------------------------------------------------------------------------------- /packages/channels/src/vonage/renderers/audio.ts: -------------------------------------------------------------------------------- 1 | import { AudioRenderer } from '../../base/renderers/audio' 2 | import { AudioContent } from '../../content/types' 3 | import { VonageContext } from '../context' 4 | 5 | export class VonageAudioRenderer extends AudioRenderer { 6 | renderAudio(context: VonageContext, payload: AudioContent) { 7 | context.messages.push({ message_type: 'audio', audio: { url: payload.audio } }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/channels/src/vonage/renderers/file.ts: -------------------------------------------------------------------------------- 1 | import { FileRenderer } from '../../base/renderers/file' 2 | import { FileContent } from '../../content/types' 3 | import { VonageContext } from '../context' 4 | 5 | export class VonageFileRenderer extends FileRenderer { 6 | renderFile(context: VonageContext, payload: FileContent) { 7 | context.messages.push({ message_type: 'file', file: { url: payload.file, caption: payload.title } }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/channels/src/vonage/renderers/image.ts: -------------------------------------------------------------------------------- 1 | import { ImageRenderer } from '../../base/renderers/image' 2 | import { ImageContent } from '../../content/types' 3 | import { VonageContext } from '../context' 4 | 5 | export class VonageImageRenderer extends ImageRenderer { 6 | renderImage(context: VonageContext, payload: ImageContent) { 7 | context.messages.push({ 8 | message_type: 'image', 9 | image: { 10 | url: payload.image, 11 | caption: payload.title 12 | } 13 | }) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/channels/src/vonage/renderers/location.ts: -------------------------------------------------------------------------------- 1 | import { LocationRenderer } from '../../base/renderers/location' 2 | import { LocationContent } from '../../content/types' 3 | import { VonageContext } from '../context' 4 | 5 | export class VonageLocationRenderer extends LocationRenderer { 6 | renderLocation(context: VonageContext, payload: LocationContent) { 7 | context.messages.push({ 8 | message_type: 'custom', 9 | custom: { 10 | type: 'location', 11 | location: { 12 | latitude: payload.latitude, 13 | longitude: payload.longitude, 14 | name: payload.title, 15 | address: payload.address 16 | } 17 | } 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/channels/src/vonage/renderers/template.ts: -------------------------------------------------------------------------------- 1 | import { ChannelRenderer } from '../../base/renderer' 2 | import { VonageContext } from '../context' 3 | 4 | export class VonageTemplateRenderer implements ChannelRenderer { 5 | get priority(): number { 6 | return 0 7 | } 8 | 9 | handles(context: VonageContext): boolean { 10 | return context.payload.type === 'vonage-template' 11 | } 12 | 13 | async render(context: VonageContext) { 14 | const payload = context.payload 15 | 16 | context.messages.push({ 17 | message_type: 'template', 18 | whatsapp: { policy: 'deterministic', locale: payload.languageCode || 'en_US' }, 19 | template: { 20 | name: `${payload.namespace}:${payload.name}`, 21 | parameters: payload.parameters 22 | } 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/channels/src/vonage/renderers/text.ts: -------------------------------------------------------------------------------- 1 | import { TextRenderer } from '../../base/renderers/text' 2 | import { TextContent } from '../../content/types' 3 | import { VonageContext } from '../context' 4 | 5 | export class VonageTextRenderer extends TextRenderer { 6 | renderText(context: VonageContext, payload: TextContent): void { 7 | context.messages.push({ message_type: 'text', text: payload.text }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/channels/src/vonage/renderers/video.ts: -------------------------------------------------------------------------------- 1 | import { VideoRenderer } from '../../base/renderers/video' 2 | import { VideoContent } from '../../content/types' 3 | import { VonageContext } from '../context' 4 | 5 | export class VonageVideoRenderer extends VideoRenderer { 6 | renderVideo(context: VonageContext, payload: VideoContent) { 7 | context.messages.push({ message_type: 'video', video: { url: payload.video, caption: payload.title } }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/channels/src/vonage/senders/index.ts: -------------------------------------------------------------------------------- 1 | import { VonageCommonSender } from './common' 2 | 3 | export const VonageSenders = [new VonageCommonSender()] 4 | -------------------------------------------------------------------------------- /packages/channels/src/vonage/service.ts: -------------------------------------------------------------------------------- 1 | import { ChannelService, ChannelState } from '../base/service' 2 | import { VonageConfig } from './config' 3 | 4 | export interface VonageState extends ChannelState {} 5 | 6 | export class VonageService extends ChannelService { 7 | async create(scope: string, config: VonageConfig) { 8 | return { 9 | config 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/channels/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // Manage references here 3 | "references": [], 4 | 5 | // Defaults (don't change this) 6 | "extends": "../../tsconfig.packages.json", 7 | "compilerOptions": { 8 | "rootDir": "src", 9 | "outDir": "dist", 10 | "tsBuildInfoFile": "dist/.tsbuildinfo" 11 | }, 12 | "include": ["src"], 13 | "exclude": ["dist", "node_modules"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/client/src/auth.ts: -------------------------------------------------------------------------------- 1 | export interface MessagingClientAuth { 2 | /** Token to send requests */ 3 | clientToken?: string 4 | /** Token to validate webhook requests */ 5 | webhookToken?: string 6 | } 7 | -------------------------------------------------------------------------------- /packages/client/src/errors.ts: -------------------------------------------------------------------------------- 1 | export const handleNotFound = async U>(func: F, returnValue: T): Promise => { 2 | try { 3 | return await func() 4 | } catch (err: any) { 5 | if (err?.response?.status === 404) { 6 | return returnValue 7 | } else { 8 | throw err 9 | } 10 | } 11 | } 12 | 13 | export const handleUnauthorized = async U>( 14 | func: F, 15 | returnValue: T 16 | ): Promise => { 17 | try { 18 | return await func() 19 | } catch (err: any) { 20 | if (err?.response?.status === 401) { 21 | return returnValue 22 | } else { 23 | throw err 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/client/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@botpress/messaging-base' 2 | export * from './auth' 3 | export * from './channel' 4 | export * from './client' 5 | export * from './errors' 6 | export * from './events' 7 | export * from './options' 8 | export * from './schema' 9 | -------------------------------------------------------------------------------- /packages/client/src/logger.ts: -------------------------------------------------------------------------------- 1 | /** Interface for a logger that can be used to get better debugging */ 2 | export interface Logger { 3 | info(message: string, data?: any): void 4 | debug(message: string, data?: any): void 5 | warn(message: string, data?: any): void 6 | error(error: Error | undefined | unknown, message?: string, data?: any): void 7 | } 8 | -------------------------------------------------------------------------------- /packages/client/src/options.ts: -------------------------------------------------------------------------------- 1 | import { uuid } from '@botpress/messaging-base' 2 | import { AxiosRequestConfig } from 'axios' 3 | import { MessagingClientAuth } from './auth' 4 | import { Logger } from './logger' 5 | 6 | export interface MessagingChannelOptions { 7 | /** Base url of the messaging server */ 8 | url: string 9 | /** Key to access admin routes. Optional */ 10 | adminKey?: string 11 | /** A custom axios config giving more control over the HTTP client used internally. Optional */ 12 | axios?: Omit 13 | /** Optional logger interface that can be used to get better debugging */ 14 | logger?: Logger 15 | /** Name of the cookie for sticky sessions */ 16 | sessionCookieName?: string 17 | } 18 | 19 | export interface MessagingOptions extends Omit, MessagingClientAuth { 20 | /** Messaging client id */ 21 | clientId: uuid 22 | } 23 | -------------------------------------------------------------------------------- /packages/client/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.packages.json", 3 | "compilerOptions": { 4 | "rootDir": "..", 5 | "paths": { 6 | "@botpress/messaging-base": ["../../base/src/index.ts"] 7 | } 8 | }, 9 | "include": ["../test/**/*", "../src/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // Manage references here 3 | "references": [ 4 | { 5 | "path": "../base" 6 | } 7 | ], 8 | 9 | // Defaults (don't change this) 10 | "extends": "../../tsconfig.packages.json", 11 | "compilerOptions": { 12 | "rootDir": "src", 13 | "outDir": "dist", 14 | "tsBuildInfoFile": "dist/.tsbuildinfo", 15 | 16 | // Specific settings for npm package 17 | "target": "es6", 18 | "declaration": true, 19 | "sourceMap": false 20 | }, 21 | "include": ["src"], 22 | "exclude": ["dist", "node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/components/README.md: -------------------------------------------------------------------------------- 1 | # Botpress Messaging Components 2 | 3 | A set of React Components to display Messaging content-types 4 | 5 | ## Usage 6 | 7 | ```typescript 8 | import React from 'react' 9 | import ReactDOM from 'react-dom' 10 | import ReactMessageRenderer, { defaultMessageConfig } from '@botpress/messaging-components' 11 | 12 | const messageContent = { 13 | type: 'text', 14 | text: 'Hello World!' 15 | } 16 | 17 | ReactDOM.render( 18 | , 19 | document.getElementById('root') 20 | ) 21 | ``` 22 | 23 | ## Development 24 | 25 | Build: 26 | 27 | ```sh 28 | $ yarn build 29 | ``` 30 | 31 | Run Tests: 32 | 33 | ```sh 34 | $ yarn test 35 | ``` 36 | 37 | View and play with components in Storybook: 38 | 39 | ```sh 40 | $ yarn storybook 41 | ``` 42 | -------------------------------------------------------------------------------- /packages/components/src/global.d.ts: -------------------------------------------------------------------------------- 1 | import { StudioConnector } from './typings' 2 | 3 | declare global { 4 | export interface Window { 5 | botpress?: StudioConnector 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/components/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import { Content, MessageType } from './content-typings' 3 | import defaultRenderer, { Renderer } from './renderer' 4 | import { Keyboard } from './renderer/keyboard' 5 | import { MessageConfig } from './typings' 6 | import { defaultMessageConfig } from './utils' 7 | 8 | export interface ReactMessageRendererProps { 9 | content: Content 10 | config: MessageConfig 11 | renderer?: Renderer 12 | } 13 | 14 | const ReactMessageRenderer: FC = ({ content, config, renderer = defaultRenderer }) => { 15 | return <>{renderer.render({ content, config })} 16 | } 17 | 18 | export default ReactMessageRenderer 19 | 20 | export { defaultRenderer, defaultMessageConfig, Content, MessageType, MessageConfig, Renderer, Keyboard } 21 | -------------------------------------------------------------------------------- /packages/components/src/react-text-format.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-text-format' { 2 | interface Props { 3 | allowedFormats?: ('URL' | 'Email' | 'Image' | 'Phone' | 'CreditCard' | 'Term')[] 4 | linkTarget?: '_blank' | '_self' | '_parent' | '_top' | 'framename' 5 | terms?: string[] 6 | linkDecorator?: (decoratedHref: string, decoratedText: string, linkTarget: string) => React.Component 7 | emailDecorator?: (decoratedHref: string, decoratedText: string) => React.Component 8 | phoneDecorator?: (decoratedText: string) => React.Component 9 | creditCardDecorator?: (decoratedText: string) => React.Component 10 | imageDecorator?: (decoratedURL: string) => React.Component 11 | termDecorator?: (decoratedText: string) => React.Component 12 | } 13 | 14 | export default class ReactFormatter extends React.Component {} 15 | } 16 | -------------------------------------------------------------------------------- /packages/components/src/renderer/text.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactTextFormat from 'react-text-format' 3 | import { MessageTypeHandlerProps } from '../typings' 4 | import { markdownToHtml } from '../utils' 5 | 6 | export const Text: React.FC> = ({ text, markdown, config }) => { 7 | const { escapeHTML } = config 8 | 9 | let message: React.ReactNode 10 | if (markdown) { 11 | const html = markdownToHtml(text, escapeHTML) 12 | message =
13 | } else { 14 | message =

{text}

15 | } 16 | 17 | return ( 18 | 19 |
{message}
20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /packages/components/src/renderer/typing.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { MessageTypeHandlerProps } from '../typings' 3 | 4 | export const TypingIndicator: React.FC> = () => ( 5 |
6 |
7 |
8 |
9 |
10 | ) 11 | -------------------------------------------------------------------------------- /packages/components/story/audio.stories.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentStory, ComponentMeta } from '@storybook/react' 2 | import React from 'react' 3 | import { Audio } from '../src/renderer/file' 4 | import { defaultMessageConfig } from '../src/utils' 5 | 6 | export default { 7 | title: 'Files/Audio', 8 | component: Audio 9 | } as ComponentMeta 10 | 11 | const Template: ComponentStory = (args) =>