Merge branch 'develop' into 'main'
Refactoring and new Bridge services See merge request digiresilience/link/link-stack!8
This commit is contained in:
commit
bbcd4ca1d9
710 changed files with 21452 additions and 28798 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,6 +1,7 @@
|
||||||
node_modules
|
node_modules
|
||||||
.env
|
.env
|
||||||
.turbo
|
.turbo
|
||||||
|
*.tsbuildinfo
|
||||||
build/**
|
build/**
|
||||||
**/dist/**
|
**/dist/**
|
||||||
.next/**
|
.next/**
|
||||||
|
|
@ -19,10 +20,11 @@ docker-compose.yml
|
||||||
coverage
|
coverage
|
||||||
.pgpass
|
.pgpass
|
||||||
**/dist/**
|
**/dist/**
|
||||||
.metamigo.local.json
|
.bridge.local.json
|
||||||
out/
|
out/
|
||||||
signald-state/*
|
signald-state/*
|
||||||
!./signald-state/.gitkeep
|
!./signald-state/.gitkeep
|
||||||
baileys-state
|
baileys-state
|
||||||
signald-state
|
signald-state
|
||||||
project.org
|
project.org
|
||||||
|
**/.openapi-generator/
|
||||||
|
|
|
||||||
|
|
@ -84,38 +84,49 @@ leafcutter-docker-release:
|
||||||
variables:
|
variables:
|
||||||
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/leafcutter
|
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/leafcutter
|
||||||
|
|
||||||
metamigo-docker-build:
|
bridge-frontend-docker-build:
|
||||||
extends: .docker-build
|
extends: .docker-build
|
||||||
variables:
|
variables:
|
||||||
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/metamigo
|
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/bridge-frontend
|
||||||
DOCKERFILE_PATH: ./apps/metamigo-cli/Dockerfile
|
DOCKERFILE_PATH: ./apps/bridge-frontend/Dockerfile
|
||||||
|
|
||||||
metamigo-docker-release:
|
bridge-frontend-docker-release:
|
||||||
extends: .docker-release
|
extends: .docker-release
|
||||||
variables:
|
variables:
|
||||||
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/metamigo
|
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/bridge-frontend
|
||||||
|
|
||||||
elasticsearch-docker-build:
|
bridge-worker-docker-build:
|
||||||
extends: .docker-build
|
extends: .docker-build
|
||||||
variables:
|
variables:
|
||||||
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/elasticsearch
|
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/bridge-worker
|
||||||
DOCKERFILE_PATH: ./docker/elasticsearch/Dockerfile
|
DOCKERFILE_PATH: ./apps/bridge-worker/Dockerfile
|
||||||
|
|
||||||
elasticsearch-docker-release:
|
bridge-worker-docker-release:
|
||||||
extends: .docker-release
|
extends: .docker-release
|
||||||
variables:
|
variables:
|
||||||
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/elasticsearch
|
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/bridge-worker
|
||||||
|
|
||||||
label-studio-docker-build:
|
bridge-whatsapp-docker-build:
|
||||||
extends: .docker-build
|
extends: .docker-build
|
||||||
variables:
|
variables:
|
||||||
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/label-studio
|
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/bridge-whatsapp
|
||||||
DOCKERFILE_PATH: ./docker/label-studio/Dockerfile
|
DOCKERFILE_PATH: ./apps/bridge-whatsapp/Dockerfile
|
||||||
|
|
||||||
label-studio-docker-release:
|
bridge-whatsapp-docker-release:
|
||||||
extends: .docker-release
|
extends: .docker-release
|
||||||
variables:
|
variables:
|
||||||
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/label-studio
|
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/bridge-whatsapp
|
||||||
|
|
||||||
|
signal-cli-rest-api-docker-build:
|
||||||
|
extends: .docker-build
|
||||||
|
variables:
|
||||||
|
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/signal-cli-rest-api
|
||||||
|
DOCKERFILE_PATH: ./docker/signal-cli-rest-api/Dockerfile
|
||||||
|
|
||||||
|
signal-cli-rest-api-docker-release:
|
||||||
|
extends: .docker-release
|
||||||
|
variables:
|
||||||
|
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/signal-cli-rest-api
|
||||||
|
|
||||||
memcached-docker-build:
|
memcached-docker-build:
|
||||||
extends: .docker-build
|
extends: .docker-build
|
||||||
|
|
@ -183,17 +194,6 @@ redis-docker-release:
|
||||||
variables:
|
variables:
|
||||||
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/redis
|
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/redis
|
||||||
|
|
||||||
signald-docker-build:
|
|
||||||
extends: .docker-build
|
|
||||||
variables:
|
|
||||||
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/signald
|
|
||||||
DOCKERFILE_PATH: ./docker/signald/Dockerfile
|
|
||||||
|
|
||||||
signald-docker-release:
|
|
||||||
extends: .docker-release
|
|
||||||
variables:
|
|
||||||
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/signald
|
|
||||||
|
|
||||||
zammad-docker-build:
|
zammad-docker-build:
|
||||||
extends: .docker-build
|
extends: .docker-build
|
||||||
variables:
|
variables:
|
||||||
|
|
@ -206,7 +206,7 @@ zammad-docker-build:
|
||||||
- npm install npm@latest -g
|
- npm install npm@latest -g
|
||||||
- npm install -g turbo
|
- npm install -g turbo
|
||||||
- npm ci
|
- npm ci
|
||||||
- turbo build --force --filter zammad-addon-*
|
- turbo build --force --filter @link-stack/zammad-addon-*
|
||||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||||
- DOCKER_BUILDKIT=1 docker build --build-arg EMBEDDED=true --pull --no-cache -t ${DOCKER_NS}:${DOCKER_TAG} -f ${DOCKERFILE_PATH} ${DOCKER_CONTEXT}
|
- DOCKER_BUILDKIT=1 docker build --build-arg EMBEDDED=true --pull --no-cache -t ${DOCKER_NS}:${DOCKER_TAG} -f ${DOCKERFILE_PATH} ${DOCKER_CONTEXT}
|
||||||
- docker push ${DOCKER_NS}:${DOCKER_TAG}
|
- docker push ${DOCKER_NS}:${DOCKER_TAG}
|
||||||
|
|
@ -228,7 +228,7 @@ zammad-standalone-docker-build:
|
||||||
- npm install npm@latest -g
|
- npm install npm@latest -g
|
||||||
- npm install -g turbo
|
- npm install -g turbo
|
||||||
- npm ci
|
- npm ci
|
||||||
- turbo build --force --filter zammad-addon-*
|
- turbo build --force --filter @link-stack/zammad-addon-*
|
||||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||||
- DOCKER_BUILDKIT=1 docker build --pull --no-cache -t ${DOCKER_NS}:${DOCKER_TAG} -f ${DOCKERFILE_PATH} ${DOCKER_CONTEXT}
|
- DOCKER_BUILDKIT=1 docker build --pull --no-cache -t ${DOCKER_NS}:${DOCKER_TAG} -f ${DOCKERFILE_PATH} ${DOCKER_CONTEXT}
|
||||||
- docker push ${DOCKER_NS}:${DOCKER_TAG}
|
- docker push ${DOCKER_NS}:${DOCKER_TAG}
|
||||||
|
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
FROM gitpod/workspace-full
|
|
||||||
|
|
||||||
# install tools we need
|
|
||||||
RUN set -ex; \
|
|
||||||
pyenv global system; \
|
|
||||||
sudo add-apt-repository ppa:ansible/ansible; \
|
|
||||||
sudo add-apt-repository ppa:maxmind/ppa; \
|
|
||||||
curl -s https://helm.baltorepo.com/organization/signing.asc | sudo apt-key add - ; \
|
|
||||||
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash; \
|
|
||||||
echo "deb https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list ; \
|
|
||||||
sudo apt-get update; \
|
|
||||||
sudo apt-get -y upgrade ; \
|
|
||||||
sudo apt-get install -y \
|
|
||||||
ansible \
|
|
||||||
build-essential \
|
|
||||||
httpie \
|
|
||||||
fd-find \
|
|
||||||
ffmpeg \
|
|
||||||
geoipupdate \
|
|
||||||
gitlab-runner \
|
|
||||||
helm \
|
|
||||||
htop \
|
|
||||||
iotop \
|
|
||||||
iptraf \
|
|
||||||
jq \
|
|
||||||
kitty-terminfo \
|
|
||||||
libolm-dev \
|
|
||||||
ncdu \
|
|
||||||
postgresql \
|
|
||||||
pwgen \
|
|
||||||
python3-wheel \
|
|
||||||
ripgrep \
|
|
||||||
rsync \
|
|
||||||
scdaemon \
|
|
||||||
socat \
|
|
||||||
tmux \
|
|
||||||
unrar \
|
|
||||||
unzip \
|
|
||||||
vifm \
|
|
||||||
vim \
|
|
||||||
yamllint \
|
|
||||||
zsh \
|
|
||||||
zsh-syntax-highlighting \
|
|
||||||
; sudo rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN set -ex; \
|
|
||||||
brew install \
|
|
||||||
zoxide \
|
|
||||||
fzf;
|
|
||||||
|
|
||||||
# needed for tailscale
|
|
||||||
RUN sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-nft
|
|
||||||
|
|
||||||
# install npm global packages we need
|
|
||||||
RUN set -ex; \
|
|
||||||
npm install -g \
|
|
||||||
standard-version \
|
|
||||||
turbo \
|
|
||||||
;
|
|
||||||
|
|
||||||
# make a place for all our warez
|
|
||||||
RUN sudo mkdir -p /usr/local/bin
|
|
||||||
|
|
||||||
# install AWS' kubectl
|
|
||||||
# from https://docs.aws.amazon.com/eks/latest/userguide/install-kubectl.html
|
|
||||||
ARG KUBECTL_URL="https://amazon-eks.s3.us-west-2.amazonaws.com/1.21.2/2021-07-05/bin/linux/amd64/kubectl"
|
|
||||||
RUN set -ex; \
|
|
||||||
curl -o kubectl "${KUBECTL_URL}"; \
|
|
||||||
chmod +x kubectl; \
|
|
||||||
sudo mv kubectl /usr/local/bin
|
|
||||||
|
|
||||||
# install cloudflared
|
|
||||||
# from https://github.com/cloudflare/cloudflared/releases
|
|
||||||
ARG CLOUDFLARED_VERSION="2023.2.1"
|
|
||||||
RUN set -ex; \
|
|
||||||
wget --progress=dot:mega https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64.deb; \
|
|
||||||
sudo dpkg -i cloudflared-linux-amd64.deb; \
|
|
||||||
cloudflared --version
|
|
||||||
63
.gitpod.yml
63
.gitpod.yml
|
|
@ -1,63 +0,0 @@
|
||||||
---
|
|
||||||
# build the docker image for our gitpod from this dockerfile
|
|
||||||
image:
|
|
||||||
file: .gitpod.dockerfile
|
|
||||||
# all init+before are run in prebuilds, and on workspace startup
|
|
||||||
tasks:
|
|
||||||
- name: npm install
|
|
||||||
init: |
|
|
||||||
npm install
|
|
||||||
# extra extensions we share
|
|
||||||
vscode:
|
|
||||||
extensions:
|
|
||||||
- redhat.vscode-yaml
|
|
||||||
- ms-azuretools.vscode-docker
|
|
||||||
- ms-kubernetes-tools.vscode-kubernetes-tools
|
|
||||||
- ms-vscode.makefile-tools
|
|
||||||
- bungcip.better-toml
|
|
||||||
- sleistner.vscode-fileutils
|
|
||||||
- esbenp.prettier-vscode
|
|
||||||
- darkriszty.markdown-table-prettify
|
|
||||||
- VisualStudioExptTeam.vscodeintellicode
|
|
||||||
|
|
||||||
ports:
|
|
||||||
- name: Zammad
|
|
||||||
port: 8001
|
|
||||||
onOpen: notify
|
|
||||||
|
|
||||||
- name: Leafcutter Local
|
|
||||||
port: 3001
|
|
||||||
onOpen: notify
|
|
||||||
|
|
||||||
- name: Leafcutter
|
|
||||||
port: 8004
|
|
||||||
onOpen: notify
|
|
||||||
|
|
||||||
- name: Link
|
|
||||||
port: 8003
|
|
||||||
onOpen: notify
|
|
||||||
|
|
||||||
- name: Link Local
|
|
||||||
port: 3000
|
|
||||||
onOpen: notify
|
|
||||||
|
|
||||||
|
|
||||||
- name: Metamigo
|
|
||||||
port: 8002
|
|
||||||
onOpen: notify
|
|
||||||
|
|
||||||
- name: Metamigo Local
|
|
||||||
port: 2999
|
|
||||||
onOpen: notify
|
|
||||||
|
|
||||||
- name: Metamigo API
|
|
||||||
port: 8004
|
|
||||||
onOpen: notify
|
|
||||||
|
|
||||||
- name: Zammad Postgres
|
|
||||||
port: 5432
|
|
||||||
onOpen: notify
|
|
||||||
|
|
||||||
- name: Metamigo Postgres
|
|
||||||
port: 5433
|
|
||||||
onOpen: notify
|
|
||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"prettier.prettierPath": ""
|
|
||||||
}
|
|
||||||
83
Makefile
83
Makefile
|
|
@ -1,83 +0,0 @@
|
||||||
CURRENT_UID := $(shell id -u):$(shell id -g)
|
|
||||||
PACKAGE_NAME ?= $(shell jq -r '.name' package.json)
|
|
||||||
PACKAGE_VERSION?= $(shell jq -r '.version' package.json)
|
|
||||||
BUILD_DATE ?=$(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
||||||
DOCKER_ARGS ?=
|
|
||||||
DOCKER_NS ?= registry.gitlab.com/digiresilience/link/${PACKAGE_NAME}
|
|
||||||
DOCKER_TAG ?= test
|
|
||||||
DOCKER_BUILD := docker build ${DOCKER_ARGS} --build-arg BUILD_DATE=${BUILD_DATE}
|
|
||||||
DOCKER_BUILD_FRESH := ${DOCKER_BUILD} --pull --no-cache
|
|
||||||
DOCKER_BUILD_ARGS := --build-arg VCS_REF=${CI_COMMIT_SHORT_SHA}
|
|
||||||
DOCKER_PUSH := docker push
|
|
||||||
DOCKER_BUILD_TAG := ${DOCKER_NS}:${DOCKER_TAG}
|
|
||||||
|
|
||||||
.PHONY: .npmrc
|
|
||||||
.EXPORT_ALL_VARIABLES:
|
|
||||||
|
|
||||||
.npmrc:
|
|
||||||
ifdef CI_JOB_TOKEN
|
|
||||||
echo '@guardianproject-ops:registry=https://gitlab.com/api/v4/packages/npm/' > .npmrc
|
|
||||||
echo '@digiresilience:registry=https://gitlab.com/api/v4/packages/npm/' >> .npmrc
|
|
||||||
echo '//gitlab.com/api/v4/packages/npm/:_authToken=${CI_JOB_TOKEN}' >> .npmrc
|
|
||||||
echo '//gitlab.com/api/v4/projects/:_authToken=${CI_JOB_TOKEN}' >> .npmrc
|
|
||||||
echo '//gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}' >> .npmrc
|
|
||||||
endif
|
|
||||||
|
|
||||||
docker/build: .npmrc
|
|
||||||
DOCKER_BUILDKIT=1 ${DOCKER_BUILD} ${DOCKER_BUILD_ARGS} -t ${DOCKER_BUILD_TAG} ${PWD}
|
|
||||||
|
|
||||||
docker/build-fresh: .npmrc
|
|
||||||
DOCKER_BUILDKIT=1 ${DOCKER_BUILD_FRESH} ${DOCKER_BUILD_ARGS} -t ${DOCKER_BUILD_TAG} ${PWD}
|
|
||||||
|
|
||||||
docker/add-tag:
|
|
||||||
docker pull ${DOCKER_NS}:${DOCKER_TAG}
|
|
||||||
docker tag ${DOCKER_NS}:${DOCKER_TAG} ${DOCKER_NS}:${DOCKER_TAG_NEW}
|
|
||||||
docker push ${DOCKER_NS}:${DOCKER_TAG_NEW}
|
|
||||||
|
|
||||||
docker/push:
|
|
||||||
${DOCKER_PUSH} ${DOCKER_BUILD_TAG}
|
|
||||||
|
|
||||||
docker/build-push: docker/build docker/push
|
|
||||||
docker/build-fresh-push: docker/build-fresh docker/push
|
|
||||||
|
|
||||||
# don't use this to generate passwords for production
|
|
||||||
generate-secrets:
|
|
||||||
ZAMMAD_DATABASE_PASSWORD=$(shell openssl rand -hex 16)
|
|
||||||
METAMIGO_DATABASE_ROOT_PASSWORD=$(shell openssl rand -hex 16)
|
|
||||||
METAMIGO_DATABASE_PASSWORD=$(shell openssl rand -hex 16)
|
|
||||||
METAMIGO_DATABASE_AUTHENTICATOR_PASSWORD=$(shell openssl rand -hex 16)
|
|
||||||
NEXTAUTH_AUDIENCE=$(shell openssl rand -hex 16)
|
|
||||||
NEXTAUTH_SECRET=$(shell openssl rand -hex 16)
|
|
||||||
|
|
||||||
generate-keys:
|
|
||||||
docker exec -i $(shell docker ps -aqf "name=metamigo-frontend") bash -c "/opt/metamigo/cli gen-jwks"
|
|
||||||
|
|
||||||
setup-signal:
|
|
||||||
mkdir -p signald
|
|
||||||
|
|
||||||
create-admin-user:
|
|
||||||
docker exec -i $(shell docker ps -aqf "name=metamigo-postgresql") bash < ./scripts/create-admin-user.sh
|
|
||||||
|
|
||||||
|
|
||||||
.env:
|
|
||||||
@test -f .env || echo "You must create .env please refer to the README" && exit 1
|
|
||||||
|
|
||||||
start: .env
|
|
||||||
CURRENT_UID=$(CURRENT_UID) docker compose -f docker-compose.link.yml up -d
|
|
||||||
|
|
||||||
start-dev: .env
|
|
||||||
CURRENT_UID=$(CURRENT_UID) docker compose up --build -d
|
|
||||||
|
|
||||||
restart: .env
|
|
||||||
CURRENT_UID=$(CURRENT_UID) docker restart $(shell docker ps -a -q)
|
|
||||||
|
|
||||||
stop:
|
|
||||||
CURRENT_UID=$(CURRENT_UID) docker compose down
|
|
||||||
|
|
||||||
destroy:
|
|
||||||
docker compose down
|
|
||||||
docker volume prune
|
|
||||||
|
|
||||||
|
|
||||||
dev-metamigo:
|
|
||||||
CURRENT_UID=$(CURRENT_UID) docker compose up -d metamigo-postgresql signald
|
|
||||||
37
README.md
37
README.md
|
|
@ -1,36 +1 @@
|
||||||
# Dev Setup
|
# TK
|
||||||
|
|
||||||
> NOTE: When using Gitpod/Codespaces, use at least 16GB RAM
|
|
||||||
|
|
||||||
Local dev with docker-compose
|
|
||||||
|
|
||||||
* Create `link-stack/.env` from Bitwarden `.env for root of link-stack`
|
|
||||||
* Run local dev with docker-compose:
|
|
||||||
```
|
|
||||||
git clone ...
|
|
||||||
cd link-stack
|
|
||||||
make start-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Or for local dev of a single app
|
|
||||||
|
|
||||||
* Create `link-stack/apps/link/.env.local` from Bitwarden `.env.local for link-stack/apps/link`
|
|
||||||
* Create `link-stack/apps/metamigo-frontend/.metamigo.local.json` from Bitwarden `.metamigo.local.json for link-stack/apps/metamigo/frontend`
|
|
||||||
* Build locally for development:
|
|
||||||
```
|
|
||||||
npm install
|
|
||||||
make dev-metamigo # this starts the containers
|
|
||||||
npm run migrate # this migrates the db
|
|
||||||
npm run dev:metamigo # this runs metamigo frontend and api
|
|
||||||
```
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
|
|
||||||
- [ ] Delete old JWT config stuff
|
|
||||||
- [ ] Consolidate config
|
|
||||||
- [ ] Complete react-admin upgrade.. make all the metamigo-frontend stuff work
|
|
||||||
* https://marmelab.com/react-admin/Upgrade.html#no-more-prop-injection-in-page-components
|
|
||||||
- [ ] Get metamigo-worker working
|
|
||||||
- [ ] Migrate off mui/styles
|
|
||||||
* https://mui.com/material-ui/migration/v5-style-changes/
|
|
||||||
* the codemods might help us?
|
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,27 @@
|
||||||
FROM node:20 as base
|
FROM node:20-bookworm AS base
|
||||||
|
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
ARG APP_DIR=/opt/metamigo-cli
|
ARG APP_DIR=/opt/bridge-frontend
|
||||||
RUN mkdir -p ${APP_DIR}/
|
RUN mkdir -p ${APP_DIR}/
|
||||||
RUN npm i -g turbo
|
RUN npm i -g turbo
|
||||||
WORKDIR ${APP_DIR}
|
WORKDIR ${APP_DIR}
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN turbo prune --scope=@digiresilience/metamigo-cli --docker
|
RUN turbo prune --scope=@link-stack/bridge-frontend --scope=@link-stack/bridge-migrations --docker
|
||||||
|
|
||||||
FROM base AS installer
|
FROM base AS installer
|
||||||
ARG APP_DIR=/opt/metamigo-cli
|
ARG APP_DIR=/opt/bridge-frontend
|
||||||
WORKDIR ${APP_DIR}
|
WORKDIR ${APP_DIR}
|
||||||
COPY .gitignore .gitignore
|
COPY --from=builder ${APP_DIR}/.gitignore .gitignore
|
||||||
COPY --from=builder ${APP_DIR}/out/json/ .
|
COPY --from=builder ${APP_DIR}/out/json/ .
|
||||||
COPY --from=builder ${APP_DIR}/out/package-lock.json ./package-lock.json
|
COPY --from=builder ${APP_DIR}/out/package-lock.json ./package-lock.json
|
||||||
RUN npm i
|
RUN npm ci
|
||||||
|
|
||||||
COPY --from=builder ${APP_DIR}/out/full/ .
|
COPY --from=builder ${APP_DIR}/out/full/ .
|
||||||
RUN npm i -g turbo
|
RUN npm i -g turbo
|
||||||
RUN turbo run build --filter=metamigo-cli
|
RUN turbo run build --filter=@link-stack/bridge-frontend --filter=@link-stack/bridge-migrations
|
||||||
|
|
||||||
FROM base AS runner
|
FROM base AS runner
|
||||||
ARG APP_DIR=/opt/metamigo-cli
|
ARG APP_DIR=/opt/bridge-frontend
|
||||||
WORKDIR ${APP_DIR}/
|
WORKDIR ${APP_DIR}/
|
||||||
ARG BUILD_DATE
|
ARG BUILD_DATE
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
|
|
@ -33,21 +33,16 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
dumb-init
|
dumb-init
|
||||||
RUN mkdir -p ${APP_DIR}
|
RUN mkdir -p ${APP_DIR}
|
||||||
RUN chown -R node ${APP_DIR}/
|
|
||||||
|
|
||||||
USER node
|
|
||||||
WORKDIR ${APP_DIR}
|
WORKDIR ${APP_DIR}
|
||||||
COPY --from=installer ${APP_DIR}/node_modules/ ./node_modules/
|
COPY --from=installer ${APP_DIR}/node_modules/ ./node_modules/
|
||||||
COPY --from=installer ${APP_DIR}/packages/ ./packages/
|
COPY --from=installer ${APP_DIR}/apps/bridge-frontend/ ./apps/bridge-frontend/
|
||||||
COPY --from=installer ${APP_DIR}/apps/metamigo-cli/ ./apps/metamigo-cli/
|
COPY --from=installer ${APP_DIR}/apps/bridge-migrations/ ./apps/bridge-migrations/
|
||||||
COPY --from=installer ${APP_DIR}/apps/metamigo-api/ ./apps/metamigo-api/
|
|
||||||
COPY --from=installer ${APP_DIR}/apps/metamigo-worker/ ./apps/metamigo-worker/
|
|
||||||
COPY --from=installer ${APP_DIR}/package.json ./package.json
|
COPY --from=installer ${APP_DIR}/package.json ./package.json
|
||||||
USER root
|
RUN chown -R node:node ${APP_DIR}/
|
||||||
WORKDIR ${APP_DIR}/apps/metamigo-cli/
|
WORKDIR ${APP_DIR}/apps/bridge-frontend/
|
||||||
RUN chmod +x docker-entrypoint.sh
|
RUN chmod +x docker-entrypoint.sh
|
||||||
USER node
|
USER node
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
ENV PORT 3000
|
ENV PORT 3000
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
ENTRYPOINT ["/opt/metamigo-cli/apps/metamigo-cli/docker-entrypoint.sh"]
|
ENTRYPOINT ["/opt/bridge-frontend/apps/bridge-frontend/docker-entrypoint.sh"]
|
||||||
14
apps/bridge-frontend/app/(login)/login/page.tsx
Normal file
14
apps/bridge-frontend/app/(login)/login/page.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { Metadata } from "next";
|
||||||
|
import { getSession } from "next-auth/react";
|
||||||
|
import { Login } from "@/app/_components/Login";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Login",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
const session = await getSession();
|
||||||
|
return <Login session={session} />;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Create } from "@link-stack/bridge-ui";
|
||||||
|
|
||||||
|
type PageProps = {
|
||||||
|
params: { segment: string[] };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Page({ params: { segment } }: PageProps) {
|
||||||
|
const service = segment[0];
|
||||||
|
|
||||||
|
return <Create service={service} />;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { db } from "@link-stack/bridge-common";
|
||||||
|
import { serviceConfig, Detail } from "@link-stack/bridge-ui";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
params: { segment: string[] };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Page({ params: { segment } }: Props) {
|
||||||
|
const service = segment[0];
|
||||||
|
const id = segment?.[1];
|
||||||
|
|
||||||
|
if (!id) return null;
|
||||||
|
|
||||||
|
const {
|
||||||
|
[service]: { table },
|
||||||
|
} = serviceConfig;
|
||||||
|
|
||||||
|
const row = await db
|
||||||
|
.selectFrom(table)
|
||||||
|
.selectAll()
|
||||||
|
.where("id", "=", id)
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
if (!row) return null;
|
||||||
|
|
||||||
|
return <Detail service={service} row={row} />;
|
||||||
|
}
|
||||||
27
apps/bridge-frontend/app/(main)/[...segment]/@edit/page.tsx
Normal file
27
apps/bridge-frontend/app/(main)/[...segment]/@edit/page.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { db } from "@link-stack/bridge-common";
|
||||||
|
import { serviceConfig, Edit } from "@link-stack/bridge-ui";
|
||||||
|
|
||||||
|
type PageProps = {
|
||||||
|
params: { segment: string[] };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Page({ params: { segment } }: PageProps) {
|
||||||
|
const service = segment[0];
|
||||||
|
const id = segment?.[1];
|
||||||
|
|
||||||
|
if (!id) return null;
|
||||||
|
|
||||||
|
const {
|
||||||
|
[service]: { table },
|
||||||
|
} = serviceConfig;
|
||||||
|
|
||||||
|
const row = await db
|
||||||
|
.selectFrom(table)
|
||||||
|
.selectAll()
|
||||||
|
.where("id", "=", id)
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
if (!row) return null;
|
||||||
|
|
||||||
|
return <Edit service={service} row={row} />;
|
||||||
|
}
|
||||||
3
apps/bridge-frontend/app/(main)/[...segment]/layout.tsx
Normal file
3
apps/bridge-frontend/app/(main)/[...segment]/layout.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { ServiceLayout } from "@link-stack/bridge-ui";
|
||||||
|
|
||||||
|
export default ServiceLayout;
|
||||||
22
apps/bridge-frontend/app/(main)/[...segment]/page.tsx
Normal file
22
apps/bridge-frontend/app/(main)/[...segment]/page.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { db } from "@link-stack/bridge-common";
|
||||||
|
import { serviceConfig, List } from "@link-stack/bridge-ui";
|
||||||
|
|
||||||
|
type PageProps = {
|
||||||
|
params: {
|
||||||
|
segment: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Page({ params: { segment } }: PageProps) {
|
||||||
|
const service = segment[0];
|
||||||
|
|
||||||
|
if (!service) return null;
|
||||||
|
|
||||||
|
const config = serviceConfig[service];
|
||||||
|
|
||||||
|
if (!config) return null;
|
||||||
|
|
||||||
|
const rows = await db.selectFrom(config.table).selectAll().execute();
|
||||||
|
|
||||||
|
return <List service={service} rows={rows} />;
|
||||||
|
}
|
||||||
9
apps/bridge-frontend/app/(main)/layout.tsx
Normal file
9
apps/bridge-frontend/app/(main)/layout.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { InternalLayout } from "@/app/_components/InternalLayout";
|
||||||
|
|
||||||
|
export default function Layout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return <InternalLayout>{children}</InternalLayout>;
|
||||||
|
}
|
||||||
5
apps/bridge-frontend/app/(main)/page.tsx
Normal file
5
apps/bridge-frontend/app/(main)/page.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { Home } from "@link-stack/bridge-ui";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return <Home />;
|
||||||
|
}
|
||||||
35
apps/bridge-frontend/app/_components/InternalLayout.tsx
Normal file
35
apps/bridge-frontend/app/_components/InternalLayout.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC, PropsWithChildren, useState } from "react";
|
||||||
|
import { Grid } from "@mui/material";
|
||||||
|
import { CssBaseline } from "@mui/material";
|
||||||
|
import { SessionProvider } from "next-auth/react";
|
||||||
|
import { css, Global } from "@emotion/react";
|
||||||
|
import { fonts } from "@link-stack/ui";
|
||||||
|
import { Sidebar } from "./Sidebar";
|
||||||
|
|
||||||
|
export const InternalLayout: FC<PropsWithChildren> = ({ children }) => {
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
const { roboto } = fonts;
|
||||||
|
const globalCSS = css`
|
||||||
|
* {
|
||||||
|
font-family: ${roboto.style.fontFamily};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SessionProvider>
|
||||||
|
<Global styles={globalCSS} />
|
||||||
|
<CssBaseline />
|
||||||
|
<Grid container direction="row">
|
||||||
|
<Sidebar open={open} setOpen={setOpen} />
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
sx={{ ml: open ? "270px" : "70px", width: "100%", height: "100vh" }}
|
||||||
|
>
|
||||||
|
{children as any}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</SessionProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
185
apps/bridge-frontend/app/_components/Login.tsx
Normal file
185
apps/bridge-frontend/app/_components/Login.tsx
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC, useState } from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Grid,
|
||||||
|
Container,
|
||||||
|
IconButton,
|
||||||
|
Typography,
|
||||||
|
TextField,
|
||||||
|
} from "@mui/material";
|
||||||
|
import {
|
||||||
|
Apple as AppleIcon,
|
||||||
|
Google as GoogleIcon,
|
||||||
|
Key as KeyIcon,
|
||||||
|
} from "@mui/icons-material";
|
||||||
|
import { signIn } from "next-auth/react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import LinkLogo from "@/app/_images/link-logo-small.png";
|
||||||
|
import { colors, fonts } from "@link-stack/ui";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
|
||||||
|
type LoginProps = {
|
||||||
|
session: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Login: FC<LoginProps> = ({ session }) => {
|
||||||
|
const origin =
|
||||||
|
typeof window !== "undefined" && window.location.origin
|
||||||
|
? window.location.origin
|
||||||
|
: "";
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const params = useSearchParams();
|
||||||
|
const error = params.get("error");
|
||||||
|
const { darkGray, cdrLinkOrange, white } = colors;
|
||||||
|
const { poppins } = fonts;
|
||||||
|
const buttonStyles = {
|
||||||
|
borderRadius: 500,
|
||||||
|
width: "100%",
|
||||||
|
fontSize: "16px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
backgroundColor: white,
|
||||||
|
"&:hover": {
|
||||||
|
color: white,
|
||||||
|
backgroundColor: cdrLinkOrange,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const fieldStyles = {
|
||||||
|
"& label.Mui-focused": {
|
||||||
|
color: cdrLinkOrange,
|
||||||
|
},
|
||||||
|
"& .MuiInput-underline:after": {
|
||||||
|
borderBottomColor: cdrLinkOrange,
|
||||||
|
},
|
||||||
|
"& .MuiFilledInput-underline:after": {
|
||||||
|
borderBottomColor: cdrLinkOrange,
|
||||||
|
},
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
"&.Mui-focused fieldset": {
|
||||||
|
borderColor: cdrLinkOrange,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ backgroundColor: darkGray, height: "100vh" }}>
|
||||||
|
<Container maxWidth="md" sx={{ p: 10 }}>
|
||||||
|
<Grid container spacing={2} direction="column" alignItems="center">
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
container
|
||||||
|
direction="row"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Grid item>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "70px",
|
||||||
|
height: "70px",
|
||||||
|
margin: "0 auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={LinkLogo}
|
||||||
|
alt="Link logo"
|
||||||
|
width={70}
|
||||||
|
height={70}
|
||||||
|
style={{
|
||||||
|
objectFit: "cover",
|
||||||
|
filter: "grayscale(100) brightness(100)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Typography
|
||||||
|
variant="h2"
|
||||||
|
sx={{
|
||||||
|
fontSize: 36,
|
||||||
|
color: "white",
|
||||||
|
fontWeight: 700,
|
||||||
|
mt: 1,
|
||||||
|
ml: 0.5,
|
||||||
|
fontFamily: poppins.style.fontFamily,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
CDR Bridge
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item sx={{ width: "100%" }}>
|
||||||
|
{!session ? (
|
||||||
|
<Container
|
||||||
|
maxWidth="xs"
|
||||||
|
sx={{
|
||||||
|
p: 3,
|
||||||
|
mt: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={3}
|
||||||
|
direction="column"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
{error ? (
|
||||||
|
<Grid item sx={{ width: "100%" }}>
|
||||||
|
<Box sx={{ backgroundColor: "red", p: 3 }}>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{
|
||||||
|
fontSize: 18,
|
||||||
|
color: "white",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{`${error} error`}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
) : null}
|
||||||
|
<Grid item sx={{ width: "100%" }}>
|
||||||
|
<IconButton
|
||||||
|
sx={buttonStyles}
|
||||||
|
onClick={() =>
|
||||||
|
signIn("google", {
|
||||||
|
callbackUrl: `${origin}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<GoogleIcon sx={{ mr: 1 }} />
|
||||||
|
Sign in with Google
|
||||||
|
</IconButton>
|
||||||
|
</Grid>
|
||||||
|
<Grid item sx={{ width: "100%" }}>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Sign in with Apple"
|
||||||
|
sx={buttonStyles}
|
||||||
|
onClick={() =>
|
||||||
|
signIn("apple", {
|
||||||
|
callbackUrl: `${window.location.origin}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<AppleIcon sx={{ mr: 1 }} />
|
||||||
|
Sign in with Apple
|
||||||
|
</IconButton>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
) : null}
|
||||||
|
{session ? (
|
||||||
|
<Box component="h4">
|
||||||
|
{` ${session.user.name ?? session.user.email}.`}
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
399
apps/bridge-frontend/app/_components/Sidebar.tsx
Normal file
399
apps/bridge-frontend/app/_components/Sidebar.tsx
Normal file
|
|
@ -0,0 +1,399 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC } from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Grid,
|
||||||
|
Typography,
|
||||||
|
List,
|
||||||
|
ListItemButton,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
ListItemSecondaryAction,
|
||||||
|
Drawer,
|
||||||
|
} from "@mui/material";
|
||||||
|
import {
|
||||||
|
ExpandCircleDown as ExpandCircleDownIcon,
|
||||||
|
AccountCircle as AccountCircleIcon,
|
||||||
|
Chat as ChatIcon,
|
||||||
|
PermPhoneMsg as PhoneIcon,
|
||||||
|
WhatsApp as WhatsAppIcon,
|
||||||
|
Facebook as FacebookIcon,
|
||||||
|
AirlineStops as AirlineStopsIcon,
|
||||||
|
Logout as LogoutIcon,
|
||||||
|
} from "@mui/icons-material";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { typography, fonts, Button } from "@link-stack/ui";
|
||||||
|
import LinkLogo from "@/app/_images/link-logo-small.png";
|
||||||
|
import { useSession, signOut } from "next-auth/react";
|
||||||
|
|
||||||
|
const openWidth = 270;
|
||||||
|
const closedWidth = 70;
|
||||||
|
|
||||||
|
const MenuItem = ({
|
||||||
|
name,
|
||||||
|
href,
|
||||||
|
Icon,
|
||||||
|
iconSize,
|
||||||
|
inset = false,
|
||||||
|
selected = false,
|
||||||
|
open = true,
|
||||||
|
badge,
|
||||||
|
target = "_self",
|
||||||
|
}: any) => (
|
||||||
|
<Link href={href} target={target}>
|
||||||
|
<ListItemButton
|
||||||
|
sx={{
|
||||||
|
p: 0,
|
||||||
|
mb: 1,
|
||||||
|
bl: iconSize === 0 ? "1px solid white" : "inherit",
|
||||||
|
}}
|
||||||
|
selected={selected}
|
||||||
|
>
|
||||||
|
{iconSize > 0 ? (
|
||||||
|
<ListItemIcon
|
||||||
|
sx={{
|
||||||
|
color: `white`,
|
||||||
|
minWidth: 0,
|
||||||
|
mr: 2,
|
||||||
|
textAlign: "center",
|
||||||
|
margin: open ? "0 8 0 0" : "0 auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: iconSize,
|
||||||
|
height: iconSize,
|
||||||
|
mr: 0.5,
|
||||||
|
mt: "-4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon />
|
||||||
|
</Box>
|
||||||
|
</ListItemIcon>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 30,
|
||||||
|
height: "28px",
|
||||||
|
position: "relative",
|
||||||
|
ml: "9px",
|
||||||
|
mr: "1px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "1px",
|
||||||
|
height: "56px",
|
||||||
|
backgroundColor: "white",
|
||||||
|
position: "absolute",
|
||||||
|
left: "3px",
|
||||||
|
top: "-10px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "42px",
|
||||||
|
height: "42px",
|
||||||
|
position: "absolute",
|
||||||
|
top: "-27px",
|
||||||
|
left: "3px",
|
||||||
|
border: "1px solid #fff",
|
||||||
|
borderColor: "transparent transparent transparent #fff",
|
||||||
|
borderRadius: "60px",
|
||||||
|
rotate: "-35deg",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{open && (
|
||||||
|
<ListItemText
|
||||||
|
inset={inset}
|
||||||
|
primary={
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "bold",
|
||||||
|
border: 0,
|
||||||
|
textAlign: "left",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{badge && badge > 0 ? (
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Typography
|
||||||
|
color="textSecondary"
|
||||||
|
variant="body1"
|
||||||
|
className="badge"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "#FFB620",
|
||||||
|
color: "black !important",
|
||||||
|
borderRadius: 10,
|
||||||
|
px: 1,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{badge}
|
||||||
|
</Typography>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
) : null}
|
||||||
|
</ListItemButton>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface SidebarProps {
|
||||||
|
open: boolean;
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const { poppins } = fonts;
|
||||||
|
const { bodyLarge } = typography;
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const user = session?.user;
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
signOut({ callbackUrl: "/login" });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
sx={{ width: open ? openWidth : closedWidth, flexShrink: 0 }}
|
||||||
|
variant="permanent"
|
||||||
|
anchor="left"
|
||||||
|
open={open}
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
width: open ? openWidth : closedWidth,
|
||||||
|
border: 0,
|
||||||
|
overflow: "visible",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 24,
|
||||||
|
right: open ? -8 : -16,
|
||||||
|
color: "#1C75FD",
|
||||||
|
rotate: open ? "90deg" : "-90deg",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setOpen!(!open);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExpandCircleDownIcon
|
||||||
|
sx={{
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
background: "white",
|
||||||
|
borderRadius: 500,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="column"
|
||||||
|
justifyContent="space-between"
|
||||||
|
wrap="nowrap"
|
||||||
|
spacing={0}
|
||||||
|
sx={{ backgroundColor: "#25272A", height: "100%", p: 2 }}
|
||||||
|
>
|
||||||
|
<Grid item container>
|
||||||
|
<Grid item sx={{ width: open ? "40px" : "100%" }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "40px",
|
||||||
|
height: "40px",
|
||||||
|
margin: open ? "0" : "0 auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={LinkLogo}
|
||||||
|
alt="Link logo"
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
style={{
|
||||||
|
objectFit: "cover",
|
||||||
|
filter: "grayscale(100) brightness(100)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
.
|
||||||
|
</Grid>
|
||||||
|
{open && (
|
||||||
|
<Grid item>
|
||||||
|
<Typography
|
||||||
|
variant="h2"
|
||||||
|
sx={{
|
||||||
|
fontSize: 26,
|
||||||
|
color: "white",
|
||||||
|
fontWeight: 700,
|
||||||
|
mt: 1,
|
||||||
|
ml: 0.5,
|
||||||
|
fontFamily: poppins.style.fontFamily,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
CDR Bridge
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: "0.5px",
|
||||||
|
width: "100%",
|
||||||
|
backgroundColor: "#666",
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
container
|
||||||
|
direction="column"
|
||||||
|
sx={{
|
||||||
|
mt: "6px",
|
||||||
|
overflow: "scroll",
|
||||||
|
scrollbarWidth: "none",
|
||||||
|
msOverflowStyle: "none",
|
||||||
|
"&::-webkit-scrollbar": { display: "none" },
|
||||||
|
}}
|
||||||
|
flexGrow={1}
|
||||||
|
>
|
||||||
|
<List
|
||||||
|
component="nav"
|
||||||
|
sx={{
|
||||||
|
a: {
|
||||||
|
textDecoration: "none",
|
||||||
|
|
||||||
|
".MuiListItemButton-root": {
|
||||||
|
p: 1,
|
||||||
|
borderRadius: 2,
|
||||||
|
"&:hover": {
|
||||||
|
background: "#555",
|
||||||
|
},
|
||||||
|
".MuiTypography-root": {
|
||||||
|
p: {
|
||||||
|
color: "#999 !important",
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
".badge": {
|
||||||
|
p: { fontSize: 12, color: "black !important" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
".Mui-selected": {
|
||||||
|
background: "#444",
|
||||||
|
color: "#fff !important",
|
||||||
|
".MuiTypography-root": {
|
||||||
|
p: {
|
||||||
|
color: "#fff !important",
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
".badge": {
|
||||||
|
p: { fontSize: 12, color: "black !important" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
name="WhatsApp"
|
||||||
|
href="/whatsapp"
|
||||||
|
selected={pathname.endsWith("/whatsapp")}
|
||||||
|
Icon={WhatsAppIcon}
|
||||||
|
iconSize={20}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
name="Signal"
|
||||||
|
href="/signal"
|
||||||
|
selected={pathname.startsWith("/signal")}
|
||||||
|
Icon={ChatIcon}
|
||||||
|
iconSize={20}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
name="Facebook"
|
||||||
|
href="/facebook"
|
||||||
|
selected={pathname.startsWith("/facebook")}
|
||||||
|
Icon={FacebookIcon}
|
||||||
|
iconSize={20}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
name="Voice"
|
||||||
|
href="/voice"
|
||||||
|
selected={pathname.startsWith("/voice")}
|
||||||
|
Icon={PhoneIcon}
|
||||||
|
iconSize={20}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
name="Webhooks"
|
||||||
|
href="/webhooks"
|
||||||
|
selected={pathname.startsWith("/webhooks")}
|
||||||
|
Icon={AirlineStopsIcon}
|
||||||
|
iconSize={20}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
name="Users"
|
||||||
|
href="/users"
|
||||||
|
selected={pathname.startsWith("/users")}
|
||||||
|
Icon={AccountCircleIcon}
|
||||||
|
iconSize={20}
|
||||||
|
/>
|
||||||
|
</List>
|
||||||
|
</Grid>
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
container
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
spacing={1}
|
||||||
|
sx={{
|
||||||
|
borderTop: "1px solid #ffffff33",
|
||||||
|
pt: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{user?.image && (
|
||||||
|
<Grid item>
|
||||||
|
<Box sx={{ width: 20, height: 20 }}>
|
||||||
|
<Image
|
||||||
|
src={user?.image ?? ""}
|
||||||
|
alt="Profile image"
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
unoptimized
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Grid item>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
...bodyLarge,
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{user?.email}
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Button text="Logout" kind="secondary" onClick={logout} />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
};
|
||||||
BIN
apps/bridge-frontend/app/_images/link-logo-small.png
Normal file
BIN
apps/bridge-frontend/app/_images/link-logo-small.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
17
apps/bridge-frontend/app/_lib/authentication.ts
Normal file
17
apps/bridge-frontend/app/_lib/authentication.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import GoogleProvider from "next-auth/providers/google";
|
||||||
|
import { KyselyAdapter } from "@auth/kysely-adapter";
|
||||||
|
import { db } from "@link-stack/bridge-common";
|
||||||
|
|
||||||
|
export const authOptions = {
|
||||||
|
// @ts-ignore
|
||||||
|
adapter: KyselyAdapter(db),
|
||||||
|
providers: [
|
||||||
|
GoogleProvider({
|
||||||
|
clientId: process.env.GOOGLE_CLIENT_ID!,
|
||||||
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
session: {
|
||||||
|
strategy: "jwt" as any,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { receiveMessage as POST } from "@link-stack/bridge-ui";
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { getBot as GET } from "@link-stack/bridge-ui";
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { sendMessage as POST } from "@link-stack/bridge-ui";
|
||||||
3
apps/bridge-frontend/app/api/[service]/webhooks/route.ts
Normal file
3
apps/bridge-frontend/app/api/[service]/webhooks/route.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { handleWebhook } from "@link-stack/bridge-ui";
|
||||||
|
|
||||||
|
export { handleWebhook as GET, handleWebhook as POST };
|
||||||
7
apps/bridge-frontend/app/api/auth/[...nextauth]/route.ts
Normal file
7
apps/bridge-frontend/app/api/auth/[...nextauth]/route.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import NextAuth from "next-auth";
|
||||||
|
import { authOptions } from "@/app/_lib/authentication";
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
const handler = NextAuth(authOptions);
|
||||||
|
|
||||||
|
export { handler as GET, handler as POST };
|
||||||
23
apps/bridge-frontend/app/layout.tsx
Normal file
23
apps/bridge-frontend/app/layout.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import { LicenseInfo } from "@mui/x-license";
|
||||||
|
|
||||||
|
LicenseInfo.setLicenseKey(
|
||||||
|
"c787ac6613c5f2aa0494c4285fe3e9f2Tz04OTY1NyxFPTE3NDYzNDE0ODkwMDAsUz1wcm8sTE09c3Vic2NyaXB0aW9uLEtWPTI=",
|
||||||
|
);
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "CDR Bridge",
|
||||||
|
description: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
apps/bridge-frontend/docker-entrypoint.sh
Normal file
7
apps/bridge-frontend/docker-entrypoint.sh
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
echo "running migrations"
|
||||||
|
(cd ../bridge-migrations/ && npm run migrate:up:all)
|
||||||
|
echo "starting bridge-frontend"
|
||||||
|
exec dumb-init npm run start
|
||||||
24
apps/bridge-frontend/middleware.ts
Normal file
24
apps/bridge-frontend/middleware.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { withAuth } from "next-auth/middleware";
|
||||||
|
|
||||||
|
export default withAuth({
|
||||||
|
pages: {
|
||||||
|
signIn: `/login`,
|
||||||
|
},
|
||||||
|
callbacks: {
|
||||||
|
authorized: ({ token }) => {
|
||||||
|
if (process.env.SETUP_MODE === "true") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token?.email) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: ["/((?!ws|wss|api|_next/static|_next/image|favicon.ico).*)"],
|
||||||
|
};
|
||||||
6
apps/bridge-frontend/next.config.js
Normal file
6
apps/bridge-frontend/next.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
transpilePackages: ["@link-stack/ui", "@link-stack/bridge-common", "@link-stack/bridge-ui"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
58
apps/bridge-frontend/package.json
Normal file
58
apps/bridge-frontend/package.json
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
{
|
||||||
|
"name": "@link-stack/bridge-frontend",
|
||||||
|
"version": "2.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"migrate:up:all": "tsx database/migrate.ts up:all",
|
||||||
|
"migrate:up:one": "tsx database/migrate.ts up:one",
|
||||||
|
"migrate:down:all": "tsx database/migrate.ts down:all",
|
||||||
|
"migrate:down:one": "tsx database/migrate.ts down:one"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@auth/kysely-adapter": "^1.4.2",
|
||||||
|
"@emotion/cache": "^11.13.1",
|
||||||
|
"@emotion/react": "^11.13.0",
|
||||||
|
"@emotion/styled": "^11.13.0",
|
||||||
|
"@mui/icons-material": "^5",
|
||||||
|
"@mui/material": "^5",
|
||||||
|
"@mui/material-nextjs": "^5.16.6",
|
||||||
|
"@mui/x-data-grid-pro": "^7.12.0",
|
||||||
|
"@mui/x-date-pickers-pro": "^7.12.0",
|
||||||
|
"@mui/x-license": "^7.12.0",
|
||||||
|
"@link-stack/bridge-common": "*",
|
||||||
|
"@link-stack/bridge-ui": "*",
|
||||||
|
"@link-stack/signal-api": "*",
|
||||||
|
"date-fns": "^3.6.0",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"graphile-worker": "^0.16.6",
|
||||||
|
"kysely": "0.26.1",
|
||||||
|
"material-ui-popup-state": "^5.1.2",
|
||||||
|
"mui-chips-input": "^2.1.5",
|
||||||
|
"next": "14.2.5",
|
||||||
|
"next-auth": "^4.24.7",
|
||||||
|
"pg": "^8.12.0",
|
||||||
|
"react": "18.3.1",
|
||||||
|
"react-cookie": "^7.2.0",
|
||||||
|
"react-digit-input": "^2.1.0",
|
||||||
|
"react-dom": "18.3.1",
|
||||||
|
"react-qr-code": "^2.0.15",
|
||||||
|
"react-timer-hook": "^3.0.7",
|
||||||
|
"sharp": "^0.33.4",
|
||||||
|
"tss-react": "^4.9.12",
|
||||||
|
"tsx": "^4.16.5",
|
||||||
|
"@link-stack/ui": "*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22",
|
||||||
|
"@types/pg": "^8.11.6",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"@link-stack/eslint-config": "*",
|
||||||
|
"@link-stack/typescript-config": "*",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
27
apps/bridge-frontend/tsconfig.json
Normal file
27
apps/bridge-frontend/tsconfig.json
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
93
apps/bridge-migrations/migrate.ts
Normal file
93
apps/bridge-migrations/migrate.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
import * as path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import {
|
||||||
|
Kysely,
|
||||||
|
Migrator,
|
||||||
|
MigrationResult,
|
||||||
|
FileMigrationProvider,
|
||||||
|
PostgresDialect,
|
||||||
|
CamelCasePlugin,
|
||||||
|
} from "kysely";
|
||||||
|
import pkg from "pg";
|
||||||
|
const { Pool } = pkg;
|
||||||
|
import * as dotenv from "dotenv";
|
||||||
|
|
||||||
|
interface Database {}
|
||||||
|
|
||||||
|
export const migrate = async (arg: string) => {
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
dotenv.config({ path: path.join(__dirname, "../.env.local") });
|
||||||
|
}
|
||||||
|
const db = new Kysely<Database>({
|
||||||
|
dialect: new PostgresDialect({
|
||||||
|
pool: new Pool({
|
||||||
|
host: process.env.DATABASE_HOST,
|
||||||
|
database: process.env.DATABASE_NAME,
|
||||||
|
port: parseInt(process.env.DATABASE_PORT!),
|
||||||
|
user: process.env.DATABASE_USER,
|
||||||
|
password: process.env.DATABASE_PASSWORD,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
plugins: [new CamelCasePlugin()],
|
||||||
|
});
|
||||||
|
const migrator = new Migrator({
|
||||||
|
db,
|
||||||
|
provider: new FileMigrationProvider({
|
||||||
|
fs,
|
||||||
|
path,
|
||||||
|
migrationFolder: path.join(__dirname, "migrations"),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
let error: any = null;
|
||||||
|
let results: MigrationResult[] = [];
|
||||||
|
|
||||||
|
if (arg === "up:all") {
|
||||||
|
const out = await migrator.migrateToLatest();
|
||||||
|
results = out.results ?? [];
|
||||||
|
error = out.error;
|
||||||
|
} else if (arg === "up:one") {
|
||||||
|
const out = await migrator.migrateUp();
|
||||||
|
results = out.results ?? [];
|
||||||
|
error = out.error;
|
||||||
|
} else if (arg === "down:all") {
|
||||||
|
const migrations = await migrator.getMigrations();
|
||||||
|
for (const _ of migrations) {
|
||||||
|
const out = await migrator.migrateDown();
|
||||||
|
if (out.results) {
|
||||||
|
results = results.concat(out.results);
|
||||||
|
error = out.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (arg === "down:one") {
|
||||||
|
const out = await migrator.migrateDown();
|
||||||
|
if (out.results) {
|
||||||
|
results = out.results ?? [];
|
||||||
|
error = out.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results?.forEach((it) => {
|
||||||
|
if (it.status === "Success") {
|
||||||
|
console.log(
|
||||||
|
`Migration "${it.migrationName} ${it.direction.toLowerCase()}" was executed successfully`,
|
||||||
|
);
|
||||||
|
} else if (it.status === "Error") {
|
||||||
|
console.error(`Failed to execute migration "${it.migrationName}"`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Failed to migrate");
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.destroy();
|
||||||
|
};
|
||||||
|
|
||||||
|
const arg = process.argv.slice(2).pop();
|
||||||
|
migrate(arg as string);
|
||||||
72
apps/bridge-migrations/migrations/0001-add-next-auth.ts
Normal file
72
apps/bridge-migrations/migrations/0001-add-next-auth.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { Kysely, sql } from "kysely";
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.createTable("User")
|
||||||
|
.addColumn("id", "uuid", (col) =>
|
||||||
|
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||||
|
)
|
||||||
|
.addColumn("name", "text")
|
||||||
|
.addColumn("email", "text", (col) => col.unique().notNull())
|
||||||
|
.addColumn("emailVerified", "timestamptz")
|
||||||
|
.addColumn("image", "text")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createTable("Account")
|
||||||
|
.addColumn("id", "uuid", (col) =>
|
||||||
|
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||||
|
)
|
||||||
|
.addColumn("userId", "uuid", (col) =>
|
||||||
|
col.references("User.id").onDelete("cascade").notNull(),
|
||||||
|
)
|
||||||
|
.addColumn("type", "text", (col) => col.notNull())
|
||||||
|
.addColumn("provider", "text", (col) => col.notNull())
|
||||||
|
.addColumn("providerAccountId", "text", (col) => col.notNull())
|
||||||
|
.addColumn("refresh_token", "text")
|
||||||
|
.addColumn("access_token", "text")
|
||||||
|
.addColumn("expires_at", "bigint")
|
||||||
|
.addColumn("token_type", "text")
|
||||||
|
.addColumn("scope", "text")
|
||||||
|
.addColumn("id_token", "text")
|
||||||
|
.addColumn("session_state", "text")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createTable("Session")
|
||||||
|
.addColumn("id", "uuid", (col) =>
|
||||||
|
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||||
|
)
|
||||||
|
.addColumn("userId", "uuid", (col) =>
|
||||||
|
col.references("User.id").onDelete("cascade").notNull(),
|
||||||
|
)
|
||||||
|
.addColumn("sessionToken", "text", (col) => col.notNull().unique())
|
||||||
|
.addColumn("expires", "timestamptz", (col) => col.notNull())
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createTable("VerificationToken")
|
||||||
|
.addColumn("identifier", "text", (col) => col.notNull())
|
||||||
|
.addColumn("token", "text", (col) => col.notNull().unique())
|
||||||
|
.addColumn("expires", "timestamptz", (col) => col.notNull())
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex("Account_userId_index")
|
||||||
|
.on("Account")
|
||||||
|
.column("userId")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex("Session_userId_index")
|
||||||
|
.on("Session")
|
||||||
|
.column("userId")
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema.dropTable("Account").ifExists().execute();
|
||||||
|
await db.schema.dropTable("Session").ifExists().execute();
|
||||||
|
await db.schema.dropTable("User").ifExists().execute();
|
||||||
|
await db.schema.dropTable("VerificationToken").ifExists().execute();
|
||||||
|
}
|
||||||
33
apps/bridge-migrations/migrations/0002-add-signal.ts
Normal file
33
apps/bridge-migrations/migrations/0002-add-signal.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Kysely, sql } from "kysely";
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.createTable("SignalBot")
|
||||||
|
.addColumn("id", "uuid", (col) =>
|
||||||
|
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||||
|
)
|
||||||
|
.addColumn("phone_number", "text")
|
||||||
|
.addColumn("token", "text", (col) => col.unique().notNull())
|
||||||
|
.addColumn("user_id", "uuid")
|
||||||
|
.addColumn("name", "text")
|
||||||
|
.addColumn("description", "text")
|
||||||
|
.addColumn("qr_code", "text")
|
||||||
|
.addColumn("verified", "boolean", (col) => col.notNull().defaultTo(false))
|
||||||
|
.addColumn("created_at", "timestamptz", (col) =>
|
||||||
|
col.notNull().defaultTo(sql`now()`),
|
||||||
|
)
|
||||||
|
.addColumn("updated_at", "timestamptz", (col) =>
|
||||||
|
col.notNull().defaultTo(sql`now()`),
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex("SignalBotToken")
|
||||||
|
.on("SignalBot")
|
||||||
|
.column("token")
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema.dropTable("SignalBot").ifExists().execute();
|
||||||
|
}
|
||||||
33
apps/bridge-migrations/migrations/0003-add-whatsapp.ts
Normal file
33
apps/bridge-migrations/migrations/0003-add-whatsapp.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Kysely, sql } from "kysely";
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.createTable("WhatsappBot")
|
||||||
|
.addColumn("id", "uuid", (col) =>
|
||||||
|
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||||
|
)
|
||||||
|
.addColumn("phone_number", "text")
|
||||||
|
.addColumn("token", "text", (col) => col.unique().notNull())
|
||||||
|
.addColumn("user_id", "uuid")
|
||||||
|
.addColumn("name", "text")
|
||||||
|
.addColumn("description", "text")
|
||||||
|
.addColumn("qr_code", "text")
|
||||||
|
.addColumn("verified", "boolean", (col) => col.notNull().defaultTo(false))
|
||||||
|
.addColumn("created_at", "timestamptz", (col) =>
|
||||||
|
col.notNull().defaultTo(sql`now()`),
|
||||||
|
)
|
||||||
|
.addColumn("updated_at", "timestamptz", (col) =>
|
||||||
|
col.notNull().defaultTo(sql`now()`),
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex("WhatsappBotToken")
|
||||||
|
.on("WhatsappBot")
|
||||||
|
.column("token")
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema.dropTable("WhatsappBot").ifExists().execute();
|
||||||
|
}
|
||||||
77
apps/bridge-migrations/migrations/0004-add-voice.ts
Normal file
77
apps/bridge-migrations/migrations/0004-add-voice.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { Kysely, sql } from "kysely";
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.createTable("VoiceProvider")
|
||||||
|
.addColumn("id", "uuid", (col) =>
|
||||||
|
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||||
|
)
|
||||||
|
.addColumn("kind", "text", (col) => col.notNull())
|
||||||
|
.addColumn("name", "text", (col) => col.notNull())
|
||||||
|
.addColumn("description", "text")
|
||||||
|
.addColumn("credentials", "jsonb", (col) => col.notNull())
|
||||||
|
.addColumn("created_at", "timestamptz", (col) =>
|
||||||
|
col.notNull().defaultTo(sql`now()`),
|
||||||
|
)
|
||||||
|
.addColumn("updated_at", "timestamptz", (col) =>
|
||||||
|
col.notNull().defaultTo(sql`now()`),
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex("VoiceProviderName")
|
||||||
|
.on("VoiceProvider")
|
||||||
|
.column("name")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createTable("VoiceLine")
|
||||||
|
.addColumn("id", "uuid", (col) =>
|
||||||
|
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||||
|
)
|
||||||
|
.addColumn("provider_id", "uuid", (col) =>
|
||||||
|
col.notNull().references("VoiceProvider.id").onDelete("cascade"),
|
||||||
|
)
|
||||||
|
.addColumn("provider_line_sid", "text", (col) => col.notNull())
|
||||||
|
.addColumn("number", "text", (col) => col.notNull())
|
||||||
|
.addColumn("name", "text", (col) => col.notNull())
|
||||||
|
.addColumn("description", "text")
|
||||||
|
.addColumn("language", "text", (col) => col.notNull())
|
||||||
|
.addColumn("voice", "text", (col) => col.notNull())
|
||||||
|
.addColumn("prompt_text", "text")
|
||||||
|
.addColumn("prompt_audio", "jsonb")
|
||||||
|
.addColumn("audio_prompt_enabled", "boolean", (col) =>
|
||||||
|
col.notNull().defaultTo(false),
|
||||||
|
)
|
||||||
|
.addColumn("audio_converted_at", "timestamptz")
|
||||||
|
.addColumn("created_at", "timestamptz", (col) =>
|
||||||
|
col.notNull().defaultTo(sql`now()`),
|
||||||
|
)
|
||||||
|
.addColumn("updated_at", "timestamptz", (col) =>
|
||||||
|
col.notNull().defaultTo(sql`now()`),
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex("VoiceLineProviderId")
|
||||||
|
.on("VoiceLine")
|
||||||
|
.column("provider_id")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex("VoiceLineProviderLineSid")
|
||||||
|
.on("VoiceLine")
|
||||||
|
.column("provider_line_sid")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex("VoiceLineNumber")
|
||||||
|
.on("VoiceLine")
|
||||||
|
.column("number")
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema.dropTable("VoiceLine").ifExists().execute();
|
||||||
|
await db.schema.dropTable("VoiceProvider").ifExists().execute();
|
||||||
|
}
|
||||||
36
apps/bridge-migrations/migrations/0005-add-facebook.ts
Normal file
36
apps/bridge-migrations/migrations/0005-add-facebook.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { Kysely, sql } from "kysely";
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.createTable("FacebookBot")
|
||||||
|
.addColumn("id", "uuid", (col) =>
|
||||||
|
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||||
|
)
|
||||||
|
.addColumn("name", "text")
|
||||||
|
.addColumn("description", "text")
|
||||||
|
.addColumn("token", "text")
|
||||||
|
.addColumn("page_access_token", "text")
|
||||||
|
.addColumn("app_secret", "text")
|
||||||
|
.addColumn("verify_token", "text")
|
||||||
|
.addColumn("page_id", "text")
|
||||||
|
.addColumn("app_id", "text")
|
||||||
|
.addColumn("user_id", "uuid")
|
||||||
|
.addColumn("verified", "boolean", (col) => col.notNull().defaultTo(false))
|
||||||
|
.addColumn("created_at", "timestamptz", (col) =>
|
||||||
|
col.notNull().defaultTo(sql`now()`),
|
||||||
|
)
|
||||||
|
.addColumn("updated_at", "timestamptz", (col) =>
|
||||||
|
col.notNull().defaultTo(sql`now()`),
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex("FacebookBotToken")
|
||||||
|
.on("FacebookBot")
|
||||||
|
.column("token")
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema.dropTable("FacebookBot").ifExists().execute();
|
||||||
|
}
|
||||||
41
apps/bridge-migrations/migrations/0006-add-webhooks.ts
Normal file
41
apps/bridge-migrations/migrations/0006-add-webhooks.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { Kysely, sql } from "kysely";
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.createTable("Webhook")
|
||||||
|
.addColumn("id", "uuid", (col) =>
|
||||||
|
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||||
|
)
|
||||||
|
.addColumn("name", "text", (col) => col.notNull())
|
||||||
|
.addColumn("description", "text")
|
||||||
|
.addColumn("backend_type", "text", (col) => col.notNull())
|
||||||
|
.addColumn("backend_id", "uuid", (col) => col.notNull())
|
||||||
|
.addColumn("endpoint_url", "text", (col) =>
|
||||||
|
col.notNull().check(sql`endpoint_url ~ '^https?://[^/]+'`),
|
||||||
|
)
|
||||||
|
.addColumn("http_method", "text", (col) =>
|
||||||
|
col
|
||||||
|
.notNull()
|
||||||
|
.defaultTo("post")
|
||||||
|
.check(sql`http_method in ('post', 'put')`),
|
||||||
|
)
|
||||||
|
.addColumn("headers", "jsonb")
|
||||||
|
.addColumn("created_at", "timestamptz", (col) =>
|
||||||
|
col.notNull().defaultTo(sql`now()`),
|
||||||
|
)
|
||||||
|
.addColumn("updated_at", "timestamptz", (col) =>
|
||||||
|
col.notNull().defaultTo(sql`now()`),
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex("WebhookBackendTypeBackendId")
|
||||||
|
.on("Webhook")
|
||||||
|
.column("backend_type")
|
||||||
|
.column("backend_id")
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema.dropTable("Webhook").ifExists().execute();
|
||||||
|
}
|
||||||
28
apps/bridge-migrations/migrations/0007-add-settings.ts
Normal file
28
apps/bridge-migrations/migrations/0007-add-settings.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { Kysely, sql } from "kysely";
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.createTable("Setting")
|
||||||
|
.addColumn("id", "uuid", (col) =>
|
||||||
|
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||||
|
)
|
||||||
|
.addColumn("name", "text")
|
||||||
|
.addColumn("value", "jsonb")
|
||||||
|
.addColumn("created_at", "timestamptz", (col) =>
|
||||||
|
col.notNull().defaultTo(sql`now()`),
|
||||||
|
)
|
||||||
|
.addColumn("updated_at", "timestamptz", (col) =>
|
||||||
|
col.notNull().defaultTo(sql`now()`),
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex("SettingName")
|
||||||
|
.on("Setting")
|
||||||
|
.column("name")
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema.dropTable("Setting").ifExists().execute();
|
||||||
|
}
|
||||||
9
apps/bridge-migrations/migrations/0008-add-user-role.ts
Normal file
9
apps/bridge-migrations/migrations/0008-add-user-role.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Kysely } from "kysely";
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema.alterTable("User").addColumn("role", "text").execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema.alterTable("User").dropColumn("role").execute();
|
||||||
|
}
|
||||||
24
apps/bridge-migrations/package.json
Normal file
24
apps/bridge-migrations/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "@link-stack/bridge-migrations",
|
||||||
|
"version": "2.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"migrate:up:all": "tsx migrate.ts up:all",
|
||||||
|
"migrate:up:one": "tsx migrate.ts up:one",
|
||||||
|
"migrate:down:all": "tsx migrate.ts down:all",
|
||||||
|
"migrate:down:one": "tsx migrate.ts down:one"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"kysely": "0.26.1",
|
||||||
|
"pg": "^8.12.0",
|
||||||
|
"tsx": "^4.16.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22",
|
||||||
|
"@types/pg": "^8.11.6",
|
||||||
|
"@link-stack/eslint-config": "*",
|
||||||
|
"@link-stack/typescript-config": "*",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
39
apps/bridge-whatsapp/Dockerfile
Normal file
39
apps/bridge-whatsapp/Dockerfile
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
FROM node:20-bookworm AS base
|
||||||
|
|
||||||
|
FROM base AS builder
|
||||||
|
ARG APP_DIR=/opt/bridge-whatsapp
|
||||||
|
RUN mkdir -p ${APP_DIR}/
|
||||||
|
RUN npm i -g turbo
|
||||||
|
WORKDIR ${APP_DIR}
|
||||||
|
COPY . .
|
||||||
|
RUN turbo prune --scope=@link-stack/bridge-whatsapp --docker
|
||||||
|
|
||||||
|
FROM base AS installer
|
||||||
|
ARG APP_DIR=/opt/bridge-whatsapp
|
||||||
|
WORKDIR ${APP_DIR}
|
||||||
|
COPY --from=builder ${APP_DIR}/out/json/ .
|
||||||
|
COPY --from=builder ${APP_DIR}/out/full/ .
|
||||||
|
COPY --from=builder ${APP_DIR}/out/package-lock.json ./package-lock.json
|
||||||
|
RUN npm ci
|
||||||
|
RUN npm i -g turbo
|
||||||
|
RUN turbo run build --filter=@link-stack/bridge-whatsapp
|
||||||
|
|
||||||
|
FROM base as runner
|
||||||
|
ARG BUILD_DATE
|
||||||
|
ARG VERSION
|
||||||
|
ARG APP_DIR=/opt/bridge-whatsapp
|
||||||
|
RUN mkdir -p ${APP_DIR}/
|
||||||
|
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
dumb-init
|
||||||
|
WORKDIR ${APP_DIR}
|
||||||
|
COPY --from=installer ${APP_DIR} ./
|
||||||
|
RUN chown -R node:node ${APP_DIR}
|
||||||
|
WORKDIR ${APP_DIR}/apps/bridge-whatsapp/
|
||||||
|
RUN chmod +x docker-entrypoint.sh
|
||||||
|
USER node
|
||||||
|
RUN mkdir /home/node/baileys
|
||||||
|
EXPOSE 5000
|
||||||
|
ENV PORT 5000
|
||||||
|
ENV NODE_ENV production
|
||||||
|
ENTRYPOINT ["/opt/bridge-whatsapp/apps/bridge-whatsapp/docker-entrypoint.sh"]
|
||||||
5
apps/bridge-whatsapp/docker-entrypoint.sh
Normal file
5
apps/bridge-whatsapp/docker-entrypoint.sh
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
echo "starting bridge-whatsapp"
|
||||||
|
exec dumb-init npm run start
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"preset": "jest-config-link",
|
"preset": "jest-config",
|
||||||
"setupFiles": ["<rootDir>/src/setup.test.ts"]
|
"setupFiles": ["<rootDir>/src/setup.test.ts"]
|
||||||
}
|
}
|
||||||
31
apps/bridge-whatsapp/package.json
Normal file
31
apps/bridge-whatsapp/package.json
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"name": "@link-stack/bridge-whatsapp",
|
||||||
|
"version": "2.1.0",
|
||||||
|
"main": "build/main/index.js",
|
||||||
|
"author": "Darren Clarke <darren@redaranj.com>",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"dependencies": {
|
||||||
|
"@adiwajshing/keyed-db": "0.2.4",
|
||||||
|
"@hapi/boom": "^10.0.1",
|
||||||
|
"@hapi/hapi": "^21.3.10",
|
||||||
|
"@hapipal/schmervice": "^3.0.0",
|
||||||
|
"@hapipal/toys": "^4.0.0",
|
||||||
|
"@whiskeysockets/baileys": "^6.7.5",
|
||||||
|
"hapi-pino": "^12.1.0",
|
||||||
|
"link-preview-js": "^3.0.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@link-stack/eslint-config": "*",
|
||||||
|
"@link-stack/jest-config": "*",
|
||||||
|
"@link-stack/typescript-config": "*",
|
||||||
|
"@types/node": "*",
|
||||||
|
"dotenv-cli": "^7.4.2",
|
||||||
|
"tsx": "^4.16.5",
|
||||||
|
"typescript": "^5.5.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"dev": "dotenv -- tsx src/index.ts",
|
||||||
|
"start": "node build/main/index.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
39
apps/bridge-whatsapp/src/index.ts
Normal file
39
apps/bridge-whatsapp/src/index.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import * as Hapi from "@hapi/hapi";
|
||||||
|
import hapiPino from "hapi-pino";
|
||||||
|
import Schmervice from "@hapipal/schmervice";
|
||||||
|
import WhatsappService from "./service.js";
|
||||||
|
import {
|
||||||
|
RegisterBotRoute,
|
||||||
|
UnverifyBotRoute,
|
||||||
|
GetBotRoute,
|
||||||
|
SendMessageRoute,
|
||||||
|
ReceiveMessageRoute,
|
||||||
|
} from "./routes.js";
|
||||||
|
|
||||||
|
const server = Hapi.server({ port: 5000 });
|
||||||
|
|
||||||
|
const startServer = async () => {
|
||||||
|
await server.register({ plugin: hapiPino });
|
||||||
|
|
||||||
|
server.route(RegisterBotRoute);
|
||||||
|
server.route(UnverifyBotRoute);
|
||||||
|
server.route(GetBotRoute);
|
||||||
|
server.route(SendMessageRoute);
|
||||||
|
server.route(ReceiveMessageRoute);
|
||||||
|
|
||||||
|
await server.register(Schmervice);
|
||||||
|
server.registerService(WhatsappService);
|
||||||
|
|
||||||
|
await server.start();
|
||||||
|
|
||||||
|
return server;
|
||||||
|
};
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
await startServer();
|
||||||
|
};
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
118
apps/bridge-whatsapp/src/routes.ts
Normal file
118
apps/bridge-whatsapp/src/routes.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
import * as Hapi from "@hapi/hapi";
|
||||||
|
import Toys from "@hapipal/toys";
|
||||||
|
import WhatsappService from "./service";
|
||||||
|
|
||||||
|
const withDefaults = Toys.withRouteDefaults({
|
||||||
|
options: {
|
||||||
|
cors: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const getService = (request: Hapi.Request): WhatsappService => {
|
||||||
|
const { whatsappService } = request.services();
|
||||||
|
|
||||||
|
return whatsappService as WhatsappService;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface MessageRequest {
|
||||||
|
phoneNumber: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SendMessageRoute = withDefaults({
|
||||||
|
method: "post",
|
||||||
|
path: "/api/bots/{id}/send",
|
||||||
|
options: {
|
||||||
|
description: "Send a message",
|
||||||
|
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||||
|
const { id } = request.params;
|
||||||
|
console.log({ payload: request.payload });
|
||||||
|
const { phoneNumber, message } = request.payload as MessageRequest;
|
||||||
|
const whatsappService = getService(request);
|
||||||
|
await whatsappService.send(id, phoneNumber, message as string);
|
||||||
|
request.logger.info({ id }, "Sent a message at %s", new Date());
|
||||||
|
|
||||||
|
return _h
|
||||||
|
.response({
|
||||||
|
result: {
|
||||||
|
recipient: phoneNumber,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
source: id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.code(200);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ReceiveMessageRoute = withDefaults({
|
||||||
|
method: "get",
|
||||||
|
path: "/api/bots/{id}/receive",
|
||||||
|
options: {
|
||||||
|
description: "Receive messages",
|
||||||
|
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||||
|
const { id } = request.params;
|
||||||
|
const whatsappService = getService(request);
|
||||||
|
const date = new Date();
|
||||||
|
const twoDaysAgo = new Date(date.getTime());
|
||||||
|
twoDaysAgo.setDate(date.getDate() - 2);
|
||||||
|
request.logger.info({ id }, "Received messages at %s", new Date());
|
||||||
|
|
||||||
|
return whatsappService.receive(id, twoDaysAgo);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const RegisterBotRoute = withDefaults({
|
||||||
|
method: "post",
|
||||||
|
path: "/api/bots/{id}/register",
|
||||||
|
options: {
|
||||||
|
description: "Register a bot",
|
||||||
|
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||||
|
const { id } = request.params;
|
||||||
|
const whatsappService = getService(request);
|
||||||
|
|
||||||
|
await whatsappService.register(id);
|
||||||
|
/*
|
||||||
|
, (error: string) => {
|
||||||
|
if (error) {
|
||||||
|
return _h.response(error).code(500);
|
||||||
|
}
|
||||||
|
request.logger.info({ id }, "Register bot at %s", new Date());
|
||||||
|
|
||||||
|
return _h.response().code(200);
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
return _h.response().code(200);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UnverifyBotRoute = withDefaults({
|
||||||
|
method: "post",
|
||||||
|
path: "/api/bots/{id}/unverify",
|
||||||
|
options: {
|
||||||
|
description: "Unverify bot",
|
||||||
|
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||||
|
const { id } = request.params;
|
||||||
|
const whatsappService = getService(request);
|
||||||
|
|
||||||
|
return whatsappService.unverify(id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GetBotRoute = withDefaults({
|
||||||
|
method: "get",
|
||||||
|
path: "/api/bots/{id}",
|
||||||
|
options: {
|
||||||
|
description: "Get bot info",
|
||||||
|
async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) {
|
||||||
|
const { id } = request.params;
|
||||||
|
const whatsappService = getService(request);
|
||||||
|
|
||||||
|
return whatsappService.getBot(id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
/* eslint-disable unicorn/no-abusive-eslint-disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
import { Server } from "@hapi/hapi";
|
import { Server } from "@hapi/hapi";
|
||||||
import { Service } from "@hapipal/schmervice";
|
import { Service } from "@hapipal/schmervice";
|
||||||
import { SavedWhatsappBot as Bot } from "@digiresilience/metamigo-db";
|
|
||||||
import makeWASocket, {
|
import makeWASocket, {
|
||||||
DisconnectReason,
|
DisconnectReason,
|
||||||
proto,
|
proto,
|
||||||
|
|
@ -14,16 +11,15 @@ import makeWASocket, {
|
||||||
useMultiFileAuthState,
|
useMultiFileAuthState,
|
||||||
} from "@whiskeysockets/baileys";
|
} from "@whiskeysockets/baileys";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import workerUtils from "../../worker-utils.js";
|
|
||||||
|
|
||||||
export type AuthCompleteCallback = (error?: string) => void;
|
export type AuthCompleteCallback = (error?: string) => void;
|
||||||
|
|
||||||
export default class WhatsappService extends Service {
|
export default class WhatsappService extends Service {
|
||||||
connections: { [key: string]: any; } = {};
|
connections: { [key: string]: any } = {};
|
||||||
loginConnections: { [key: string]: any; } = {};
|
loginConnections: { [key: string]: any } = {};
|
||||||
|
|
||||||
static browserDescription: [string, string, string] = [
|
static browserDescription: [string, string, string] = [
|
||||||
"Metamigo",
|
"Bridge",
|
||||||
"Chrome",
|
"Chrome",
|
||||||
"2.0",
|
"2.0",
|
||||||
];
|
];
|
||||||
|
|
@ -32,8 +28,16 @@ export default class WhatsappService extends Service {
|
||||||
super(server, options);
|
super(server, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuthDirectory(bot: Bot): string {
|
getBaseDirectory(): string {
|
||||||
return `/baileys/${bot.id}`;
|
return `/home/node/baileys`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBotDirectory(id: string): string {
|
||||||
|
return `${this.getBaseDirectory()}/${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAuthDirectory(id: string): string {
|
||||||
|
return `${this.getBotDirectory(id)}/auth`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
|
|
@ -45,7 +49,6 @@ export default class WhatsappService extends Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sleep(ms: number): Promise<void> {
|
private async sleep(ms: number): Promise<void> {
|
||||||
console.log(`pausing ${ms}`);
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,17 +64,18 @@ export default class WhatsappService extends Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createConnection(
|
private async createConnection(
|
||||||
bot: Bot,
|
botID: string,
|
||||||
server: Server,
|
server: Server,
|
||||||
options: any,
|
options: any,
|
||||||
authCompleteCallback?: any
|
authCompleteCallback?: any,
|
||||||
) {
|
) {
|
||||||
const directory = this.getAuthDirectory(bot);
|
const authDirectory = this.getAuthDirectory(botID);
|
||||||
const { state, saveCreds } = await useMultiFileAuthState(directory);
|
const { state, saveCreds } = await useMultiFileAuthState(authDirectory);
|
||||||
const msgRetryCounterMap: any = {};
|
const msgRetryCounterMap: any = {};
|
||||||
const socket = makeWASocket({
|
const socket = makeWASocket({
|
||||||
...options,
|
...options,
|
||||||
auth: state,
|
auth: state,
|
||||||
|
generateHighQualityLinkPreview: false,
|
||||||
msgRetryCounterMap,
|
msgRetryCounterMap,
|
||||||
shouldIgnoreJid: (jid) =>
|
shouldIgnoreJid: (jid) =>
|
||||||
isJidBroadcast(jid) || isJidStatusBroadcast(jid),
|
isJidBroadcast(jid) || isJidStatusBroadcast(jid),
|
||||||
|
|
@ -89,27 +93,29 @@ export default class WhatsappService extends Service {
|
||||||
} = update;
|
} = update;
|
||||||
if (qr) {
|
if (qr) {
|
||||||
console.log("got qr code");
|
console.log("got qr code");
|
||||||
await this.server.db().whatsappBots.updateQR(bot, qr);
|
const botDirectory = this.getBotDirectory(botID);
|
||||||
|
const qrPath = `${botDirectory}/qr.txt`;
|
||||||
|
fs.writeFileSync(qrPath, qr, "utf8");
|
||||||
} else if (isNewLogin) {
|
} else if (isNewLogin) {
|
||||||
console.log("got new login");
|
console.log("got new login");
|
||||||
await this.server.db().whatsappBots.updateVerified(bot, true);
|
const botDirectory = this.getBotDirectory(botID);
|
||||||
|
const verifiedFile = `${botDirectory}/verified`;
|
||||||
|
fs.writeFileSync(verifiedFile, "");
|
||||||
} else if (connectionState === "open") {
|
} else if (connectionState === "open") {
|
||||||
console.log("opened connection");
|
console.log("opened connection");
|
||||||
} else if (connectionState === "close") {
|
} else if (connectionState === "close") {
|
||||||
console.log("connection closed due to ", lastDisconnect.error);
|
console.log("connection closed due to ", lastDisconnect?.error);
|
||||||
const disconnectStatusCode = (lastDisconnect?.error as any)?.output
|
const disconnectStatusCode = (lastDisconnect?.error as any)?.output
|
||||||
?.statusCode;
|
?.statusCode;
|
||||||
|
|
||||||
if (disconnectStatusCode === DisconnectReason.restartRequired) {
|
if (disconnectStatusCode === DisconnectReason.restartRequired) {
|
||||||
console.log("reconnecting after got new login");
|
console.log("reconnecting after got new login");
|
||||||
const updatedBot = await this.findById(bot.id);
|
await this.createConnection(botID, server, options);
|
||||||
await this.createConnection(updatedBot, server, options);
|
|
||||||
authCompleteCallback?.();
|
authCompleteCallback?.();
|
||||||
} else if (disconnectStatusCode !== DisconnectReason.loggedOut) {
|
} else if (disconnectStatusCode !== DisconnectReason.loggedOut) {
|
||||||
console.log("reconnecting");
|
console.log("reconnecting");
|
||||||
await this.sleep(pause);
|
await this.sleep(pause);
|
||||||
pause *= 2;
|
pause *= 2;
|
||||||
this.createConnection(bot, server, options);
|
this.createConnection(botID, server, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -124,39 +130,49 @@ export default class WhatsappService extends Service {
|
||||||
const upsert = events["messages.upsert"];
|
const upsert = events["messages.upsert"];
|
||||||
const { messages } = upsert;
|
const { messages } = upsert;
|
||||||
if (messages) {
|
if (messages) {
|
||||||
await this.queueUnreadMessages(bot, messages);
|
await this.queueUnreadMessages(botID, messages);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.connections[bot.id] = { socket, msgRetryCounterMap };
|
this.connections[botID] = { socket, msgRetryCounterMap };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateConnections() {
|
private async updateConnections() {
|
||||||
this.resetConnections();
|
this.resetConnections();
|
||||||
|
|
||||||
const bots = await this.server.db().whatsappBots.findAll();
|
const baseDirectory = this.getBaseDirectory();
|
||||||
for await (const bot of bots) {
|
const botIDs = fs.readdirSync(baseDirectory);
|
||||||
if (bot.isVerified) {
|
console.log({ botIDs });
|
||||||
|
for await (const botID of botIDs) {
|
||||||
|
const directory = this.getBotDirectory(botID);
|
||||||
|
const verifiedFile = `${directory}/verified`;
|
||||||
|
if (fs.existsSync(verifiedFile)) {
|
||||||
const { version, isLatest } = await fetchLatestBaileysVersion();
|
const { version, isLatest } = await fetchLatestBaileysVersion();
|
||||||
console.log(`using WA v${version.join(".")}, isLatest: ${isLatest}`);
|
console.log(`using WA v${version.join(".")}, isLatest: ${isLatest}`);
|
||||||
|
|
||||||
await this.createConnection(bot, this.server, {
|
await this.createConnection(botID, this.server, {
|
||||||
browser: WhatsappService.browserDescription,
|
browser: WhatsappService.browserDescription,
|
||||||
printQRInTerminal: false,
|
printQRInTerminal: true,
|
||||||
version,
|
version,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async queueMessage(bot: Bot, webMessageInfo: proto.IWebMessageInfo) {
|
private async queueMessage(
|
||||||
|
botID: string,
|
||||||
|
webMessageInfo: proto.IWebMessageInfo,
|
||||||
|
) {
|
||||||
const {
|
const {
|
||||||
key: { id, fromMe, remoteJid },
|
key: { id, fromMe, remoteJid },
|
||||||
message,
|
message,
|
||||||
messageTimestamp,
|
messageTimestamp,
|
||||||
} = webMessageInfo;
|
} = webMessageInfo;
|
||||||
if (!fromMe && message && remoteJid !== "status@broadcast") {
|
console.log(webMessageInfo);
|
||||||
|
const isValidMessage =
|
||||||
|
message && remoteJid !== "status@broadcast" && !fromMe;
|
||||||
|
if (isValidMessage) {
|
||||||
const { audioMessage, documentMessage, imageMessage, videoMessage } =
|
const { audioMessage, documentMessage, imageMessage, videoMessage } =
|
||||||
message;
|
message;
|
||||||
const isMediaMessage =
|
const isMediaMessage =
|
||||||
|
|
@ -164,31 +180,32 @@ export default class WhatsappService extends Service {
|
||||||
|
|
||||||
const messageContent = Object.values(message)[0];
|
const messageContent = Object.values(message)[0];
|
||||||
let messageType: MediaType;
|
let messageType: MediaType;
|
||||||
let attachment: string;
|
let attachment: string | null | undefined;
|
||||||
let filename: string;
|
let filename: string | null | undefined;
|
||||||
let mimetype: string;
|
let mimeType: string | null | undefined;
|
||||||
if (isMediaMessage) {
|
if (isMediaMessage) {
|
||||||
if (audioMessage) {
|
if (audioMessage) {
|
||||||
messageType = "audio";
|
messageType = "audio";
|
||||||
filename = id + "." + audioMessage.mimetype.split("/").pop();
|
filename = id + "." + audioMessage.mimetype?.split("/").pop();
|
||||||
mimetype = audioMessage.mimetype;
|
mimeType = audioMessage.mimetype;
|
||||||
} else if (documentMessage) {
|
} else if (documentMessage) {
|
||||||
messageType = "document";
|
messageType = "document";
|
||||||
filename = documentMessage.fileName;
|
filename = documentMessage.fileName;
|
||||||
mimetype = documentMessage.mimetype;
|
mimeType = documentMessage.mimetype;
|
||||||
} else if (imageMessage) {
|
} else if (imageMessage) {
|
||||||
messageType = "image";
|
messageType = "image";
|
||||||
filename = id + "." + imageMessage.mimetype.split("/").pop();
|
filename = id + "." + imageMessage.mimetype?.split("/").pop();
|
||||||
mimetype = imageMessage.mimetype;
|
mimeType = imageMessage.mimetype;
|
||||||
} else if (videoMessage) {
|
} else if (videoMessage) {
|
||||||
messageType = "video";
|
messageType = "video";
|
||||||
filename = id + "." + videoMessage.mimetype.split("/").pop();
|
filename = id + "." + videoMessage.mimetype?.split("/").pop();
|
||||||
mimetype = videoMessage.mimetype;
|
mimeType = videoMessage.mimetype;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stream = await downloadContentFromMessage(
|
const stream = await downloadContentFromMessage(
|
||||||
messageContent,
|
messageContent,
|
||||||
messageType
|
// @ts-ignore
|
||||||
|
messageType,
|
||||||
);
|
);
|
||||||
let buffer = Buffer.from([]);
|
let buffer = Buffer.from([]);
|
||||||
for await (const chunk of stream) {
|
for await (const chunk of stream) {
|
||||||
|
|
@ -198,100 +215,97 @@ export default class WhatsappService extends Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messageContent || attachment) {
|
if (messageContent || attachment) {
|
||||||
const receivedMessage = {
|
const conversation = message?.conversation;
|
||||||
waMessageId: id,
|
const extendedTextMessage = message?.extendedTextMessage?.text;
|
||||||
waMessage: JSON.stringify(webMessageInfo),
|
const imageMessage = message?.imageMessage?.caption;
|
||||||
waTimestamp: new Date((messageTimestamp as number) * 1000),
|
const videoMessage = message?.videoMessage?.caption;
|
||||||
|
const messageText = [
|
||||||
|
conversation,
|
||||||
|
extendedTextMessage,
|
||||||
|
imageMessage,
|
||||||
|
videoMessage,
|
||||||
|
].find((text) => text && text !== "");
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
to: botID,
|
||||||
|
from: remoteJid?.split("@")[0],
|
||||||
|
messageId: id,
|
||||||
|
sentAt: new Date((messageTimestamp as number) * 1000).toISOString(),
|
||||||
|
message: messageText,
|
||||||
attachment,
|
attachment,
|
||||||
filename,
|
filename,
|
||||||
mimetype,
|
mimeType,
|
||||||
whatsappBotId: bot.id,
|
|
||||||
botPhoneNumber: bot.phoneNumber,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
workerUtils.addJob("whatsapp-message", receivedMessage, {
|
await fetch(
|
||||||
jobKey: id,
|
`${process.env.BRIDGE_FRONTEND_URL}/api/whatsapp/bots/${botID}/receive`,
|
||||||
});
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async queueUnreadMessages(
|
private async queueUnreadMessages(
|
||||||
bot: Bot,
|
botID: string,
|
||||||
messages: proto.IWebMessageInfo[]
|
messages: proto.IWebMessageInfo[],
|
||||||
) {
|
) {
|
||||||
for await (const message of messages) {
|
for await (const message of messages) {
|
||||||
await this.queueMessage(bot, message);
|
await this.queueMessage(botID, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(
|
getBot(botID: string): Record<string, any> {
|
||||||
phoneNumber: string,
|
const botDirectory = this.getBotDirectory(botID);
|
||||||
description: string,
|
const qrPath = `${botDirectory}/qr.txt`;
|
||||||
email: string
|
const verifiedFile = `${botDirectory}/verified`;
|
||||||
): Promise<Bot> {
|
const qr = fs.existsSync(qrPath) ? fs.readFileSync(qrPath, "utf8") : null;
|
||||||
const db = this.server.db();
|
const verified = fs.existsSync(verifiedFile);
|
||||||
const user = await db.users.findBy({ email });
|
|
||||||
const row = await db.whatsappBots.insert({
|
return { qr, verified };
|
||||||
phoneNumber,
|
|
||||||
description,
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
return row;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async unverify(bot: Bot): Promise<Bot> {
|
async unverify(botID: string): Promise<void> {
|
||||||
const directory = this.getAuthDirectory(bot);
|
const botDirectory = this.getBotDirectory(botID);
|
||||||
fs.rmSync(directory, { recursive: true, force: true });
|
fs.rmSync(botDirectory, { recursive: true, force: true });
|
||||||
return this.server.db().whatsappBots.updateVerified(bot, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(bot: Bot): Promise<number> {
|
async register(
|
||||||
const directory = this.getAuthDirectory(bot);
|
botID: string,
|
||||||
fs.rmSync(directory, { recursive: true, force: true });
|
callback?: AuthCompleteCallback,
|
||||||
return this.server.db().whatsappBots.remove(bot);
|
): Promise<void> {
|
||||||
}
|
|
||||||
|
|
||||||
async findAll(): Promise<Bot[]> {
|
|
||||||
return this.server.db().whatsappBots.findAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
async findById(id: string): Promise<Bot> {
|
|
||||||
return this.server.db().whatsappBots.findById({ id });
|
|
||||||
}
|
|
||||||
|
|
||||||
async findByToken(token: string): Promise<Bot> {
|
|
||||||
return this.server.db().whatsappBots.findBy({ token });
|
|
||||||
}
|
|
||||||
|
|
||||||
async register(bot: Bot, callback: AuthCompleteCallback): Promise<void> {
|
|
||||||
const { version } = await fetchLatestBaileysVersion();
|
const { version } = await fetchLatestBaileysVersion();
|
||||||
await this.createConnection(bot, this.server, { version }, callback);
|
await this.createConnection(
|
||||||
|
botID,
|
||||||
|
this.server,
|
||||||
|
{ version, browser: WhatsappService.browserDescription },
|
||||||
|
callback,
|
||||||
|
);
|
||||||
|
callback?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(bot: Bot, phoneNumber: string, message: string): Promise<void> {
|
async send(
|
||||||
const connection = this.connections[bot.id]?.socket;
|
botID: string,
|
||||||
|
phoneNumber: string,
|
||||||
|
message: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const connection = this.connections[botID]?.socket;
|
||||||
const recipient = `${phoneNumber.replace(/\D+/g, "")}@s.whatsapp.net`;
|
const recipient = `${phoneNumber.replace(/\D+/g, "")}@s.whatsapp.net`;
|
||||||
await connection.sendMessage(recipient, { text: message });
|
await connection.sendMessage(recipient, { text: message });
|
||||||
}
|
}
|
||||||
|
|
||||||
async receiveSince(bot: Bot, lastReceivedDate: Date): Promise<void> {
|
|
||||||
const connection = this.connections[bot.id]?.socket;
|
|
||||||
const messages = await connection.messagesReceivedAfter(
|
|
||||||
lastReceivedDate,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
for (const message of messages) {
|
|
||||||
this.queueMessage(bot, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async receive(
|
async receive(
|
||||||
bot: Bot,
|
botID: string,
|
||||||
_lastReceivedDate: Date
|
_lastReceivedDate: Date,
|
||||||
): Promise<proto.IWebMessageInfo[]> {
|
): Promise<proto.IWebMessageInfo[]> {
|
||||||
const connection = this.connections[bot.id]?.socket;
|
const connection = this.connections[botID]?.socket;
|
||||||
const messages = await connection.loadAllUnreadMessages();
|
const messages = await connection.loadAllUnreadMessages();
|
||||||
|
|
||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
8
apps/bridge-whatsapp/src/types.ts
Normal file
8
apps/bridge-whatsapp/src/types.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import type WhatsappService from "./service.js";
|
||||||
|
|
||||||
|
declare module "@hapipal/schmervice" {
|
||||||
|
interface SchmerviceDecorator {
|
||||||
|
(namespace: "whatsapp"): WhatsappService;
|
||||||
|
}
|
||||||
|
type ServiceFunctionalInterface = { name: string };
|
||||||
|
}
|
||||||
17
apps/bridge-whatsapp/tsconfig.json
Normal file
17
apps/bridge-whatsapp/tsconfig.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"extends": "@link-stack/typescript-config/tsconfig.node.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es2018",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"outDir": "build/main",
|
||||||
|
"rootDir": "src",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"types": ["jest", "node", "long"],
|
||||||
|
"lib": ["es2020", "DOM"],
|
||||||
|
"composite": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/.*.ts"],
|
||||||
|
"exclude": ["node_modules/**"]
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,5 @@
|
||||||
**/.env*
|
**/.env*
|
||||||
**/coverage
|
**/coverage
|
||||||
**/.next
|
**/.next
|
||||||
**/amigo.*.json
|
|
||||||
**/cypress/videos
|
**/cypress/videos
|
||||||
**/cypress/screenshots
|
**/cypress/screenshots
|
||||||
36
apps/bridge-worker/Dockerfile
Normal file
36
apps/bridge-worker/Dockerfile
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
FROM node:20-bookworm AS base
|
||||||
|
|
||||||
|
FROM base AS builder
|
||||||
|
ARG APP_DIR=/opt/bridge-worker
|
||||||
|
RUN mkdir -p ${APP_DIR}/
|
||||||
|
RUN npm i -g turbo
|
||||||
|
WORKDIR ${APP_DIR}
|
||||||
|
COPY . .
|
||||||
|
RUN turbo prune --scope=@link-stack/bridge-worker --docker
|
||||||
|
|
||||||
|
FROM base AS installer
|
||||||
|
ARG APP_DIR=/opt/bridge-worker
|
||||||
|
WORKDIR ${APP_DIR}
|
||||||
|
COPY --from=builder ${APP_DIR}/out/json/ .
|
||||||
|
COPY --from=builder ${APP_DIR}/out/full/ .
|
||||||
|
COPY --from=builder ${APP_DIR}/out/package-lock.json ./package-lock.json
|
||||||
|
RUN npm ci
|
||||||
|
RUN npm i -g turbo
|
||||||
|
RUN turbo run build --filter=@link-stack/bridge-worker
|
||||||
|
|
||||||
|
FROM base as runner
|
||||||
|
ARG BUILD_DATE
|
||||||
|
ARG VERSION
|
||||||
|
ARG APP_DIR=/opt/bridge-worker
|
||||||
|
RUN mkdir -p ${APP_DIR}/
|
||||||
|
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
dumb-init
|
||||||
|
WORKDIR ${APP_DIR}
|
||||||
|
COPY --from=installer ${APP_DIR} ./
|
||||||
|
RUN chown -R node:node ${APP_DIR}
|
||||||
|
WORKDIR ${APP_DIR}/apps/bridge-worker/
|
||||||
|
RUN chmod +x docker-entrypoint.sh
|
||||||
|
USER node
|
||||||
|
ENV NODE_ENV production
|
||||||
|
ENTRYPOINT ["/opt/bridge-worker/apps/bridge-worker/docker-entrypoint.sh"]
|
||||||
1
apps/bridge-worker/crontab
Normal file
1
apps/bridge-worker/crontab
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
*/1 * * * * fetch-signal-messages ?max=1
|
||||||
5
apps/bridge-worker/docker-entrypoint.sh
Normal file
5
apps/bridge-worker/docker-entrypoint.sh
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
echo "starting bridge-worker"
|
||||||
|
exec dumb-init npm run start
|
||||||
13
apps/bridge-worker/graphile.config.ts
Normal file
13
apps/bridge-worker/graphile.config.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import type {} from "graphile-config";
|
||||||
|
import type {} from "graphile-worker";
|
||||||
|
|
||||||
|
const preset: GraphileConfig.Preset = {
|
||||||
|
worker: {
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
maxPoolSize: 10,
|
||||||
|
pollInterval: 2000,
|
||||||
|
fileExtensions: [".ts"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default preset;
|
||||||
28
apps/bridge-worker/index.ts
Normal file
28
apps/bridge-worker/index.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { run } from "graphile-worker";
|
||||||
|
import * as path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const startWorker = async () => {
|
||||||
|
console.log("Starting worker...");
|
||||||
|
console.log(process.env);
|
||||||
|
await run({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
concurrency: 10,
|
||||||
|
noHandleSignals: false,
|
||||||
|
pollInterval: 1000,
|
||||||
|
taskDirectory: `${__dirname}/tasks`,
|
||||||
|
crontabFile: `${__dirname}/crontab`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
await startWorker();
|
||||||
|
};
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
import { SavedVoiceProvider } from "@digiresilience/metamigo-db";
|
// import { SavedVoiceProvider } from "@digiresilience/bridge-db";
|
||||||
import Twilio from "twilio";
|
import Twilio from "twilio";
|
||||||
import { CallInstance } from "twilio/lib/rest/api/v2010/account/call";
|
import { CallInstance } from "twilio/lib/rest/api/v2010/account/call";
|
||||||
import { Zammad, getOrCreateUser } from "./zammad";
|
import { Zammad, getOrCreateUser } from "./zammad.js";
|
||||||
|
|
||||||
|
type SavedVoiceProvider = any;
|
||||||
|
|
||||||
export const twilioClientFor = (
|
export const twilioClientFor = (
|
||||||
provider: SavedVoiceProvider
|
provider: SavedVoiceProvider,
|
||||||
): Twilio.Twilio => {
|
): Twilio.Twilio => {
|
||||||
const { accountSid, apiKeySid, apiKeySecret } = provider.credentials;
|
const { accountSid, apiKeySid, apiKeySecret } = provider.credentials;
|
||||||
if (!accountSid || !apiKeySid || !apiKeySecret)
|
if (!accountSid || !apiKeySid || !apiKeySecret)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`twilio provider ${provider.name} does not have credentials`
|
`twilio provider ${provider.name} does not have credentials`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Twilio(apiKeySid, apiKeySecret, {
|
return Twilio(apiKeySid, apiKeySecret, {
|
||||||
|
|
@ -20,7 +22,7 @@ export const twilioClientFor = (
|
||||||
|
|
||||||
export const createZammadTicket = async (
|
export const createZammadTicket = async (
|
||||||
call: CallInstance,
|
call: CallInstance,
|
||||||
mp3: Buffer
|
mp3: Buffer,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const title = `Call from ${call.fromFormatted} at ${call.startTime}`;
|
const title = `Call from ${call.fromFormatted} at ${call.startTime}`;
|
||||||
const body = `<ul>
|
const body = `<ul>
|
||||||
|
|
@ -36,7 +38,7 @@ export const createZammadTicket = async (
|
||||||
{
|
{
|
||||||
token: "EviH_WL0p6YUlCoIER7noAZEAPsYA_fVU4FZCKdpq525Vmzzvl8d7dNuP_8d-Amb",
|
token: "EviH_WL0p6YUlCoIER7noAZEAPsYA_fVU4FZCKdpq525Vmzzvl8d7dNuP_8d-Amb",
|
||||||
},
|
},
|
||||||
"https://demo.digiresilience.org"
|
"https://demo.digiresilience.org",
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
const customer = await getOrCreateUser(zammad, call.fromFormatted);
|
const customer = await getOrCreateUser(zammad, call.fromFormatted);
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
|
/*
|
||||||
import pgPromise from "pg-promise";
|
import pgPromise from "pg-promise";
|
||||||
import * as pgMonitor from "pg-monitor";
|
import * as pgMonitor from "pg-monitor";
|
||||||
import { dbInitOptions, IRepositories, AppDatabase } from "@digiresilience/metamigo-db";
|
import {
|
||||||
import config from "@digiresilience/metamigo-config";
|
dbInitOptions,
|
||||||
|
IRepositories,
|
||||||
|
AppDatabase,
|
||||||
|
} from "@digiresilience/bridge-db";
|
||||||
|
import config from "@digiresilience/bridge-config";
|
||||||
import type { IInitOptions } from "pg-promise";
|
import type { IInitOptions } from "pg-promise";
|
||||||
|
|
||||||
export const initDiagnostics = (
|
export const initDiagnostics = (
|
||||||
logSql: boolean,
|
logSql: boolean,
|
||||||
initOpts: IInitOptions<IRepositories>
|
initOpts: IInitOptions<IRepositories>,
|
||||||
): void => {
|
): void => {
|
||||||
if (logSql) {
|
if (logSql) {
|
||||||
pgMonitor.attach(initOpts);
|
pgMonitor.attach(initOpts);
|
||||||
|
|
@ -33,8 +38,12 @@ const initDb = (): AppDatabase => {
|
||||||
export const stopDb = async (db: AppDatabase): Promise<void> => {
|
export const stopDb = async (db: AppDatabase): Promise<void> => {
|
||||||
return db.$pool.end();
|
return db.$pool.end();
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type AppDatabase = any;
|
||||||
|
|
||||||
export const withDb = <T>(f: (db: AppDatabase) => Promise<T>): Promise<T> => {
|
export const withDb = <T>(f: (db: AppDatabase) => Promise<T>): Promise<T> => {
|
||||||
|
/*
|
||||||
const db = initDb();
|
const db = initDb();
|
||||||
initDiagnostics(config.logging.sql, pgpInitOptions);
|
initDiagnostics(config.logging.sql, pgpInitOptions);
|
||||||
try {
|
try {
|
||||||
|
|
@ -42,6 +51,6 @@ export const withDb = <T>(f: (db: AppDatabase) => Promise<T>): Promise<T> => {
|
||||||
} finally {
|
} finally {
|
||||||
stopDiagnostics();
|
stopDiagnostics();
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
return f(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type { AppDatabase } from "@digiresilience/metamigo-db";
|
|
||||||
11
apps/bridge-worker/lib/logger.ts
Normal file
11
apps/bridge-worker/lib/logger.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
//import { defState } from "@digiresilience/montar";
|
||||||
|
//import { configureLogger } from "@digiresilience/bridge-common";
|
||||||
|
// import config from "@digiresilience/bridge-config";
|
||||||
|
|
||||||
|
//export const logger = defState("workerLogger", {
|
||||||
|
// start: async () => configureLogger(config),
|
||||||
|
//});
|
||||||
|
//export default logger;
|
||||||
|
|
||||||
|
export const logger = {};
|
||||||
|
export default logger;
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import * as Worker from "graphile-worker";
|
import * as Worker from "graphile-worker";
|
||||||
import { defState } from "@digiresilience/montar";
|
// import { defState } from "@digiresilience/montar";
|
||||||
import config from "@digiresilience/metamigo-config";
|
//import config from "@digiresilience/bridge-config";
|
||||||
|
|
||||||
|
/*
|
||||||
const startWorkerUtils = async (): Promise<Worker.WorkerUtils> => {
|
const startWorkerUtils = async (): Promise<Worker.WorkerUtils> => {
|
||||||
const workerUtils = await Worker.makeWorkerUtils({
|
const workerUtils = await Worker.makeWorkerUtils({
|
||||||
connectionString: config.worker.connection,
|
connectionString: config.worker.connection,
|
||||||
|
|
@ -18,4 +19,8 @@ const workerUtils = defState("workerUtils", {
|
||||||
stop: stopWorkerUtils,
|
stop: stopWorkerUtils,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const workerUtils: any = {};
|
||||||
export default workerUtils;
|
export default workerUtils;
|
||||||
39
apps/bridge-worker/package.json
Normal file
39
apps/bridge-worker/package.json
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"name": "@link-stack/bridge-worker",
|
||||||
|
"version": "2.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "build/main/index.js",
|
||||||
|
"author": "Darren Clarke <darren@redaranj.com>",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json && cp crontab build/main/crontab",
|
||||||
|
"dev": "dotenv -- graphile-worker",
|
||||||
|
"start": "node build/main/index.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hapi/wreck": "^18.1.0",
|
||||||
|
"@link-stack/bridge-common": "*",
|
||||||
|
"@link-stack/signal-api": "*",
|
||||||
|
"fluent-ffmpeg": "^2.1.3",
|
||||||
|
"graphile-worker": "^0.16.6",
|
||||||
|
"html-to-text": "^9.0.5",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"kysely": "^0.27.3",
|
||||||
|
"pg": "^8.12.0",
|
||||||
|
"remeda": "^2.10.0",
|
||||||
|
"twilio": "^5.2.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "7.25.2",
|
||||||
|
"@babel/preset-env": "7.25.3",
|
||||||
|
"@babel/preset-typescript": "7.24.7",
|
||||||
|
"@types/fluent-ffmpeg": "^2.1.25",
|
||||||
|
"dotenv-cli": "^7.4.2",
|
||||||
|
"@link-stack/eslint-config": "*",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"@link-stack/typescript-config": "*",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typedoc": "^0.26.5",
|
||||||
|
"typescript": "^5.5.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
32
apps/bridge-worker/tasks/common/notify-webhooks.ts
Normal file
32
apps/bridge-worker/tasks/common/notify-webhooks.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { db } from "@link-stack/bridge-common";
|
||||||
|
|
||||||
|
export interface NotifyWebhooksOptions {
|
||||||
|
backendId: string;
|
||||||
|
payload: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifyWebhooksTask = async (
|
||||||
|
options: NotifyWebhooksOptions,
|
||||||
|
): Promise<void> => {
|
||||||
|
const { backendId, payload } = options;
|
||||||
|
|
||||||
|
const webhooks = await db
|
||||||
|
.selectFrom("Webhook")
|
||||||
|
.selectAll()
|
||||||
|
.where("backendId", "=", backendId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
for (const webhook of webhooks) {
|
||||||
|
const { endpointUrl, httpMethod, headers } = webhook;
|
||||||
|
const finalHeaders = { "Content-Type": "application/json", ...headers };
|
||||||
|
console.log({ endpointUrl, httpMethod, headers, finalHeaders });
|
||||||
|
const result = await fetch(endpointUrl, {
|
||||||
|
method: httpMethod,
|
||||||
|
headers: finalHeaders,
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
console.log(result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default notifyWebhooksTask;
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { db, getWorkerUtils } from "@link-stack/bridge-common";
|
||||||
|
|
||||||
|
interface ReceiveFacebookMessageTaskOptions {
|
||||||
|
message: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const receiveFacebookMessageTask = async ({
|
||||||
|
message,
|
||||||
|
}: ReceiveFacebookMessageTaskOptions): Promise<void> => {
|
||||||
|
const worker = await getWorkerUtils();
|
||||||
|
|
||||||
|
for (const entry of message.entry) {
|
||||||
|
for (const messaging of entry.messaging) {
|
||||||
|
const pageId = messaging.recipient.id;
|
||||||
|
const row = await db
|
||||||
|
.selectFrom("FacebookBot")
|
||||||
|
.selectAll()
|
||||||
|
.where("pageId", "=", pageId)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
const backendId = row.id;
|
||||||
|
const payload = {
|
||||||
|
to: pageId,
|
||||||
|
from: messaging.sender.id,
|
||||||
|
sent_at: new Date(messaging.timestamp).toISOString(),
|
||||||
|
message: messaging.message.text,
|
||||||
|
message_id: messaging.message.mid,
|
||||||
|
};
|
||||||
|
|
||||||
|
await worker.addJob("common/notify-webhooks", { backendId, payload });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default receiveFacebookMessageTask;
|
||||||
41
apps/bridge-worker/tasks/facebook/send-facebook-message.ts
Normal file
41
apps/bridge-worker/tasks/facebook/send-facebook-message.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { db } from "@link-stack/bridge-common";
|
||||||
|
|
||||||
|
interface SendFacebookMessageTaskOptions {
|
||||||
|
token: string;
|
||||||
|
to: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendFacebookMessageTask = async (
|
||||||
|
options: SendFacebookMessageTaskOptions,
|
||||||
|
): Promise<void> => {
|
||||||
|
const { token, to, message } = options;
|
||||||
|
const { pageId, pageAccessToken } = await db
|
||||||
|
.selectFrom("FacebookBot")
|
||||||
|
.selectAll()
|
||||||
|
.where("token", "=", token)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
|
const endpoint = `https://graph.facebook.com/v19.0/${pageId}/messages`;
|
||||||
|
|
||||||
|
const outgoingMessage = {
|
||||||
|
recipient: { id: to },
|
||||||
|
message: { text: message },
|
||||||
|
messaging_type: "RESPONSE",
|
||||||
|
access_token: pageAccessToken,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(outgoingMessage),
|
||||||
|
});
|
||||||
|
console.log({ response });
|
||||||
|
} catch (error) {
|
||||||
|
console.error({ error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sendFacebookMessageTask;
|
||||||
44
apps/bridge-worker/tasks/fetch-signal-messages.ts
Normal file
44
apps/bridge-worker/tasks/fetch-signal-messages.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { db, getWorkerUtils } from "@link-stack/bridge-common";
|
||||||
|
import * as signalApi from "@link-stack/signal-api";
|
||||||
|
const { Configuration, MessagesApi } = signalApi;
|
||||||
|
|
||||||
|
const fetchSignalMessagesTask = async (): Promise<void> => {
|
||||||
|
const worker = await getWorkerUtils();
|
||||||
|
const rows = await db.selectFrom("SignalBot").selectAll().execute();
|
||||||
|
const config = new Configuration({
|
||||||
|
basePath: process.env.BRIDGE_SIGNAL_URL,
|
||||||
|
});
|
||||||
|
const messagesClient = new MessagesApi(config);
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
const { id, phoneNumber: number } = row;
|
||||||
|
const messages = await messagesClient.v1ReceiveNumberGet({ number });
|
||||||
|
|
||||||
|
for (const msg of messages) {
|
||||||
|
const { envelope } = msg as any;
|
||||||
|
const { source, sourceUuid, dataMessage } = envelope;
|
||||||
|
const message = dataMessage?.message;
|
||||||
|
const rawTimestamp = dataMessage?.timestamp;
|
||||||
|
const timestamp = new Date(rawTimestamp);
|
||||||
|
const messageId = `${sourceUuid}-${rawTimestamp}`;
|
||||||
|
const attachment = undefined;
|
||||||
|
const mimeType = undefined;
|
||||||
|
const filename = undefined;
|
||||||
|
if (source !== number && message) {
|
||||||
|
await worker.addJob("signal/receive-signal-message", {
|
||||||
|
token: id,
|
||||||
|
to: number,
|
||||||
|
from: source,
|
||||||
|
messageId,
|
||||||
|
message,
|
||||||
|
sentAt: timestamp.toISOString(),
|
||||||
|
attachment,
|
||||||
|
filename,
|
||||||
|
mimeType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default fetchSignalMessagesTask;
|
||||||
|
|
@ -1,31 +1,39 @@
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
/*
|
||||||
import { convert } from "html-to-text";
|
import { convert } from "html-to-text";
|
||||||
import fetch from "node-fetch";
|
|
||||||
import { URLSearchParams } from "url";
|
import { URLSearchParams } from "url";
|
||||||
import { withDb, AppDatabase } from "../db";
|
import { withDb, AppDatabase } from "../../lib/db.js";
|
||||||
import { loadConfig } from "@digiresilience/metamigo-config";
|
// import { loadConfig } from "@digiresilience/bridge-config";
|
||||||
import { tagMap } from "../lib/tag-map";
|
import { tagMap } from "../../lib/tag-map.js";
|
||||||
|
|
||||||
|
const config: any = {};
|
||||||
|
|
||||||
type FormattedZammadTicket = {
|
type FormattedZammadTicket = {
|
||||||
data: Record<string, unknown>,
|
data: Record<string, unknown>;
|
||||||
predictions: Record<string, unknown>[];
|
predictions: Record<string, unknown>[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getZammadTickets = async (page: number, minUpdatedTimestamp: Date): Promise<[boolean, FormattedZammadTicket[]]> => {
|
const getZammadTickets = async (
|
||||||
const { leafcutter: { zammadApiUrl, zammadApiKey, contributorName, contributorId } } = await loadConfig();
|
page: number,
|
||||||
|
minUpdatedTimestamp: Date,
|
||||||
|
): Promise<[boolean, FormattedZammadTicket[]]> => {
|
||||||
|
const {
|
||||||
|
leafcutter: { zammadApiUrl, zammadApiKey, contributorName, contributorId },
|
||||||
|
} = config;
|
||||||
const headers = { Authorization: `Token ${zammadApiKey}` };
|
const headers = { Authorization: `Token ${zammadApiKey}` };
|
||||||
let shouldContinue = false;
|
let shouldContinue = false;
|
||||||
const docs = [];
|
const docs = [];
|
||||||
const ticketsQuery = new URLSearchParams({
|
const ticketsQuery = new URLSearchParams({
|
||||||
"expand": "true",
|
expand: "true",
|
||||||
"sort_by": "updated_at",
|
sort_by: "updated_at",
|
||||||
"order_by": "asc",
|
order_by: "asc",
|
||||||
"query": "state.name: closed",
|
query: "state.name: closed",
|
||||||
"per_page": "25",
|
per_page: "25",
|
||||||
"page": `${page}`,
|
page: `${page}`,
|
||||||
});
|
});
|
||||||
const rawTickets = await fetch(`${zammadApiUrl}/tickets/search?${ticketsQuery}`,
|
const rawTickets = await fetch(
|
||||||
{ headers }
|
`${zammadApiUrl}/tickets/search?${ticketsQuery}`,
|
||||||
|
{ headers },
|
||||||
);
|
);
|
||||||
const tickets: any = await rawTickets.json();
|
const tickets: any = await rawTickets.json();
|
||||||
console.log({ tickets });
|
console.log({ tickets });
|
||||||
|
|
@ -41,14 +49,25 @@ const getZammadTickets = async (page: number, minUpdatedTimestamp: Date): Promis
|
||||||
shouldContinue = true;
|
shouldContinue = true;
|
||||||
|
|
||||||
if (source_closed_at <= minUpdatedTimestamp) {
|
if (source_closed_at <= minUpdatedTimestamp) {
|
||||||
console.log(`Skipping ticket`, { source_id, source_updated_at, source_closed_at, minUpdatedTimestamp });
|
console.log(`Skipping ticket`, {
|
||||||
|
source_id,
|
||||||
|
source_updated_at,
|
||||||
|
source_closed_at,
|
||||||
|
minUpdatedTimestamp,
|
||||||
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Processing ticket`, { source_id, source_updated_at, source_closed_at, minUpdatedTimestamp });
|
console.log(`Processing ticket`, {
|
||||||
|
source_id,
|
||||||
|
source_updated_at,
|
||||||
|
source_closed_at,
|
||||||
|
minUpdatedTimestamp,
|
||||||
|
});
|
||||||
|
|
||||||
const rawArticles = await fetch(`${zammadApiUrl}/ticket_articles/by_ticket/${source_id}`,
|
const rawArticles = await fetch(
|
||||||
{ headers }
|
`${zammadApiUrl}/ticket_articles/by_ticket/${source_id}`,
|
||||||
|
{ headers },
|
||||||
);
|
);
|
||||||
const articles: any = await rawArticles.json();
|
const articles: any = await rawArticles.json();
|
||||||
let articleText = "";
|
let articleText = "";
|
||||||
|
|
@ -69,7 +88,9 @@ const getZammadTickets = async (page: number, minUpdatedTimestamp: Date): Promis
|
||||||
o_id: source_id,
|
o_id: source_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const rawTags = await fetch(`${zammadApiUrl}/tags?${tagsQuery}`, { headers });
|
const rawTags = await fetch(`${zammadApiUrl}/tags?${tagsQuery}`, {
|
||||||
|
headers,
|
||||||
|
});
|
||||||
const { tags }: any = await rawTags.json();
|
const { tags }: any = await rawTags.json();
|
||||||
const transformedTags = [];
|
const transformedTags = [];
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
|
|
@ -88,7 +109,7 @@ const getZammadTickets = async (page: number, minUpdatedTimestamp: Date): Promis
|
||||||
source_created_at,
|
source_created_at,
|
||||||
source_updated_at,
|
source_updated_at,
|
||||||
},
|
},
|
||||||
predictions: []
|
predictions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = transformedTags.map((tag) => {
|
const result = transformedTags.map((tag) => {
|
||||||
|
|
@ -115,12 +136,17 @@ const getZammadTickets = async (page: number, minUpdatedTimestamp: Date): Promis
|
||||||
return [shouldContinue, docs];
|
return [shouldContinue, docs];
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchFromZammad = async (minUpdatedTimestamp: Date): Promise<FormattedZammadTicket[]> => {
|
const fetchFromZammad = async (
|
||||||
|
minUpdatedTimestamp: Date,
|
||||||
|
): Promise<FormattedZammadTicket[]> => {
|
||||||
const pages = [...Array.from({ length: 10000 }).keys()];
|
const pages = [...Array.from({ length: 10000 }).keys()];
|
||||||
const allTickets: FormattedZammadTicket[] = [];
|
const allTickets: FormattedZammadTicket[] = [];
|
||||||
|
|
||||||
for await (const page of pages) {
|
for await (const page of pages) {
|
||||||
const [shouldContinue, tickets] = await getZammadTickets(page + 1, minUpdatedTimestamp);
|
const [shouldContinue, tickets] = await getZammadTickets(
|
||||||
|
page + 1,
|
||||||
|
minUpdatedTimestamp,
|
||||||
|
);
|
||||||
|
|
||||||
if (!shouldContinue) {
|
if (!shouldContinue) {
|
||||||
break;
|
break;
|
||||||
|
|
@ -135,7 +161,9 @@ const fetchFromZammad = async (minUpdatedTimestamp: Date): Promise<FormattedZamm
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendToLabelStudio = async (tickets: FormattedZammadTicket[]) => {
|
const sendToLabelStudio = async (tickets: FormattedZammadTicket[]) => {
|
||||||
const { leafcutter: { labelStudioApiUrl, labelStudioApiKey } } = await loadConfig();
|
const {
|
||||||
|
leafcutter: { labelStudioApiUrl, labelStudioApiKey },
|
||||||
|
} = config;
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
Authorization: `Token ${labelStudioApiKey}`,
|
Authorization: `Token ${labelStudioApiKey}`,
|
||||||
|
|
@ -154,13 +182,19 @@ const sendToLabelStudio = async (tickets: FormattedZammadTicket[]) => {
|
||||||
console.log(JSON.stringify(importResult, undefined, 2));
|
console.log(JSON.stringify(importResult, undefined, 2));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
const importLabelStudioTask = async (): Promise<void> => {
|
const importLabelStudioTask = async (): Promise<void> => {
|
||||||
|
/*
|
||||||
withDb(async (db: AppDatabase) => {
|
withDb(async (db: AppDatabase) => {
|
||||||
const { leafcutter: { contributorName } } = await loadConfig();
|
const {
|
||||||
|
leafcutter: { contributorName },
|
||||||
|
} = config;
|
||||||
const settingName = `${contributorName}ImportLabelStudioTask`;
|
const settingName = `${contributorName}ImportLabelStudioTask`;
|
||||||
const res: any = await db.settings.findByName(settingName);
|
const res: any = await db.settings.findByName(settingName);
|
||||||
const startTimestamp = res?.value?.minUpdatedTimestamp ? new Date(res.value.minUpdatedTimestamp as string) : new Date("2023-03-01");
|
const startTimestamp = res?.value?.minUpdatedTimestamp
|
||||||
|
? new Date(res.value.minUpdatedTimestamp as string)
|
||||||
|
: new Date("2023-03-01");
|
||||||
const tickets = await fetchFromZammad(startTimestamp);
|
const tickets = await fetchFromZammad(startTimestamp);
|
||||||
|
|
||||||
if (tickets.length > 0) {
|
if (tickets.length > 0) {
|
||||||
|
|
@ -168,9 +202,12 @@ const importLabelStudioTask = async (): Promise<void> => {
|
||||||
const lastTicket = tickets.pop();
|
const lastTicket = tickets.pop();
|
||||||
const newLastTimestamp = lastTicket.data.source_closed_at;
|
const newLastTimestamp = lastTicket.data.source_closed_at;
|
||||||
console.log({ newLastTimestamp });
|
console.log({ newLastTimestamp });
|
||||||
await db.settings.upsert(settingName, { minUpdatedTimestamp: newLastTimestamp });
|
await db.settings.upsert(settingName, {
|
||||||
|
minUpdatedTimestamp: newLastTimestamp,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
};
|
};
|
||||||
|
|
||||||
export default importLabelStudioTask;
|
export default importLabelStudioTask;
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
import fetch from "node-fetch";
|
/*
|
||||||
import { URLSearchParams } from "url";
|
import { URLSearchParams } from "url";
|
||||||
import { withDb, AppDatabase } from "../db";
|
import { withDb, AppDatabase } from "../../lib/db.js";
|
||||||
import { loadConfig } from "@digiresilience/metamigo-config";
|
// import { loadConfig } from "@digiresilience/bridge-config";
|
||||||
|
|
||||||
|
const config: any = {};
|
||||||
|
|
||||||
type LabelStudioTicket = {
|
type LabelStudioTicket = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -27,14 +29,12 @@ type LeafcutterTicket = {
|
||||||
source_updated_at: string;
|
source_updated_at: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLabelStudioTickets = async (page: number): Promise<LabelStudioTicket[]> => {
|
const getLabelStudioTickets = async (
|
||||||
|
page: number,
|
||||||
|
): Promise<LabelStudioTicket[]> => {
|
||||||
const {
|
const {
|
||||||
leafcutter: {
|
leafcutter: { labelStudioApiUrl, labelStudioApiKey },
|
||||||
labelStudioApiUrl,
|
} = config;
|
||||||
labelStudioApiKey,
|
|
||||||
}
|
|
||||||
} = await loadConfig();
|
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
Authorization: `Token ${labelStudioApiKey}`,
|
Authorization: `Token ${labelStudioApiKey}`,
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
|
|
@ -44,8 +44,10 @@ const getLabelStudioTickets = async (page: number): Promise<LabelStudioTicket[]>
|
||||||
page: `${page}`,
|
page: `${page}`,
|
||||||
});
|
});
|
||||||
console.log({ url: `${labelStudioApiUrl}/projects/1/tasks?${ticketsQuery}` });
|
console.log({ url: `${labelStudioApiUrl}/projects/1/tasks?${ticketsQuery}` });
|
||||||
const res = await fetch(`${labelStudioApiUrl}/projects/1/tasks?${ticketsQuery}`,
|
const res = await fetch(
|
||||||
{ headers });
|
`${labelStudioApiUrl}/projects/1/tasks?${ticketsQuery}`,
|
||||||
|
{ headers },
|
||||||
|
);
|
||||||
console.log({ res });
|
console.log({ res });
|
||||||
const tasksResult: any = await res.json();
|
const tasksResult: any = await res.json();
|
||||||
console.log({ tasksResult });
|
console.log({ tasksResult });
|
||||||
|
|
@ -53,7 +55,9 @@ const getLabelStudioTickets = async (page: number): Promise<LabelStudioTicket[]>
|
||||||
return tasksResult;
|
return tasksResult;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchFromLabelStudio = async (minUpdatedTimestamp: Date): Promise<LabelStudioTicket[]> => {
|
const fetchFromLabelStudio = async (
|
||||||
|
minUpdatedTimestamp: Date,
|
||||||
|
): Promise<LabelStudioTicket[]> => {
|
||||||
const pages = [...Array.from({ length: 10000 }).keys()];
|
const pages = [...Array.from({ length: 10000 }).keys()];
|
||||||
const allDocs: LabelStudioTicket[] = [];
|
const allDocs: LabelStudioTicket[] = [];
|
||||||
|
|
||||||
|
|
@ -85,9 +89,9 @@ const sendToLeafcutter = async (tickets: LabelStudioTicket[]) => {
|
||||||
contributorId,
|
contributorId,
|
||||||
opensearchApiUrl,
|
opensearchApiUrl,
|
||||||
opensearchUsername,
|
opensearchUsername,
|
||||||
opensearchPassword
|
opensearchPassword,
|
||||||
}
|
},
|
||||||
} = await loadConfig();
|
} = config;
|
||||||
|
|
||||||
console.log({ tickets });
|
console.log({ tickets });
|
||||||
const filteredTickets = tickets.filter((ticket) => ticket.is_labeled);
|
const filteredTickets = tickets.filter((ticket) => ticket.is_labeled);
|
||||||
|
|
@ -96,11 +100,7 @@ const sendToLeafcutter = async (tickets: LabelStudioTicket[]) => {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
annotations,
|
annotations,
|
||||||
data: {
|
data: { source_id, source_created_at, source_updated_at },
|
||||||
source_id,
|
|
||||||
source_created_at,
|
|
||||||
source_updated_at
|
|
||||||
}
|
|
||||||
} = ticket;
|
} = ticket;
|
||||||
|
|
||||||
const getTags = (tags: Record<string, any>[], name: string) =>
|
const getTags = (tags: Record<string, any>[], name: string) =>
|
||||||
|
|
@ -127,7 +127,7 @@ const sendToLeafcutter = async (tickets: LabelStudioTicket[]) => {
|
||||||
origin: contributorId,
|
origin: contributorId,
|
||||||
origin_id: source_id as string,
|
origin_id: source_id as string,
|
||||||
source_created_at: source_created_at as string,
|
source_created_at: source_created_at as string,
|
||||||
source_updated_at: source_updated_at as string
|
source_updated_at: source_updated_at as string,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -144,21 +144,34 @@ const sendToLeafcutter = async (tickets: LabelStudioTicket[]) => {
|
||||||
});
|
});
|
||||||
console.log({ result });
|
console.log({ result });
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
const importLeafcutterTask = async (): Promise<void> => {
|
const importLeafcutterTask = async (): Promise<void> => {
|
||||||
|
/*
|
||||||
withDb(async (db: AppDatabase) => {
|
withDb(async (db: AppDatabase) => {
|
||||||
const { leafcutter: { contributorName } } = await loadConfig();
|
const {
|
||||||
|
leafcutter: { contributorName },
|
||||||
|
} = config;
|
||||||
const settingName = `${contributorName}ImportLeafcutterTask`;
|
const settingName = `${contributorName}ImportLeafcutterTask`;
|
||||||
const res: any = await db.settings.findByName(settingName);
|
const res: any = await db.settings.findByName(settingName);
|
||||||
const startTimestamp = res?.value?.minUpdatedTimestamp ? new Date(res.value.minUpdatedTimestamp as string) : new Date("2023-03-01");
|
const startTimestamp = res?.value?.minUpdatedTimestamp
|
||||||
|
? new Date(res.value.minUpdatedTimestamp as string)
|
||||||
|
: new Date("2023-03-01");
|
||||||
const newLastTimestamp = new Date();
|
const newLastTimestamp = new Date();
|
||||||
console.log({ contributorName, settingName, res, startTimestamp, newLastTimestamp });
|
console.log({
|
||||||
|
contributorName,
|
||||||
|
settingName,
|
||||||
|
res,
|
||||||
|
startTimestamp,
|
||||||
|
newLastTimestamp,
|
||||||
|
});
|
||||||
const tickets = await fetchFromLabelStudio(startTimestamp);
|
const tickets = await fetchFromLabelStudio(startTimestamp);
|
||||||
console.log({ tickets });
|
console.log({ tickets });
|
||||||
await sendToLeafcutter(tickets);
|
await sendToLeafcutter(tickets);
|
||||||
await db.settings.upsert(settingName, { minUpdatedTimestamp: newLastTimestamp });
|
await db.settings.upsert(settingName, {
|
||||||
|
minUpdatedTimestamp: newLastTimestamp,
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
*/
|
||||||
};
|
};
|
||||||
|
|
||||||
export default importLeafcutterTask;
|
export default importLeafcutterTask;
|
||||||
49
apps/bridge-worker/tasks/signal/receive-signal-message.ts
Normal file
49
apps/bridge-worker/tasks/signal/receive-signal-message.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { db, getWorkerUtils } from "@link-stack/bridge-common";
|
||||||
|
|
||||||
|
interface ReceiveSignalMessageTaskOptions {
|
||||||
|
token: string;
|
||||||
|
to: string;
|
||||||
|
from: string;
|
||||||
|
messageId: string;
|
||||||
|
sentAt: string;
|
||||||
|
message: string;
|
||||||
|
attachment?: string;
|
||||||
|
filename?: string;
|
||||||
|
mimeType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const receiveSignalMessageTask = async ({
|
||||||
|
token,
|
||||||
|
to,
|
||||||
|
from,
|
||||||
|
messageId,
|
||||||
|
sentAt,
|
||||||
|
message,
|
||||||
|
attachment,
|
||||||
|
filename,
|
||||||
|
mimeType,
|
||||||
|
}: ReceiveSignalMessageTaskOptions): Promise<void> => {
|
||||||
|
console.log({ token, to, from });
|
||||||
|
const worker = await getWorkerUtils();
|
||||||
|
const row = await db
|
||||||
|
.selectFrom("SignalBot")
|
||||||
|
.selectAll()
|
||||||
|
.where("id", "=", token)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
|
const backendId = row.id;
|
||||||
|
const payload = {
|
||||||
|
to,
|
||||||
|
from,
|
||||||
|
message_id: messageId,
|
||||||
|
sent_at: sentAt,
|
||||||
|
message,
|
||||||
|
attachment,
|
||||||
|
filename,
|
||||||
|
mime_type: mimeType,
|
||||||
|
};
|
||||||
|
|
||||||
|
await worker.addJob("common/notify-webhooks", { backendId, payload });
|
||||||
|
};
|
||||||
|
|
||||||
|
export default receiveSignalMessageTask;
|
||||||
44
apps/bridge-worker/tasks/signal/send-signal-message.ts
Normal file
44
apps/bridge-worker/tasks/signal/send-signal-message.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { db } from "@link-stack/bridge-common";
|
||||||
|
import * as signalApi from "@link-stack/signal-api";
|
||||||
|
const { Configuration, MessagesApi } = signalApi;
|
||||||
|
|
||||||
|
interface SendSignalMessageTaskOptions {
|
||||||
|
token: string;
|
||||||
|
to: string;
|
||||||
|
message: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendSignalMessageTask = async ({
|
||||||
|
token,
|
||||||
|
to,
|
||||||
|
message,
|
||||||
|
}: SendSignalMessageTaskOptions): Promise<void> => {
|
||||||
|
console.log({ token, to });
|
||||||
|
const bot = await db
|
||||||
|
.selectFrom("SignalBot")
|
||||||
|
.selectAll()
|
||||||
|
.where("token", "=", token)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
|
const { phoneNumber: number } = bot;
|
||||||
|
const config = new Configuration({
|
||||||
|
basePath: process.env.BRIDGE_SIGNAL_URL,
|
||||||
|
});
|
||||||
|
const messagesClient = new MessagesApi(config);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await messagesClient.v2SendPost({
|
||||||
|
data: {
|
||||||
|
number,
|
||||||
|
recipients: [to],
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log({ response });
|
||||||
|
} catch (error) {
|
||||||
|
console.error({ error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sendSignalMessageTask;
|
||||||
11
apps/bridge-worker/tasks/voice/receive-voice-message.ts
Normal file
11
apps/bridge-worker/tasks/voice/receive-voice-message.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
// import { db, getWorkerUtils } from "@link-stack/bridge-common";
|
||||||
|
|
||||||
|
interface ReceiveVoiceMessageTaskOptions {
|
||||||
|
message: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const receiveVoiceMessageTask = async ({
|
||||||
|
message,
|
||||||
|
}: ReceiveVoiceMessageTaskOptions): Promise<void> => {};
|
||||||
|
|
||||||
|
export default receiveVoiceMessageTask;
|
||||||
11
apps/bridge-worker/tasks/voice/send-voice-message.ts
Normal file
11
apps/bridge-worker/tasks/voice/send-voice-message.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
// import { db, getWorkerUtils } from "@link-stack/bridge-common";
|
||||||
|
|
||||||
|
interface SendVoiceMessageTaskOptions {
|
||||||
|
message: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendVoiceMessageTask = async ({
|
||||||
|
message,
|
||||||
|
}: SendVoiceMessageTaskOptions): Promise<void> => {};
|
||||||
|
|
||||||
|
export default sendVoiceMessageTask;
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import Wreck from "@hapi/wreck";
|
import Wreck from "@hapi/wreck";
|
||||||
import { withDb, AppDatabase } from "../db";
|
import { withDb, AppDatabase } from "../../lib/db.js";
|
||||||
import { twilioClientFor } from "../common";
|
import { twilioClientFor } from "../../lib/common.js";
|
||||||
import { CallInstance } from "twilio/lib/rest/api/v2010/account/call";
|
import { CallInstance } from "twilio/lib/rest/api/v2010/account/call";
|
||||||
import workerUtils from "../utils";
|
import workerUtils from "../../lib/utils.js";
|
||||||
|
|
||||||
interface WebhookPayload {
|
interface WebhookPayload {
|
||||||
startTime: string;
|
startTime: string;
|
||||||
|
|
@ -27,7 +27,7 @@ const getTwilioRecording = async (url: string) => {
|
||||||
|
|
||||||
const formatPayload = (
|
const formatPayload = (
|
||||||
call: CallInstance,
|
call: CallInstance,
|
||||||
recording: Buffer
|
recording: Buffer,
|
||||||
): WebhookPayload => {
|
): WebhookPayload => {
|
||||||
return {
|
return {
|
||||||
startTime: call.startTime.toISOString(),
|
startTime: call.startTime.toISOString(),
|
||||||
|
|
@ -45,12 +45,12 @@ const notifyWebhooks = async (
|
||||||
db: AppDatabase,
|
db: AppDatabase,
|
||||||
voiceLineId: string,
|
voiceLineId: string,
|
||||||
call: CallInstance,
|
call: CallInstance,
|
||||||
recording: Buffer
|
recording: Buffer,
|
||||||
) => {
|
) => {
|
||||||
const webhooks = await db.webhooks.findAllByBackendId("voice", voiceLineId);
|
const webhooks = await db.webhooks.findAllByBackendId("voice", voiceLineId);
|
||||||
if (webhooks && webhooks.length === 0) return;
|
if (webhooks && webhooks.length === 0) return;
|
||||||
|
|
||||||
webhooks.forEach(({ id }) => {
|
webhooks.forEach(({ id }: any) => {
|
||||||
const payload = formatPayload(call, recording);
|
const payload = formatPayload(call, recording);
|
||||||
workerUtils.addJob(
|
workerUtils.addJob(
|
||||||
"notify-webhook",
|
"notify-webhook",
|
||||||
|
|
@ -61,7 +61,7 @@ const notifyWebhooks = async (
|
||||||
{
|
{
|
||||||
// this de-depuplicates the job
|
// this de-depuplicates the job
|
||||||
jobKey: `webhook-${id}-call-${call.sid}`,
|
jobKey: `webhook-${id}-call-${call.sid}`,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -74,7 +74,7 @@ interface TwilioRecordingTaskOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
const twilioRecordingTask = async (
|
const twilioRecordingTask = async (
|
||||||
options: TwilioRecordingTaskOptions
|
options: TwilioRecordingTaskOptions,
|
||||||
): Promise<void> =>
|
): Promise<void> =>
|
||||||
withDb(async (db: AppDatabase) => {
|
withDb(async (db: AppDatabase) => {
|
||||||
const { voiceLineId, accountSid, callSid, recordingSid } = options;
|
const { voiceLineId, accountSid, callSid, recordingSid } = options;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
import { withDb, AppDatabase } from "../db";
|
import { withDb, AppDatabase } from "../../lib/db.js";
|
||||||
import { convert } from "../lib/media-convert";
|
import { convert } from "../../lib/media-convert.js";
|
||||||
|
|
||||||
interface VoiceLineAudioUpdateTaskOptions {
|
interface VoiceLineAudioUpdateTaskOptions {
|
||||||
voiceLineId: string;
|
voiceLineId: string;
|
||||||
|
|
@ -13,7 +13,7 @@ const sha1sum = (v: any) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const voiceLineAudioUpdateTask = async (
|
const voiceLineAudioUpdateTask = async (
|
||||||
payload: VoiceLineAudioUpdateTaskOptions
|
payload: VoiceLineAudioUpdateTaskOptions,
|
||||||
): Promise<void> =>
|
): Promise<void> =>
|
||||||
withDb(async (db: AppDatabase) => {
|
withDb(async (db: AppDatabase) => {
|
||||||
const { voiceLineId } = payload;
|
const { voiceLineId } = payload;
|
||||||
|
|
@ -41,7 +41,7 @@ const voiceLineAudioUpdateTask = async (
|
||||||
"audio/mpeg": mp3.toString("base64"),
|
"audio/mpeg": mp3.toString("base64"),
|
||||||
checksum: webmSha1,
|
checksum: webmSha1,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import Twilio from "twilio";
|
import Twilio from "twilio";
|
||||||
import config from "@digiresilience/metamigo-config";
|
// import config from "@digiresilience/bridge-config";
|
||||||
import { withDb, AppDatabase } from "../db";
|
import { withDb, AppDatabase } from "../../lib/db.js";
|
||||||
|
|
||||||
|
const config: any = {};
|
||||||
|
|
||||||
interface VoiceLineDeleteTaskOptions {
|
interface VoiceLineDeleteTaskOptions {
|
||||||
voiceLineId: string;
|
voiceLineId: string;
|
||||||
|
|
@ -9,7 +11,7 @@ interface VoiceLineDeleteTaskOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
const voiceLineDeleteTask = async (
|
const voiceLineDeleteTask = async (
|
||||||
payload: VoiceLineDeleteTaskOptions
|
payload: VoiceLineDeleteTaskOptions,
|
||||||
): Promise<void> =>
|
): Promise<void> =>
|
||||||
withDb(async (db: AppDatabase) => {
|
withDb(async (db: AppDatabase) => {
|
||||||
const { voiceLineId, providerId, providerLineSid } = payload;
|
const { voiceLineId, providerId, providerLineSid } = payload;
|
||||||
|
|
@ -19,7 +21,7 @@ const voiceLineDeleteTask = async (
|
||||||
const { accountSid, apiKeySid, apiKeySecret } = provider.credentials;
|
const { accountSid, apiKeySid, apiKeySecret } = provider.credentials;
|
||||||
if (!accountSid || !apiKeySid || !apiKeySecret)
|
if (!accountSid || !apiKeySid || !apiKeySecret)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`twilio provider ${provider.name} does not have credentials`
|
`twilio provider ${provider.name} does not have credentials`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const client = Twilio(apiKeySid, apiKeySecret, {
|
const client = Twilio(apiKeySid, apiKeySecret, {
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import Twilio from "twilio";
|
import Twilio from "twilio";
|
||||||
import config from "@digiresilience/metamigo-config";
|
// import config from "@digiresilience/bridge-config";
|
||||||
import { withDb, AppDatabase } from "../db";
|
import { withDb, AppDatabase } from "../../lib/db.js";
|
||||||
|
|
||||||
|
const config: any = {};
|
||||||
|
|
||||||
interface VoiceLineUpdateTaskOptions {
|
interface VoiceLineUpdateTaskOptions {
|
||||||
voiceLineId: string;
|
voiceLineId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const voiceLineUpdateTask = async (
|
const voiceLineUpdateTask = async (
|
||||||
payload: VoiceLineUpdateTaskOptions
|
payload: VoiceLineUpdateTaskOptions,
|
||||||
): Promise<void> =>
|
): Promise<void> =>
|
||||||
withDb(async (db: AppDatabase) => {
|
withDb(async (db: AppDatabase) => {
|
||||||
const { voiceLineId } = payload;
|
const { voiceLineId } = payload;
|
||||||
|
|
@ -22,7 +24,7 @@ const voiceLineUpdateTask = async (
|
||||||
const { accountSid, apiKeySid, apiKeySecret } = provider.credentials;
|
const { accountSid, apiKeySid, apiKeySecret } = provider.credentials;
|
||||||
if (!accountSid || !apiKeySid || !apiKeySecret)
|
if (!accountSid || !apiKeySid || !apiKeySecret)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`twilio provider ${provider.name} does not have credentials`
|
`twilio provider ${provider.name} does not have credentials`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const client = Twilio(apiKeySid, apiKeySecret, {
|
const client = Twilio(apiKeySid, apiKeySecret, {
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { db, getWorkerUtils } from "@link-stack/bridge-common";
|
||||||
|
|
||||||
|
interface ReceiveWhatsappMessageTaskOptions {
|
||||||
|
token: string;
|
||||||
|
to: string;
|
||||||
|
from: string;
|
||||||
|
messageId: string;
|
||||||
|
sentAt: string;
|
||||||
|
message: string;
|
||||||
|
attachment?: string;
|
||||||
|
filename?: string;
|
||||||
|
mimeType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const receiveWhatsappMessageTask = async ({
|
||||||
|
token,
|
||||||
|
to,
|
||||||
|
from,
|
||||||
|
messageId,
|
||||||
|
sentAt,
|
||||||
|
message,
|
||||||
|
attachment,
|
||||||
|
filename,
|
||||||
|
mimeType,
|
||||||
|
}: ReceiveWhatsappMessageTaskOptions): Promise<void> => {
|
||||||
|
console.log({ token, to, from });
|
||||||
|
|
||||||
|
const worker = await getWorkerUtils();
|
||||||
|
const row = await db
|
||||||
|
.selectFrom("WhatsappBot")
|
||||||
|
.selectAll()
|
||||||
|
.where("id", "=", token)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
const backendId = row.id;
|
||||||
|
const payload = {
|
||||||
|
to,
|
||||||
|
from,
|
||||||
|
message_id: messageId,
|
||||||
|
sent_at: sentAt,
|
||||||
|
message,
|
||||||
|
attachment,
|
||||||
|
filename,
|
||||||
|
mime_type: mimeType,
|
||||||
|
};
|
||||||
|
|
||||||
|
await worker.addJob("common/notify-webhooks", { backendId, payload });
|
||||||
|
};
|
||||||
|
|
||||||
|
export default receiveWhatsappMessageTask;
|
||||||
35
apps/bridge-worker/tasks/whatsapp/send-whatsapp-message.ts
Normal file
35
apps/bridge-worker/tasks/whatsapp/send-whatsapp-message.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { db } from "@link-stack/bridge-common";
|
||||||
|
|
||||||
|
interface SendWhatsappMessageTaskOptions {
|
||||||
|
token: string;
|
||||||
|
to: string;
|
||||||
|
message: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendWhatsappMessageTask = async ({
|
||||||
|
message,
|
||||||
|
to,
|
||||||
|
token,
|
||||||
|
}: SendWhatsappMessageTaskOptions): Promise<void> => {
|
||||||
|
const bot = await db
|
||||||
|
.selectFrom("WhatsappBot")
|
||||||
|
.selectAll()
|
||||||
|
.where("token", "=", token)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
|
const url = `${process.env.BRIDGE_WHATSAPP_URL}/api/bots/${bot.id}/send`;
|
||||||
|
const params = { message, phoneNumber: to };
|
||||||
|
try {
|
||||||
|
const result = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
});
|
||||||
|
console.log({ result });
|
||||||
|
} catch (error) {
|
||||||
|
console.error({ error });
|
||||||
|
throw new Error("Failed to send message");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sendWhatsappMessageTask;
|
||||||
8
apps/bridge-worker/tsconfig.json
Normal file
8
apps/bridge-worker/tsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"extends": "@link-stack/typescript-config/tsconfig.node.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "build/main"
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts", "**/.*.ts"],
|
||||||
|
"exclude": ["node_modules", "build"]
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
FROM node:20 AS base
|
FROM node:20 AS base
|
||||||
|
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
|
|
@ -7,12 +6,12 @@ RUN mkdir -p ${APP_DIR}/
|
||||||
RUN npm i -g turbo
|
RUN npm i -g turbo
|
||||||
WORKDIR ${APP_DIR}
|
WORKDIR ${APP_DIR}
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN turbo prune --scope=leafcutter --docker
|
RUN turbo prune --scope=@link-stack/leafcutter --docker
|
||||||
|
|
||||||
FROM base AS installer
|
FROM base AS installer
|
||||||
ARG APP_DIR=/opt/leafcutter
|
ARG APP_DIR=/opt/leafcutter
|
||||||
WORKDIR ${APP_DIR}
|
WORKDIR ${APP_DIR}
|
||||||
COPY .gitignore .gitignore
|
COPY --from=builder ${APP_DIR}/.gitignore .gitignore
|
||||||
COPY --from=builder ${APP_DIR}/out/json/ .
|
COPY --from=builder ${APP_DIR}/out/json/ .
|
||||||
COPY --from=builder ${APP_DIR}/out/package-lock.json ./package-lock.json
|
COPY --from=builder ${APP_DIR}/out/package-lock.json ./package-lock.json
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
@ -20,7 +19,7 @@ RUN npm ci
|
||||||
COPY --from=builder ${APP_DIR}/out/full/ .
|
COPY --from=builder ${APP_DIR}/out/full/ .
|
||||||
ARG LINK_EMBEDDED=true
|
ARG LINK_EMBEDDED=true
|
||||||
RUN npm i -g turbo
|
RUN npm i -g turbo
|
||||||
RUN turbo run build --filter=leafcutter
|
RUN turbo run build --filter=@link-stack/leafcutter
|
||||||
|
|
||||||
FROM base AS runner
|
FROM base AS runner
|
||||||
ARG APP_DIR=/opt/leafcutter
|
ARG APP_DIR=/opt/leafcutter
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { useTranslate } from "react-polyglot";
|
||||||
import { LanguageSelect } from "app/_components/LanguageSelect";
|
import { LanguageSelect } from "app/_components/LanguageSelect";
|
||||||
import LeafcutterLogoLarge from "images/leafcutter-logo-large.png";
|
import LeafcutterLogoLarge from "images/leafcutter-logo-large.png";
|
||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
import { useAppContext } from "app/_components/AppProvider";
|
import { useLeafcutterContext } from "@link-stack/leafcutter-ui";
|
||||||
|
|
||||||
type LoginProps = {
|
type LoginProps = {
|
||||||
session: any;
|
session: any;
|
||||||
|
|
@ -20,7 +20,7 @@ export const Login: FC<LoginProps> = ({ session }) => {
|
||||||
const {
|
const {
|
||||||
colors: { leafcutterElectricBlue, lightGray },
|
colors: { leafcutterElectricBlue, lightGray },
|
||||||
typography: { h1, h4 },
|
typography: { h1, h4 },
|
||||||
} = useAppContext();
|
} = useLeafcutterContext();
|
||||||
const buttonStyles = {
|
const buttonStyles = {
|
||||||
backgroundColor: lightGray,
|
backgroundColor: lightGray,
|
||||||
borderRadius: 500,
|
borderRadius: 500,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { About } from "leafcutter-common";
|
import { About } from "@link-stack/leafcutter-ui";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <About />;
|
return <About />;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { getTemplates } from "app/_lib/opensearch";
|
import { getTemplates } from "app/_lib/opensearch";
|
||||||
import { Create } from "leafcutter-common";
|
import { Create } from "@link-stack/leafcutter-ui";
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const templates = await getTemplates(100);
|
const templates = await getTemplates(100);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { FAQ } from "leafcutter-common";
|
import { FAQ } from "@link-stack/leafcutter-ui";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <FAQ />;
|
return <FAQ />;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,5 @@
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import "app/_styles/global.css";
|
import "app/_styles/global.css";
|
||||||
import "@fontsource/poppins/400.css";
|
|
||||||
import "@fontsource/poppins/700.css";
|
|
||||||
import "@fontsource/roboto/400.css";
|
|
||||||
import "@fontsource/roboto/700.css";
|
|
||||||
import "@fontsource/playfair-display/900.css";
|
|
||||||
// import getConfig from "next/config";
|
|
||||||
// import { LicenseInfo } from "@mui/x-data-grid-pro";
|
|
||||||
import { InternalLayout } from "../_components/InternalLayout";
|
import { InternalLayout } from "../_components/InternalLayout";
|
||||||
|
|
||||||
type LayoutProps = {
|
type LayoutProps = {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { authOptions } from "app/_lib/auth";
|
import { authOptions } from "app/_lib/auth";
|
||||||
import { getUserVisualizations } from "app/_lib/opensearch";
|
import { getUserVisualizations } from "app/_lib/opensearch";
|
||||||
import { Home } from "leafcutter-common";
|
import { Home } from "@link-stack/leafcutter-ui";
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/* eslint-disable no-underscore-dangle */
|
/* eslint-disable no-underscore-dangle */
|
||||||
// import { Client } from "@opensearch-project/opensearch";
|
// import { Client } from "@opensearch-project/opensearch";
|
||||||
import { Preview } from "leafcutter-common";
|
import { Preview } from "@link-stack/leafcutter-ui";
|
||||||
// import { createVisualization } from "lib/opensearch";
|
// import { createVisualization } from "lib/opensearch";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,12 @@ import { useLayoutEffect } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Grid, CircularProgress } from "@mui/material";
|
import { Grid, CircularProgress } from "@mui/material";
|
||||||
import Iframe from "react-iframe";
|
import Iframe from "react-iframe";
|
||||||
import { useAppContext } from "leafcutter-common/components/AppProvider";
|
import { useLeafcutterContext } from "@link-stack/leafcutter-ui/components/LeafcutterProvider";
|
||||||
|
|
||||||
export const Setup: FC = () => {
|
export const Setup: FC = () => {
|
||||||
const {
|
const {
|
||||||
colors: { leafcutterElectricBlue },
|
colors: { leafcutterElectricBlue },
|
||||||
} = useAppContext();
|
} = useLeafcutterContext();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
setTimeout(() => router.push("/"), 4000);
|
setTimeout(() => router.push("/"), 4000);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { getTrends } from "app/_lib/opensearch";
|
import { getTrends } from "app/_lib/opensearch";
|
||||||
import { Trends } from "leafcutter-common";
|
import { Trends } from "@link-stack/leafcutter-ui";
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const visualizations = await getTrends(25);
|
const visualizations = await getTrends(25);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/* eslint-disable no-underscore-dangle */
|
/* eslint-disable no-underscore-dangle */
|
||||||
import { Client } from "@opensearch-project/opensearch";
|
import { Client } from "@opensearch-project/opensearch";
|
||||||
import { VisualizationDetail } from "leafcutter-common";
|
import { VisualizationDetail } from "@link-stack/leafcutter-ui";
|
||||||
|
|
||||||
const getVisualization = async (visualizationID: string) => {
|
const getVisualization = async (visualizationID: string) => {
|
||||||
const node = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`;
|
const node = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`;
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,13 @@ import {
|
||||||
bindTrigger,
|
bindTrigger,
|
||||||
bindMenu,
|
bindMenu,
|
||||||
} from "material-ui-popup-state/hooks";
|
} from "material-ui-popup-state/hooks";
|
||||||
import { useAppContext } from "leafcutter-common/components/AppProvider";
|
import { useLeafcutterContext } from "@link-stack/leafcutter-ui/components/LeafcutterProvider";
|
||||||
|
|
||||||
export const AccountButton: FC = () => {
|
export const AccountButton: FC = () => {
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
const {
|
const {
|
||||||
colors: { leafcutterElectricBlue },
|
colors: { leafcutterElectricBlue },
|
||||||
} = useAppContext();
|
} = useLeafcutterContext();
|
||||||
const popupState = usePopupState({ variant: "popover", popupId: "account" });
|
const popupState = usePopupState({ variant: "popover", popupId: "account" });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue