Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
@@ -2,5 +2,7 @@ version = 1
|
|||||||
|
|
||||||
[services]
|
[services]
|
||||||
frontend = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui" }
|
frontend = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui" }
|
||||||
delivery_worker = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui" }
|
omni_outbound = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui" }
|
||||||
|
omni_inbound = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui" }
|
||||||
|
omni_chat = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui" }
|
||||||
langfuse = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui", compose_path = "langfuse/docker-compose.yml" }
|
langfuse = { deploy_mode = "dokploy_webhook", env_storage = "dokploy_ui", compose_path = "langfuse/docker-compose.yml" }
|
||||||
|
|||||||
104
docs/adr/0001-chat-platform-service-boundaries.md
Normal file
104
docs/adr/0001-chat-platform-service-boundaries.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# ADR-0001: Разделение Chat Platform на 3 сервиса
|
||||||
|
|
||||||
|
Дата: 2026-02-21
|
||||||
|
Статус: accepted
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
Сейчас delivery уже вынесен отдельно, но часть omni-интеграции остается в приложении `frontend`.
|
||||||
|
Нужна архитектура, где входящие вебхуки, доменная логика чатов и исходящая доставка развиваются независимо и не ломают друг друга.
|
||||||
|
|
||||||
|
Критичные требования:
|
||||||
|
|
||||||
|
- входящие webhook-события не теряются при рестартах;
|
||||||
|
- delivery управляет retry/rate-limit централизованно;
|
||||||
|
- omni_chat остается единственным местом доменной логики и хранения состояния диалогов;
|
||||||
|
- сервисы можно обновлять независимо.
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
Принимаем разделение на 3 сервиса:
|
||||||
|
|
||||||
|
1. `omni_inbound`
|
||||||
|
- Принимает вебхуки провайдеров.
|
||||||
|
- Валидирует подпись/секрет.
|
||||||
|
- Нормализует событие в универсальный envelope.
|
||||||
|
- Пишет событие в durable queue (`receiver.flow`) с идемпотентным `jobId`.
|
||||||
|
- Возвращает `200` только после успешной durable enqueue.
|
||||||
|
- Не содержит бизнес-логики CRM.
|
||||||
|
|
||||||
|
2. `omni_chat`
|
||||||
|
- Потребляет входящие события из `receiver.flow`.
|
||||||
|
- Разрешает идентичности и треды.
|
||||||
|
- Создает/обновляет `OmniMessage`, `OmniThread`, статусы и доменные эффекты.
|
||||||
|
- Формирует исходящие команды и кладет их в `sender.flow`.
|
||||||
|
|
||||||
|
3. `omni_outbound`
|
||||||
|
- Потребляет `sender.flow`.
|
||||||
|
- Выполняет отправку в провайдеров (Telegram Business и др.).
|
||||||
|
- Управляет retry/backoff/failover, DLQ и статусами доставки.
|
||||||
|
- Не содержит UI и доменной логики чатов.
|
||||||
|
|
||||||
|
## Почему webhook и delivery разделены
|
||||||
|
|
||||||
|
- Входящий контур должен отвечать быстро и предсказуемо.
|
||||||
|
- Исходящий контур живет с долгими retry и ограничениями провайдера.
|
||||||
|
- Сбой внешнего API не должен блокировать прием входящих сообщений.
|
||||||
|
|
||||||
|
## Границы ответственности
|
||||||
|
|
||||||
|
`omni_inbound`:
|
||||||
|
|
||||||
|
- можно: auth, валидация, нормализация, дедуп, enqueue;
|
||||||
|
- нельзя: запись доменных сущностей CRM, принятие продуктовых решений.
|
||||||
|
|
||||||
|
`omni_chat`:
|
||||||
|
|
||||||
|
- можно: вся доменная модель чатов, orchestration, бизнес-правила;
|
||||||
|
- нельзя: прямые вызовы провайдеров из sync API-контекста.
|
||||||
|
|
||||||
|
`omni_outbound`:
|
||||||
|
|
||||||
|
- можно: провайдерные адаптеры, retry, rate limits;
|
||||||
|
- нельзя: резолвинг бизнес-правил и маршрутизации диалога.
|
||||||
|
|
||||||
|
## Универсальный протокол событий
|
||||||
|
|
||||||
|
Внутренний контракт входящих событий: `docs/contracts/omni-inbound-envelope.v1.json`.
|
||||||
|
|
||||||
|
Обязательные поля:
|
||||||
|
|
||||||
|
- `version`
|
||||||
|
- `idempotencyKey`
|
||||||
|
- `provider`, `channel`, `direction`
|
||||||
|
- `providerEventId`, `providerMessageId`
|
||||||
|
- `eventType`, `occurredAt`, `receivedAt`
|
||||||
|
- `payloadRaw`, `payloadNormalized`
|
||||||
|
|
||||||
|
## Идемпотентность и надежность
|
||||||
|
|
||||||
|
- `jobId` в очереди строится из `idempotencyKey`.
|
||||||
|
- Дубликаты входящих webhook событий безопасны и возвращают `200`.
|
||||||
|
- `200` от `omni_inbound` отдается только после успешного добавления в Redis/BullMQ.
|
||||||
|
- При ошибке durable enqueue `omni_inbound` возвращает `5xx`, провайдер выполняет повторную доставку.
|
||||||
|
- Базовые рабочие очереди: `receiver.flow` и `sender.flow`; технические очереди для эскалации: `receiver.retry`, `sender.retry`, `receiver.dlq`, `sender.dlq`.
|
||||||
|
|
||||||
|
## Последствия
|
||||||
|
|
||||||
|
Плюсы:
|
||||||
|
|
||||||
|
- независимые релизы и масштабирование по ролям;
|
||||||
|
- меньше blast radius при инцидентах;
|
||||||
|
- проще подключать новые каналы поверх общего контракта.
|
||||||
|
|
||||||
|
Минусы:
|
||||||
|
|
||||||
|
- больше инфраструктурных компонентов (очереди, мониторинг, трассировка);
|
||||||
|
- требуется дисциплина по контрактам между сервисами.
|
||||||
|
|
||||||
|
## План внедрения
|
||||||
|
|
||||||
|
1. Вводим `omni_inbound` как отдельный сервис для Telegram Business.
|
||||||
|
2. Потребление `receiver.flow` реализуем в `omni_chat`.
|
||||||
|
3. Текущее исходящее API оставляем за `omni_outbound`.
|
||||||
|
4. После стабилизации выносим оставшиеся omni endpoint'ы из `frontend` в `omni_chat`/`omni_inbound`.
|
||||||
91
docs/contracts/omni-inbound-envelope.v1.json
Normal file
91
docs/contracts/omni-inbound-envelope.v1.json
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://crm.local/contracts/omni-inbound-envelope.v1.json",
|
||||||
|
"title": "OmniInboundEnvelopeV1",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"version",
|
||||||
|
"idempotencyKey",
|
||||||
|
"provider",
|
||||||
|
"channel",
|
||||||
|
"direction",
|
||||||
|
"providerEventId",
|
||||||
|
"providerMessageId",
|
||||||
|
"eventType",
|
||||||
|
"occurredAt",
|
||||||
|
"receivedAt",
|
||||||
|
"payloadRaw",
|
||||||
|
"payloadNormalized"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"version": {
|
||||||
|
"const": 1
|
||||||
|
},
|
||||||
|
"idempotencyKey": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 512
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 64
|
||||||
|
},
|
||||||
|
"channel": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["TELEGRAM", "WHATSAPP", "INSTAGRAM", "PHONE", "EMAIL", "INTERNAL"]
|
||||||
|
},
|
||||||
|
"direction": {
|
||||||
|
"const": "IN"
|
||||||
|
},
|
||||||
|
"providerEventId": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 256
|
||||||
|
},
|
||||||
|
"providerMessageId": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"maxLength": 256
|
||||||
|
},
|
||||||
|
"eventType": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 128
|
||||||
|
},
|
||||||
|
"occurredAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"receivedAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"payloadRaw": {
|
||||||
|
"type": ["object", "array", "string", "number", "boolean", "null"]
|
||||||
|
},
|
||||||
|
"payloadNormalized": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"required": ["threadExternalId", "contactExternalId", "text", "businessConnectionId"],
|
||||||
|
"properties": {
|
||||||
|
"threadExternalId": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"maxLength": 256
|
||||||
|
},
|
||||||
|
"contactExternalId": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"maxLength": 256
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"maxLength": 4096
|
||||||
|
},
|
||||||
|
"businessConnectionId": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"maxLength": 256
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
[phases.install]
|
|
||||||
cmds = ["npm ci --legacy-peer-deps"]
|
|
||||||
|
|
||||||
[phases.build]
|
|
||||||
cmds = ["npm run db:generate", "npm run build"]
|
|
||||||
|
|
||||||
[start]
|
|
||||||
cmd = "npm run preview -- --host 0.0.0.0 --port ${PORT:-3000}"
|
|
||||||
@@ -53,7 +53,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
queue: "omni-outbound",
|
queue: process.env.SENDER_FLOW_QUEUE_NAME || process.env.OUTBOUND_DELIVERY_QUEUE_NAME || "sender.flow",
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
omniMessageId,
|
omniMessageId,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
queue: "omni-outbound",
|
queue: process.env.SENDER_FLOW_QUEUE_NAME || process.env.OUTBOUND_DELIVERY_QUEUE_NAME || "sender.flow",
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
omniMessageId,
|
omniMessageId,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { Queue, Worker, type JobsOptions, type ConnectionOptions } from "bullmq"
|
|||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { prisma } from "../utils/prisma";
|
import { prisma } from "../utils/prisma";
|
||||||
|
|
||||||
export const OUTBOUND_DELIVERY_QUEUE_NAME = "omni-outbound";
|
export const OUTBOUND_DELIVERY_QUEUE_NAME = (
|
||||||
|
process.env.SENDER_FLOW_QUEUE_NAME ||
|
||||||
|
process.env.OUTBOUND_DELIVERY_QUEUE_NAME ||
|
||||||
|
"sender.flow"
|
||||||
|
).trim();
|
||||||
|
|
||||||
export type OutboundDeliveryJob = {
|
export type OutboundDeliveryJob = {
|
||||||
omniMessageId: string;
|
omniMessageId: string;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { startOutboundDeliveryWorker } from "./outboundDelivery";
|
import { OUTBOUND_DELIVERY_QUEUE_NAME, startOutboundDeliveryWorker } from "./outboundDelivery";
|
||||||
import { prisma } from "../utils/prisma";
|
import { prisma } from "../utils/prisma";
|
||||||
import { getRedis } from "../utils/redis";
|
import { getRedis } from "../utils/redis";
|
||||||
|
|
||||||
const worker = startOutboundDeliveryWorker();
|
const worker = startOutboundDeliveryWorker();
|
||||||
console.log("[delivery-worker] started queue omni:outbound");
|
console.log(`[omni_outbound(legacy-in-frontend)] started queue ${OUTBOUND_DELIVERY_QUEUE_NAME}`);
|
||||||
|
|
||||||
async function shutdown(signal: string) {
|
async function shutdown(signal: string) {
|
||||||
console.log(`[delivery-worker] shutting down by ${signal}`);
|
console.log(`[omni_outbound(legacy-in-frontend)] shutting down by ${signal}`);
|
||||||
try {
|
try {
|
||||||
await worker.close();
|
await worker.close();
|
||||||
} catch {
|
} catch {
|
||||||
@@ -32,4 +32,3 @@ process.on("SIGINT", () => {
|
|||||||
process.on("SIGTERM", () => {
|
process.on("SIGTERM", () => {
|
||||||
void shutdown("SIGTERM");
|
void shutdown("SIGTERM");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Submodule instructions updated: 19bbaf3e08...4ddd069d88
@@ -1,8 +0,0 @@
|
|||||||
[phases.install]
|
|
||||||
cmds = ["cd frontend && npm ci --legacy-peer-deps"]
|
|
||||||
|
|
||||||
[phases.build]
|
|
||||||
cmds = ["cd frontend && npm run db:generate", "cd frontend && npm run build"]
|
|
||||||
|
|
||||||
[start]
|
|
||||||
cmd = "cd frontend && npm run preview -- --host 0.0.0.0 --port ${PORT:-3000}"
|
|
||||||
10
omni_chat/Dockerfile
Normal file
10
omni_chat/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
FROM node:22-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD ["npm", "run", "start"]
|
||||||
21
omni_chat/README.md
Normal file
21
omni_chat/README.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# omni_chat
|
||||||
|
|
||||||
|
Изолированный сервис chat-core (домен диалогов).
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
- потребляет входящие события из `receiver.flow`;
|
||||||
|
- применяет бизнес-логику диалогов;
|
||||||
|
- публикует исходящие команды в `sender.flow`.
|
||||||
|
|
||||||
|
Текущий шаг: выделен отдельный сервисный контур и health endpoint.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- `GET /health`
|
||||||
|
|
||||||
|
## Переменные окружения
|
||||||
|
|
||||||
|
- `PORT` (default: `8090`)
|
||||||
|
- `RECEIVER_FLOW_QUEUE_NAME` (default: `receiver.flow`)
|
||||||
|
- `SENDER_FLOW_QUEUE_NAME` (default: `sender.flow`)
|
||||||
588
omni_chat/package-lock.json
generated
Normal file
588
omni_chat/package-lock.json
generated
Normal file
@@ -0,0 +1,588 @@
|
|||||||
|
{
|
||||||
|
"name": "crm-omni-chat",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "crm-omni-chat",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.13.9",
|
||||||
|
"tsx": "^4.20.5",
|
||||||
|
"typescript": "^5.9.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openharmony-arm64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "22.19.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz",
|
||||||
|
"integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.27.3",
|
||||||
|
"@esbuild/android-arm": "0.27.3",
|
||||||
|
"@esbuild/android-arm64": "0.27.3",
|
||||||
|
"@esbuild/android-x64": "0.27.3",
|
||||||
|
"@esbuild/darwin-arm64": "0.27.3",
|
||||||
|
"@esbuild/darwin-x64": "0.27.3",
|
||||||
|
"@esbuild/freebsd-arm64": "0.27.3",
|
||||||
|
"@esbuild/freebsd-x64": "0.27.3",
|
||||||
|
"@esbuild/linux-arm": "0.27.3",
|
||||||
|
"@esbuild/linux-arm64": "0.27.3",
|
||||||
|
"@esbuild/linux-ia32": "0.27.3",
|
||||||
|
"@esbuild/linux-loong64": "0.27.3",
|
||||||
|
"@esbuild/linux-mips64el": "0.27.3",
|
||||||
|
"@esbuild/linux-ppc64": "0.27.3",
|
||||||
|
"@esbuild/linux-riscv64": "0.27.3",
|
||||||
|
"@esbuild/linux-s390x": "0.27.3",
|
||||||
|
"@esbuild/linux-x64": "0.27.3",
|
||||||
|
"@esbuild/netbsd-arm64": "0.27.3",
|
||||||
|
"@esbuild/netbsd-x64": "0.27.3",
|
||||||
|
"@esbuild/openbsd-arm64": "0.27.3",
|
||||||
|
"@esbuild/openbsd-x64": "0.27.3",
|
||||||
|
"@esbuild/openharmony-arm64": "0.27.3",
|
||||||
|
"@esbuild/sunos-x64": "0.27.3",
|
||||||
|
"@esbuild/win32-arm64": "0.27.3",
|
||||||
|
"@esbuild/win32-ia32": "0.27.3",
|
||||||
|
"@esbuild/win32-x64": "0.27.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-tsconfig": {
|
||||||
|
"version": "4.13.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
|
||||||
|
"integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"resolve-pkg-maps": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/resolve-pkg-maps": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsx": {
|
||||||
|
"version": "4.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||||
|
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "~0.27.0",
|
||||||
|
"get-tsconfig": "^4.7.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"tsx": "dist/cli.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
omni_chat/package.json
Normal file
14
omni_chat/package.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "crm-omni-chat",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "tsx src/index.ts",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.13.9",
|
||||||
|
"tsx": "^4.20.5",
|
||||||
|
"typescript": "^5.9.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
36
omni_chat/src/index.ts
Normal file
36
omni_chat/src/index.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { createServer } from "node:http";
|
||||||
|
|
||||||
|
const port = Number(process.env.PORT || 8090);
|
||||||
|
const service = "omni_chat";
|
||||||
|
|
||||||
|
const server = createServer((req, res) => {
|
||||||
|
if (req.method === "GET" && req.url === "/health") {
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
ok: true,
|
||||||
|
service,
|
||||||
|
receiverFlow: process.env.RECEIVER_FLOW_QUEUE_NAME || "receiver.flow",
|
||||||
|
senderFlow: process.env.SENDER_FLOW_QUEUE_NAME || "sender.flow",
|
||||||
|
now: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
res.statusCode = 200;
|
||||||
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
||||||
|
res.end(payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
||||||
|
res.end(JSON.stringify({ ok: false, error: "not_found" }));
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(port, "0.0.0.0", () => {
|
||||||
|
console.log(`[omni_chat] listening on :${port}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
function shutdown(signal: string) {
|
||||||
|
console.log(`[omni_chat] shutting down by ${signal}`);
|
||||||
|
server.close(() => process.exit(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
||||||
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
||||||
10
omni_inbound/Dockerfile
Normal file
10
omni_inbound/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
FROM node:22-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD ["npm", "run", "start"]
|
||||||
47
omni_inbound/README.md
Normal file
47
omni_inbound/README.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# omni_inbound
|
||||||
|
|
||||||
|
Отдельный сервис приема входящих webhook-событий каналов (первый канал: Telegram Business).
|
||||||
|
|
||||||
|
## Задача сервиса
|
||||||
|
|
||||||
|
- принимать webhook;
|
||||||
|
- валидировать секрет;
|
||||||
|
- нормализовать событие в универсальный envelope;
|
||||||
|
- делать durable enqueue в BullMQ (`receiver.flow`);
|
||||||
|
- возвращать `200` только после успешного enqueue.
|
||||||
|
|
||||||
|
Сервис **не** содержит бизнес-логику CRM и не вызывает provider API для исходящих сообщений.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### `GET /health`
|
||||||
|
Проверка живости сервиса.
|
||||||
|
|
||||||
|
### `POST /webhooks/telegram/business`
|
||||||
|
Прием Telegram Business webhook.
|
||||||
|
|
||||||
|
При активном `TELEGRAM_WEBHOOK_SECRET` ожидается заголовок:
|
||||||
|
|
||||||
|
- `x-telegram-bot-api-secret-token: <TELEGRAM_WEBHOOK_SECRET>`
|
||||||
|
|
||||||
|
## Переменные окружения
|
||||||
|
|
||||||
|
- `PORT` (default: `8080`)
|
||||||
|
- `REDIS_URL` (default: `redis://localhost:6379`)
|
||||||
|
- `RECEIVER_FLOW_QUEUE_NAME` (default: `receiver.flow`)
|
||||||
|
- `INBOUND_QUEUE_NAME` (legacy alias, optional)
|
||||||
|
- `TELEGRAM_WEBHOOK_SECRET` (optional, но обязателен для production)
|
||||||
|
- `MAX_BODY_SIZE_BYTES` (default: `1048576`)
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm ci
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Надежность
|
||||||
|
|
||||||
|
- Идемпотентность: `jobId` строится из `idempotencyKey` (SHA-256).
|
||||||
|
- Дубликаты webhook безопасны и не приводят к повторной постановке события.
|
||||||
|
- При ошибке enqueue сервис возвращает `503`, чтобы провайдер повторил доставку.
|
||||||
908
omni_inbound/package-lock.json
generated
Normal file
908
omni_inbound/package-lock.json
generated
Normal file
@@ -0,0 +1,908 @@
|
|||||||
|
{
|
||||||
|
"name": "crm-omni-inbound",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "crm-omni-inbound",
|
||||||
|
"dependencies": {
|
||||||
|
"bullmq": "^5.58.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.13.9",
|
||||||
|
"tsx": "^4.20.5",
|
||||||
|
"typescript": "^5.9.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openharmony-arm64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ioredis/commands": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "22.19.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz",
|
||||||
|
"integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bullmq": {
|
||||||
|
"version": "5.69.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.69.4.tgz",
|
||||||
|
"integrity": "sha512-Lp7ymp875I/rtjMm6oxzQ3PrvDDHkgge0oaAznmZsKtGyglfdrg9zbidPSszTXgWFkS2rCgMcTRNJfM3uUMOjQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cron-parser": "4.9.0",
|
||||||
|
"ioredis": "5.9.2",
|
||||||
|
"msgpackr": "1.11.5",
|
||||||
|
"node-abort-controller": "3.1.1",
|
||||||
|
"semver": "7.7.4",
|
||||||
|
"tslib": "2.8.1",
|
||||||
|
"uuid": "11.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cluster-key-slot": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cron-parser": {
|
||||||
|
"version": "4.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
|
||||||
|
"integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"luxon": "^3.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/denque": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/detect-libc": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||||
|
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.27.3",
|
||||||
|
"@esbuild/android-arm": "0.27.3",
|
||||||
|
"@esbuild/android-arm64": "0.27.3",
|
||||||
|
"@esbuild/android-x64": "0.27.3",
|
||||||
|
"@esbuild/darwin-arm64": "0.27.3",
|
||||||
|
"@esbuild/darwin-x64": "0.27.3",
|
||||||
|
"@esbuild/freebsd-arm64": "0.27.3",
|
||||||
|
"@esbuild/freebsd-x64": "0.27.3",
|
||||||
|
"@esbuild/linux-arm": "0.27.3",
|
||||||
|
"@esbuild/linux-arm64": "0.27.3",
|
||||||
|
"@esbuild/linux-ia32": "0.27.3",
|
||||||
|
"@esbuild/linux-loong64": "0.27.3",
|
||||||
|
"@esbuild/linux-mips64el": "0.27.3",
|
||||||
|
"@esbuild/linux-ppc64": "0.27.3",
|
||||||
|
"@esbuild/linux-riscv64": "0.27.3",
|
||||||
|
"@esbuild/linux-s390x": "0.27.3",
|
||||||
|
"@esbuild/linux-x64": "0.27.3",
|
||||||
|
"@esbuild/netbsd-arm64": "0.27.3",
|
||||||
|
"@esbuild/netbsd-x64": "0.27.3",
|
||||||
|
"@esbuild/openbsd-arm64": "0.27.3",
|
||||||
|
"@esbuild/openbsd-x64": "0.27.3",
|
||||||
|
"@esbuild/openharmony-arm64": "0.27.3",
|
||||||
|
"@esbuild/sunos-x64": "0.27.3",
|
||||||
|
"@esbuild/win32-arm64": "0.27.3",
|
||||||
|
"@esbuild/win32-ia32": "0.27.3",
|
||||||
|
"@esbuild/win32-x64": "0.27.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-tsconfig": {
|
||||||
|
"version": "4.13.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
|
||||||
|
"integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"resolve-pkg-maps": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ioredis": {
|
||||||
|
"version": "5.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.2.tgz",
|
||||||
|
"integrity": "sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@ioredis/commands": "1.5.0",
|
||||||
|
"cluster-key-slot": "^1.1.0",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"denque": "^2.1.0",
|
||||||
|
"lodash.defaults": "^4.2.0",
|
||||||
|
"lodash.isarguments": "^3.1.0",
|
||||||
|
"redis-errors": "^1.2.0",
|
||||||
|
"redis-parser": "^3.0.0",
|
||||||
|
"standard-as-callback": "^2.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.22.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/ioredis"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lodash.defaults": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isarguments": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/luxon": {
|
||||||
|
"version": "3.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
|
||||||
|
"integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/msgpackr": {
|
||||||
|
"version": "1.11.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz",
|
||||||
|
"integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"msgpackr-extract": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/msgpackr-extract": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"node-gyp-build-optional-packages": "5.2.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"download-msgpackr-prebuilds": "bin/download-prebuilds.js"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
|
||||||
|
"@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
|
||||||
|
"@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
|
||||||
|
"@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
|
||||||
|
"@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
|
||||||
|
"@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-abort-controller": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/node-gyp-build-optional-packages": {
|
||||||
|
"version": "5.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
|
||||||
|
"integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"node-gyp-build-optional-packages": "bin.js",
|
||||||
|
"node-gyp-build-optional-packages-optional": "optional.js",
|
||||||
|
"node-gyp-build-optional-packages-test": "build-test.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/redis-errors": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/redis-parser": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"redis-errors": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/resolve-pkg-maps": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "7.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||||
|
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/standard-as-callback": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
|
"node_modules/tsx": {
|
||||||
|
"version": "4.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||||
|
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "~0.27.0",
|
||||||
|
"get-tsconfig": "^4.7.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"tsx": "dist/cli.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "11.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||||
|
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/esm/bin/uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
omni_inbound/package.json
Normal file
17
omni_inbound/package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "crm-omni-inbound",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "tsx src/index.ts",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bullmq": "^5.58.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.13.9",
|
||||||
|
"tsx": "^4.20.5",
|
||||||
|
"typescript": "^5.9.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
38
omni_inbound/src/index.ts
Normal file
38
omni_inbound/src/index.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { closeInboundQueue } from "./queue";
|
||||||
|
import { startServer } from "./server";
|
||||||
|
|
||||||
|
const server = startServer();
|
||||||
|
|
||||||
|
async function shutdown(signal: string) {
|
||||||
|
console.log(`[omni_inbound] shutting down by ${signal}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server.close((error) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// ignore shutdown errors
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await closeInboundQueue();
|
||||||
|
} catch {
|
||||||
|
// ignore shutdown errors
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
void shutdown("SIGINT");
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
void shutdown("SIGTERM");
|
||||||
|
});
|
||||||
67
omni_inbound/src/queue.ts
Normal file
67
omni_inbound/src/queue.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { Queue, type ConnectionOptions } from "bullmq";
|
||||||
|
import type { OmniInboundEnvelopeV1 } from "./types";
|
||||||
|
|
||||||
|
export const RECEIVER_FLOW_QUEUE_NAME = (
|
||||||
|
process.env.RECEIVER_FLOW_QUEUE_NAME ||
|
||||||
|
process.env.INBOUND_QUEUE_NAME ||
|
||||||
|
"receiver.flow"
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
let queueInstance: Queue<OmniInboundEnvelopeV1, unknown, "ingest"> | null = null;
|
||||||
|
|
||||||
|
function redisConnectionFromEnv(): ConnectionOptions {
|
||||||
|
const raw = (process.env.REDIS_URL || "redis://localhost:6379").trim();
|
||||||
|
const parsed = new URL(raw);
|
||||||
|
|
||||||
|
return {
|
||||||
|
host: parsed.hostname,
|
||||||
|
port: parsed.port ? Number(parsed.port) : 6379,
|
||||||
|
username: parsed.username ? decodeURIComponent(parsed.username) : undefined,
|
||||||
|
password: parsed.password ? decodeURIComponent(parsed.password) : undefined,
|
||||||
|
db: parsed.pathname && parsed.pathname !== "/" ? Number(parsed.pathname.slice(1)) : undefined,
|
||||||
|
maxRetriesPerRequest: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toJobId(idempotencyKey: string) {
|
||||||
|
const hash = createHash("sha256").update(idempotencyKey).digest("hex");
|
||||||
|
return `inbound:${hash}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inboundQueue() {
|
||||||
|
if (queueInstance) return queueInstance;
|
||||||
|
|
||||||
|
queueInstance = new Queue<OmniInboundEnvelopeV1, unknown, "ingest">(RECEIVER_FLOW_QUEUE_NAME, {
|
||||||
|
connection: redisConnectionFromEnv(),
|
||||||
|
defaultJobOptions: {
|
||||||
|
removeOnComplete: { count: 10000 },
|
||||||
|
removeOnFail: { count: 20000 },
|
||||||
|
attempts: 8,
|
||||||
|
backoff: { type: "exponential", delay: 1000 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return queueInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function enqueueInboundEvent(envelope: OmniInboundEnvelopeV1) {
|
||||||
|
const q = inboundQueue();
|
||||||
|
const jobId = toJobId(envelope.idempotencyKey);
|
||||||
|
|
||||||
|
return q.add("ingest", envelope, {
|
||||||
|
jobId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDuplicateJobError(error: unknown) {
|
||||||
|
if (!error || typeof error !== "object") return false;
|
||||||
|
const message = String((error as { message?: string }).message || "").toLowerCase();
|
||||||
|
return message.includes("job") && message.includes("exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function closeInboundQueue() {
|
||||||
|
if (!queueInstance) return;
|
||||||
|
await queueInstance.close();
|
||||||
|
queueInstance = null;
|
||||||
|
}
|
||||||
108
omni_inbound/src/server.ts
Normal file
108
omni_inbound/src/server.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
||||||
|
import { RECEIVER_FLOW_QUEUE_NAME, enqueueInboundEvent, isDuplicateJobError } from "./queue";
|
||||||
|
import { parseTelegramBusinessUpdate } from "./telegram";
|
||||||
|
|
||||||
|
const MAX_BODY_SIZE_BYTES = Number(process.env.MAX_BODY_SIZE_BYTES || 1024 * 1024);
|
||||||
|
|
||||||
|
function writeJson(res: ServerResponse, statusCode: number, body: unknown) {
|
||||||
|
const payload = JSON.stringify(body);
|
||||||
|
res.statusCode = statusCode;
|
||||||
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
||||||
|
res.end(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readJsonBody(req: IncomingMessage): Promise<unknown> {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
for await (const chunk of req) {
|
||||||
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||||
|
total += buf.length;
|
||||||
|
if (total > MAX_BODY_SIZE_BYTES) {
|
||||||
|
throw new Error(`payload_too_large:${MAX_BODY_SIZE_BYTES}`);
|
||||||
|
}
|
||||||
|
chunks.push(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = Buffer.concat(chunks).toString("utf8");
|
||||||
|
if (!raw) return {};
|
||||||
|
return JSON.parse(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateTelegramSecret(req: IncomingMessage): boolean {
|
||||||
|
const expected = (process.env.TELEGRAM_WEBHOOK_SECRET || "").trim();
|
||||||
|
if (!expected) return true;
|
||||||
|
|
||||||
|
const incoming = String(req.headers["x-telegram-bot-api-secret-token"] || "").trim();
|
||||||
|
return incoming !== "" && incoming === expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startServer() {
|
||||||
|
const port = Number(process.env.PORT || 8080);
|
||||||
|
|
||||||
|
const server = createServer(async (req, res) => {
|
||||||
|
if (!req.url || !req.method) {
|
||||||
|
writeJson(res, 404, { ok: false, error: "not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.url === "/health" && req.method === "GET") {
|
||||||
|
writeJson(res, 200, {
|
||||||
|
ok: true,
|
||||||
|
service: "omni_inbound",
|
||||||
|
queue: RECEIVER_FLOW_QUEUE_NAME,
|
||||||
|
now: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.url === "/webhooks/telegram/business" && req.method === "POST") {
|
||||||
|
if (!validateTelegramSecret(req)) {
|
||||||
|
writeJson(res, 401, { ok: false, error: "invalid_webhook_secret" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await readJsonBody(req);
|
||||||
|
const envelope = parseTelegramBusinessUpdate(body);
|
||||||
|
|
||||||
|
await enqueueInboundEvent(envelope);
|
||||||
|
|
||||||
|
writeJson(res, 200, {
|
||||||
|
ok: true,
|
||||||
|
queued: true,
|
||||||
|
duplicate: false,
|
||||||
|
providerEventId: envelope.providerEventId,
|
||||||
|
idempotencyKey: envelope.idempotencyKey,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (isDuplicateJobError(error)) {
|
||||||
|
writeJson(res, 200, {
|
||||||
|
ok: true,
|
||||||
|
queued: false,
|
||||||
|
duplicate: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
const statusCode = message.startsWith("payload_too_large:") ? 413 : 503;
|
||||||
|
writeJson(res, statusCode, {
|
||||||
|
ok: false,
|
||||||
|
error: "receiver_enqueue_failed",
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJson(res, 404, { ok: false, error: "not_found" });
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(port, "0.0.0.0", () => {
|
||||||
|
console.log(`[omni_inbound] listening on :${port}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
127
omni_inbound/src/telegram.ts
Normal file
127
omni_inbound/src/telegram.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import type { JsonObject, OmniInboundEnvelopeV1 } from "./types";
|
||||||
|
|
||||||
|
const MAX_TEXT_LENGTH = 4096;
|
||||||
|
|
||||||
|
function asObject(value: unknown): JsonObject {
|
||||||
|
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonObject) : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickMessage(update: JsonObject): JsonObject {
|
||||||
|
const candidates = [
|
||||||
|
update.message,
|
||||||
|
update.edited_message,
|
||||||
|
update.business_message,
|
||||||
|
update.edited_business_message,
|
||||||
|
update.channel_post,
|
||||||
|
update.edited_channel_post,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const obj = asObject(candidate);
|
||||||
|
if (Object.keys(obj).length > 0) return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickEventType(update: JsonObject): string {
|
||||||
|
if (update.business_message) return "business_message";
|
||||||
|
if (update.edited_business_message) return "edited_business_message";
|
||||||
|
if (update.business_connection) return "business_connection";
|
||||||
|
if (update.deleted_business_messages) return "deleted_business_messages";
|
||||||
|
if (update.message) return "message";
|
||||||
|
if (update.edited_message) return "edited_message";
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isoFromUnix(value: unknown) {
|
||||||
|
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
||||||
|
return new Date(value * 1000).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cropText(value: unknown) {
|
||||||
|
if (typeof value !== "string") return null;
|
||||||
|
return value.slice(0, MAX_TEXT_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireString(value: unknown, fallback: string) {
|
||||||
|
const v = String(value ?? "").trim();
|
||||||
|
return v || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeFallbackEventId(raw: unknown) {
|
||||||
|
return createHash("sha256").update(JSON.stringify(raw ?? null)).digest("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseTelegramBusinessUpdate(raw: unknown): OmniInboundEnvelopeV1 {
|
||||||
|
const update = asObject(raw);
|
||||||
|
const message = pickMessage(update);
|
||||||
|
const receivedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
const updateId = update.update_id;
|
||||||
|
const messageId = message.message_id;
|
||||||
|
const businessConnection = asObject(update.business_connection);
|
||||||
|
|
||||||
|
const providerEventId =
|
||||||
|
(updateId != null && requireString(updateId, "")) ||
|
||||||
|
(messageId != null && requireString(messageId, "")) ||
|
||||||
|
makeFallbackEventId(raw);
|
||||||
|
|
||||||
|
const providerMessageId = messageId != null ? String(messageId) : null;
|
||||||
|
|
||||||
|
const chat = asObject(message.chat);
|
||||||
|
const from = asObject(message.from);
|
||||||
|
|
||||||
|
const threadExternalId =
|
||||||
|
chat.id != null
|
||||||
|
? String(chat.id)
|
||||||
|
: businessConnection.user_chat_id != null
|
||||||
|
? String(businessConnection.user_chat_id)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const contactExternalId = from.id != null ? String(from.id) : null;
|
||||||
|
|
||||||
|
const text = cropText(message.text) ?? cropText(message.caption);
|
||||||
|
|
||||||
|
const businessConnectionId =
|
||||||
|
message.business_connection_id != null
|
||||||
|
? String(message.business_connection_id)
|
||||||
|
: businessConnection.id != null
|
||||||
|
? String(businessConnection.id)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const occurredAt =
|
||||||
|
isoFromUnix(message.date) ??
|
||||||
|
isoFromUnix(businessConnection.date) ??
|
||||||
|
receivedAt;
|
||||||
|
|
||||||
|
const eventType = pickEventType(update);
|
||||||
|
|
||||||
|
const idempotencyKey = ["telegram_business", providerEventId, businessConnectionId || "no-bc"].join(":");
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: 1,
|
||||||
|
idempotencyKey,
|
||||||
|
provider: "telegram_business",
|
||||||
|
channel: "TELEGRAM",
|
||||||
|
direction: "IN",
|
||||||
|
providerEventId,
|
||||||
|
providerMessageId,
|
||||||
|
eventType,
|
||||||
|
occurredAt,
|
||||||
|
receivedAt,
|
||||||
|
payloadRaw: raw,
|
||||||
|
payloadNormalized: {
|
||||||
|
threadExternalId,
|
||||||
|
contactExternalId,
|
||||||
|
text,
|
||||||
|
businessConnectionId,
|
||||||
|
updateId: updateId != null ? String(updateId) : null,
|
||||||
|
chatTitle: typeof chat.title === "string" ? chat.title : null,
|
||||||
|
fromUsername: typeof from.username === "string" ? from.username : null,
|
||||||
|
fromFirstName: typeof from.first_name === "string" ? from.first_name : null,
|
||||||
|
fromLastName: typeof from.last_name === "string" ? from.last_name : null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
24
omni_inbound/src/types.ts
Normal file
24
omni_inbound/src/types.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export type OmniInboundChannel = "TELEGRAM" | "WHATSAPP" | "INSTAGRAM" | "PHONE" | "EMAIL" | "INTERNAL";
|
||||||
|
|
||||||
|
export type OmniInboundEnvelopeV1 = {
|
||||||
|
version: 1;
|
||||||
|
idempotencyKey: string;
|
||||||
|
provider: string;
|
||||||
|
channel: OmniInboundChannel;
|
||||||
|
direction: "IN";
|
||||||
|
providerEventId: string;
|
||||||
|
providerMessageId: string | null;
|
||||||
|
eventType: string;
|
||||||
|
occurredAt: string;
|
||||||
|
receivedAt: string;
|
||||||
|
payloadRaw: unknown;
|
||||||
|
payloadNormalized: {
|
||||||
|
threadExternalId: string | null;
|
||||||
|
contactExternalId: string | null;
|
||||||
|
text: string | null;
|
||||||
|
businessConnectionId: string | null;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JsonObject = Record<string, unknown>;
|
||||||
14
omni_inbound/tsconfig.json
Normal file
14
omni_inbound/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"verbatimModuleSyntax": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
16
omni_outbound/README.md
Normal file
16
omni_outbound/README.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# omni_outbound
|
||||||
|
|
||||||
|
Изолированный сервис исходящей доставки.
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
- потребляет задачи из `sender.flow`;
|
||||||
|
- выполняет отправку в провайдеров;
|
||||||
|
- применяет retry/backoff и финальный fail-status.
|
||||||
|
|
||||||
|
## Переменные окружения
|
||||||
|
|
||||||
|
- `REDIS_URL`
|
||||||
|
- `DATABASE_URL`
|
||||||
|
- `SENDER_FLOW_QUEUE_NAME` (default: `sender.flow`)
|
||||||
|
- `OUTBOUND_DELIVERY_QUEUE_NAME` (legacy alias, optional)
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "crm-delivery",
|
"name": "crm-omni-outbound",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "crm-delivery",
|
"name": "crm-omni-outbound",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.16.1",
|
"@prisma/client": "^6.16.1",
|
||||||
"bullmq": "^5.58.2",
|
"bullmq": "^5.58.2",
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "crm-delivery",
|
"name": "crm-omni-outbound",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -2,7 +2,11 @@ import { Queue, Worker, type Job, type JobsOptions, type ConnectionOptions } fro
|
|||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { prisma } from "../utils/prisma";
|
import { prisma } from "../utils/prisma";
|
||||||
|
|
||||||
export const OUTBOUND_DELIVERY_QUEUE_NAME = "omni-outbound";
|
export const OUTBOUND_DELIVERY_QUEUE_NAME = (
|
||||||
|
process.env.SENDER_FLOW_QUEUE_NAME ||
|
||||||
|
process.env.OUTBOUND_DELIVERY_QUEUE_NAME ||
|
||||||
|
"sender.flow"
|
||||||
|
).trim();
|
||||||
|
|
||||||
export type OutboundDeliveryJob = {
|
export type OutboundDeliveryJob = {
|
||||||
omniMessageId: string;
|
omniMessageId: string;
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { startOutboundDeliveryWorker } from "./queues/outboundDelivery";
|
import { OUTBOUND_DELIVERY_QUEUE_NAME, startOutboundDeliveryWorker } from "./queues/outboundDelivery";
|
||||||
import { prisma } from "./utils/prisma";
|
import { prisma } from "./utils/prisma";
|
||||||
import { getRedis } from "./utils/redis";
|
import { getRedis } from "./utils/redis";
|
||||||
|
|
||||||
const worker = startOutboundDeliveryWorker();
|
const worker = startOutboundDeliveryWorker();
|
||||||
console.log("[delivery-worker] started queue omni:outbound");
|
console.log(`[omni_outbound] started queue ${OUTBOUND_DELIVERY_QUEUE_NAME}`);
|
||||||
|
|
||||||
async function shutdown(signal: string) {
|
async function shutdown(signal: string) {
|
||||||
console.log(`[delivery-worker] shutting down by ${signal}`);
|
console.log(`[omni_outbound] shutting down by ${signal}`);
|
||||||
try {
|
try {
|
||||||
await worker.close();
|
await worker.close();
|
||||||
} catch {
|
} catch {
|
||||||
14
omni_outbound/tsconfig.json
Normal file
14
omni_outbound/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"verbatimModuleSyntax": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
79
research/chatwoot/.all-contributorsrc
Normal file
79
research/chatwoot/.all-contributorsrc
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"docs/contributors.md"
|
||||||
|
],
|
||||||
|
"imageSize": 48,
|
||||||
|
"commit": false,
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"login": "nithindavid",
|
||||||
|
"name": "Nithin David Thomas",
|
||||||
|
"avatar_url": "https://avatars2.githubusercontent.com/u/1277421?v=4",
|
||||||
|
"profile": "http://nithindavid.me",
|
||||||
|
"contributions": [
|
||||||
|
"bug",
|
||||||
|
"blog",
|
||||||
|
"code",
|
||||||
|
"doc",
|
||||||
|
"design",
|
||||||
|
"maintenance",
|
||||||
|
"review"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"login": "sojan-official",
|
||||||
|
"name": "Sojan Jose",
|
||||||
|
"avatar_url": "https://avatars1.githubusercontent.com/u/73185?v=4",
|
||||||
|
"profile": "http://sojan.me",
|
||||||
|
"contributions": [
|
||||||
|
"bug",
|
||||||
|
"blog",
|
||||||
|
"code",
|
||||||
|
"doc",
|
||||||
|
"design",
|
||||||
|
"maintenance",
|
||||||
|
"review"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "pranavrajs",
|
||||||
|
"name": "Pranav Raj S",
|
||||||
|
"avatar_url": "https://avatars3.githubusercontent.com/u/2246121?v=4",
|
||||||
|
"profile": "https://github.com/pranavrajs",
|
||||||
|
"contributions": [
|
||||||
|
"bug",
|
||||||
|
"blog",
|
||||||
|
"code",
|
||||||
|
"doc",
|
||||||
|
"design",
|
||||||
|
"maintenance",
|
||||||
|
"review"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "subintp",
|
||||||
|
"name": "Subin T P",
|
||||||
|
"avatar_url": "https://avatars1.githubusercontent.com/u/1742357?v=4",
|
||||||
|
"profile": "http://www.linkedin.com/in/subintp",
|
||||||
|
"contributions": [
|
||||||
|
"bug",
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "manojmj92",
|
||||||
|
"name": "Manoj M J",
|
||||||
|
"avatar_url": "https://avatars1.githubusercontent.com/u/4034241?v=4",
|
||||||
|
"profile": "https://github.com/manojmj92",
|
||||||
|
"contributions": [
|
||||||
|
"bug",
|
||||||
|
"code",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"contributorsPerLine": 7,
|
||||||
|
"projectName": "chatwoot",
|
||||||
|
"projectOwner": "chatwoot",
|
||||||
|
"repoType": "github",
|
||||||
|
"repoHost": "https://github.com"
|
||||||
|
}
|
||||||
1
research/chatwoot/.browserslistrc
Normal file
1
research/chatwoot/.browserslistrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
defaults
|
||||||
3
research/chatwoot/.bundler-audit.yml
Normal file
3
research/chatwoot/.bundler-audit.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
ignore:
|
||||||
|
- CVE-2021-41098 # https://github.com/chatwoot/chatwoot/issues/3097 (update once azure blob storage is updated)
|
||||||
374
research/chatwoot/.circleci/config.yml
Normal file
374
research/chatwoot/.circleci/config.yml
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
version: 2.1
|
||||||
|
orbs:
|
||||||
|
node: circleci/node@6.1.0
|
||||||
|
qlty-orb: qltysh/qlty-orb@0.0
|
||||||
|
|
||||||
|
# Shared defaults for setup steps
|
||||||
|
defaults: &defaults
|
||||||
|
working_directory: ~/build
|
||||||
|
machine:
|
||||||
|
image: ubuntu-2204:2024.05.1
|
||||||
|
resource_class: large
|
||||||
|
environment:
|
||||||
|
RAILS_LOG_TO_STDOUT: false
|
||||||
|
COVERAGE: true
|
||||||
|
LOG_LEVEL: warn
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Separate job for linting (no parallelism needed)
|
||||||
|
lint:
|
||||||
|
<<: *defaults
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
|
||||||
|
# Install minimal system dependencies for linting
|
||||||
|
- run:
|
||||||
|
name: Install System Dependencies
|
||||||
|
command: |
|
||||||
|
sudo apt-get update
|
||||||
|
DEBIAN_FRONTEND=noninteractive sudo apt-get install -y \
|
||||||
|
libpq-dev \
|
||||||
|
build-essential \
|
||||||
|
git \
|
||||||
|
curl \
|
||||||
|
libssl-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
libreadline-dev \
|
||||||
|
libyaml-dev \
|
||||||
|
openjdk-11-jdk \
|
||||||
|
jq \
|
||||||
|
software-properties-common \
|
||||||
|
ca-certificates \
|
||||||
|
imagemagick \
|
||||||
|
libxml2-dev \
|
||||||
|
libxslt1-dev \
|
||||||
|
file \
|
||||||
|
g++ \
|
||||||
|
gcc \
|
||||||
|
autoconf \
|
||||||
|
gnupg2 \
|
||||||
|
patch \
|
||||||
|
ruby-dev \
|
||||||
|
liblzma-dev \
|
||||||
|
libgmp-dev \
|
||||||
|
libncurses5-dev \
|
||||||
|
libffi-dev \
|
||||||
|
libgdbm6 \
|
||||||
|
libgdbm-dev \
|
||||||
|
libvips
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Install RVM and Ruby 3.4.4
|
||||||
|
command: |
|
||||||
|
sudo apt-get install -y gpg
|
||||||
|
gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
|
||||||
|
\curl -sSL https://get.rvm.io | bash -s stable
|
||||||
|
echo 'source ~/.rvm/scripts/rvm' >> $BASH_ENV
|
||||||
|
source ~/.rvm/scripts/rvm
|
||||||
|
rvm install "3.4.4"
|
||||||
|
rvm use 3.4.4 --default
|
||||||
|
gem install bundler -v 2.5.16
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Install Application Dependencies
|
||||||
|
command: |
|
||||||
|
source ~/.rvm/scripts/rvm
|
||||||
|
bundle install
|
||||||
|
|
||||||
|
- node/install:
|
||||||
|
node-version: '24.13'
|
||||||
|
- node/install-pnpm
|
||||||
|
- node/install-packages:
|
||||||
|
pkg-manager: pnpm
|
||||||
|
override-ci-command: pnpm i
|
||||||
|
|
||||||
|
# Swagger verification
|
||||||
|
- run:
|
||||||
|
name: Verify swagger API specification
|
||||||
|
command: |
|
||||||
|
bundle exec rake swagger:build
|
||||||
|
if [[ `git status swagger/swagger.json --porcelain` ]]
|
||||||
|
then
|
||||||
|
echo "ERROR: The swagger.json file is not in sync with the yaml specification. Run 'rake swagger:build' and commit 'swagger/swagger.json'."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p ~/tmp
|
||||||
|
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar
|
||||||
|
java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json
|
||||||
|
|
||||||
|
# Bundle audit
|
||||||
|
- run:
|
||||||
|
name: Bundle audit
|
||||||
|
command: bundle exec bundle audit update && bundle exec bundle audit check -v
|
||||||
|
|
||||||
|
# Rubocop linting
|
||||||
|
- run:
|
||||||
|
name: Rubocop
|
||||||
|
command: bundle exec rubocop --parallel
|
||||||
|
|
||||||
|
# ESLint linting
|
||||||
|
- run:
|
||||||
|
name: eslint
|
||||||
|
command: pnpm run eslint
|
||||||
|
|
||||||
|
# Separate job for frontend tests
|
||||||
|
frontend-tests:
|
||||||
|
<<: *defaults
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- node/install:
|
||||||
|
node-version: '24.13'
|
||||||
|
- node/install-pnpm
|
||||||
|
- node/install-packages:
|
||||||
|
pkg-manager: pnpm
|
||||||
|
override-ci-command: pnpm i
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Run frontend tests (with coverage)
|
||||||
|
command: pnpm run test:coverage
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Move coverage files if they exist
|
||||||
|
command: |
|
||||||
|
if [ -d "coverage" ]; then
|
||||||
|
mkdir -p ~/build/coverage
|
||||||
|
cp -r coverage ~/build/coverage/frontend || true
|
||||||
|
fi
|
||||||
|
when: always
|
||||||
|
|
||||||
|
- persist_to_workspace:
|
||||||
|
root: ~/build
|
||||||
|
paths:
|
||||||
|
- coverage
|
||||||
|
|
||||||
|
# Backend tests with parallelization
|
||||||
|
backend-tests:
|
||||||
|
<<: *defaults
|
||||||
|
parallelism: 18
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- node/install:
|
||||||
|
node-version: '24.13'
|
||||||
|
- node/install-pnpm
|
||||||
|
- node/install-packages:
|
||||||
|
pkg-manager: pnpm
|
||||||
|
override-ci-command: pnpm i
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Add PostgreSQL repository and update
|
||||||
|
command: |
|
||||||
|
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
||||||
|
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
|
||||||
|
sudo apt-get update -y
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Install System Dependencies
|
||||||
|
command: |
|
||||||
|
sudo apt-get update
|
||||||
|
DEBIAN_FRONTEND=noninteractive sudo apt-get install -y \
|
||||||
|
libpq-dev \
|
||||||
|
redis-server \
|
||||||
|
postgresql-common \
|
||||||
|
postgresql-16 \
|
||||||
|
postgresql-16-pgvector \
|
||||||
|
build-essential \
|
||||||
|
git \
|
||||||
|
curl \
|
||||||
|
libssl-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
libreadline-dev \
|
||||||
|
libyaml-dev \
|
||||||
|
openjdk-11-jdk \
|
||||||
|
jq \
|
||||||
|
software-properties-common \
|
||||||
|
ca-certificates \
|
||||||
|
imagemagick \
|
||||||
|
libxml2-dev \
|
||||||
|
libxslt1-dev \
|
||||||
|
file \
|
||||||
|
g++ \
|
||||||
|
gcc \
|
||||||
|
autoconf \
|
||||||
|
gnupg2 \
|
||||||
|
patch \
|
||||||
|
ruby-dev \
|
||||||
|
liblzma-dev \
|
||||||
|
libgmp-dev \
|
||||||
|
libncurses5-dev \
|
||||||
|
libffi-dev \
|
||||||
|
libgdbm6 \
|
||||||
|
libgdbm-dev \
|
||||||
|
libvips
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Install RVM and Ruby 3.4.4
|
||||||
|
command: |
|
||||||
|
sudo apt-get install -y gpg
|
||||||
|
gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
|
||||||
|
\curl -sSL https://get.rvm.io | bash -s stable
|
||||||
|
echo 'source ~/.rvm/scripts/rvm' >> $BASH_ENV
|
||||||
|
source ~/.rvm/scripts/rvm
|
||||||
|
rvm install "3.4.4"
|
||||||
|
rvm use 3.4.4 --default
|
||||||
|
gem install bundler -v 2.5.16
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Install Application Dependencies
|
||||||
|
command: |
|
||||||
|
source ~/.rvm/scripts/rvm
|
||||||
|
bundle install
|
||||||
|
|
||||||
|
# Install and configure OpenSearch
|
||||||
|
- run:
|
||||||
|
name: Install OpenSearch
|
||||||
|
command: |
|
||||||
|
# Download and install OpenSearch 2.11.0 (compatible with Elasticsearch 7.x clients)
|
||||||
|
wget https://artifacts.opensearch.org/releases/bundle/opensearch/2.11.0/opensearch-2.11.0-linux-x64.tar.gz
|
||||||
|
tar -xzf opensearch-2.11.0-linux-x64.tar.gz
|
||||||
|
sudo mv opensearch-2.11.0 /opt/opensearch
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Configure and Start OpenSearch
|
||||||
|
command: |
|
||||||
|
# Configure OpenSearch for single-node testing
|
||||||
|
cat > /opt/opensearch/config/opensearch.yml \<< EOF
|
||||||
|
cluster.name: chatwoot-test
|
||||||
|
node.name: node-1
|
||||||
|
network.host: 0.0.0.0
|
||||||
|
http.port: 9200
|
||||||
|
discovery.type: single-node
|
||||||
|
plugins.security.disabled: true
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Set ownership and permissions
|
||||||
|
sudo chown -R $USER:$USER /opt/opensearch
|
||||||
|
|
||||||
|
# Start OpenSearch in background
|
||||||
|
/opt/opensearch/bin/opensearch -d -p /tmp/opensearch.pid
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Wait for OpenSearch to be ready
|
||||||
|
command: |
|
||||||
|
echo "Waiting for OpenSearch to start..."
|
||||||
|
for i in {1..30}; do
|
||||||
|
if curl -s http://localhost:9200/_cluster/health | grep -q '"status"'; then
|
||||||
|
echo "OpenSearch is ready!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Waiting... ($i/30)"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "OpenSearch failed to start"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
# Configure environment and database
|
||||||
|
- run:
|
||||||
|
name: Database Setup and Configure Environment Variables
|
||||||
|
command: |
|
||||||
|
pg_pass=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 15 ; echo '')
|
||||||
|
sed -i "s/REPLACE_WITH_PASSWORD/${pg_pass}/g" ${PWD}/.circleci/setup_chatwoot.sql
|
||||||
|
chmod 644 ${PWD}/.circleci/setup_chatwoot.sql
|
||||||
|
mv ${PWD}/.circleci/setup_chatwoot.sql /tmp/
|
||||||
|
sudo -i -u postgres psql -f /tmp/setup_chatwoot.sql
|
||||||
|
cp .env.example .env
|
||||||
|
sed -i '/^FRONTEND_URL/d' .env
|
||||||
|
sed -i -e '/REDIS_URL/ s/=.*/=redis:\/\/localhost:6379/' .env
|
||||||
|
sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env
|
||||||
|
sed -i -e '/POSTGRES_USERNAME/ s/=.*/=chatwoot/' .env
|
||||||
|
sed -i -e "/POSTGRES_PASSWORD/ s/=.*/=$pg_pass/" .env
|
||||||
|
echo -en "\nINSTALLATION_ENV=circleci" >> ".env"
|
||||||
|
echo -en "\nOPENSEARCH_URL=http://localhost:9200" >> ".env"
|
||||||
|
|
||||||
|
# Database setup
|
||||||
|
- run:
|
||||||
|
name: Run DB migrations
|
||||||
|
command: bundle exec rails db:chatwoot_prepare
|
||||||
|
|
||||||
|
# Run backend tests (parallelized)
|
||||||
|
- run:
|
||||||
|
name: Run backend tests
|
||||||
|
command: |
|
||||||
|
mkdir -p ~/tmp/test-results/rspec
|
||||||
|
mkdir -p ~/tmp/test-artifacts
|
||||||
|
mkdir -p ~/build/coverage/backend
|
||||||
|
|
||||||
|
# Use round-robin distribution (same as GitHub Actions) for better test isolation
|
||||||
|
# This prevents tests with similar timing from being grouped on the same runner
|
||||||
|
SPEC_FILES=($(find spec -name '*_spec.rb' | sort))
|
||||||
|
TESTS=""
|
||||||
|
|
||||||
|
for i in "${!SPEC_FILES[@]}"; do
|
||||||
|
if [ $(( i % $CIRCLE_NODE_TOTAL )) -eq $CIRCLE_NODE_INDEX ]; then
|
||||||
|
TESTS="$TESTS ${SPEC_FILES[$i]}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
bundle exec rspec -I ./spec --require coverage_helper --require spec_helper --format progress \
|
||||||
|
--format RspecJunitFormatter \
|
||||||
|
--out ~/tmp/test-results/rspec.xml \
|
||||||
|
-- $TESTS
|
||||||
|
no_output_timeout: 30m
|
||||||
|
|
||||||
|
# Store test results for better splitting in future runs
|
||||||
|
- store_test_results:
|
||||||
|
path: ~/tmp/test-results
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Move coverage files if they exist
|
||||||
|
command: |
|
||||||
|
if [ -d "coverage" ]; then
|
||||||
|
mkdir -p ~/build/coverage
|
||||||
|
cp -r coverage ~/build/coverage/backend || true
|
||||||
|
fi
|
||||||
|
when: always
|
||||||
|
|
||||||
|
- persist_to_workspace:
|
||||||
|
root: ~/build
|
||||||
|
paths:
|
||||||
|
- coverage
|
||||||
|
|
||||||
|
# Collect coverage from all jobs
|
||||||
|
coverage:
|
||||||
|
<<: *defaults
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- attach_workspace:
|
||||||
|
at: ~/build
|
||||||
|
|
||||||
|
# Qlty coverage publish
|
||||||
|
- qlty-orb/coverage_publish:
|
||||||
|
files: |
|
||||||
|
coverage/frontend/lcov.info
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: List coverage directory contents
|
||||||
|
command: |
|
||||||
|
ls -R ~/build/coverage || echo "No coverage directory"
|
||||||
|
|
||||||
|
- store_artifacts:
|
||||||
|
path: coverage
|
||||||
|
destination: coverage
|
||||||
|
|
||||||
|
build:
|
||||||
|
<<: *defaults
|
||||||
|
steps:
|
||||||
|
- run:
|
||||||
|
name: Legacy build aggregator
|
||||||
|
command: |
|
||||||
|
echo "All main jobs passed; build job kept only for GitHub required check compatibility."
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
build:
|
||||||
|
jobs:
|
||||||
|
- lint
|
||||||
|
- frontend-tests
|
||||||
|
- backend-tests
|
||||||
|
- coverage:
|
||||||
|
requires:
|
||||||
|
- frontend-tests
|
||||||
|
- backend-tests
|
||||||
|
- build:
|
||||||
|
requires:
|
||||||
|
- lint
|
||||||
|
- coverage
|
||||||
11
research/chatwoot/.circleci/setup_chatwoot.sql
Normal file
11
research/chatwoot/.circleci/setup_chatwoot.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
CREATE USER chatwoot CREATEDB;
|
||||||
|
ALTER USER chatwoot PASSWORD 'REPLACE_WITH_PASSWORD';
|
||||||
|
ALTER ROLE chatwoot SUPERUSER;
|
||||||
|
|
||||||
|
UPDATE pg_database SET datistemplate = FALSE WHERE datname = 'template1';
|
||||||
|
DROP DATABASE template1;
|
||||||
|
CREATE DATABASE template1 WITH TEMPLATE = template0 ENCODING = 'UNICODE';
|
||||||
|
UPDATE pg_database SET datistemplate = TRUE WHERE datname = 'template1';
|
||||||
|
|
||||||
|
\c template1;
|
||||||
|
VACUUM FREEZE;
|
||||||
23
research/chatwoot/.dependabot/config.yml
Normal file
23
research/chatwoot/.dependabot/config.yml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
version: 1
|
||||||
|
update_configs:
|
||||||
|
- package_manager: "ruby:bundler"
|
||||||
|
directory: "/"
|
||||||
|
update_schedule: "weekly"
|
||||||
|
default_reviewers:
|
||||||
|
- "sony-mathew"
|
||||||
|
- "sojan-official"
|
||||||
|
- "subintp"
|
||||||
|
default_labels:
|
||||||
|
- "dependencies"
|
||||||
|
- "ruby"
|
||||||
|
version_requirement_updates: "auto"
|
||||||
|
- package_manager: "javascript"
|
||||||
|
directory: "/"
|
||||||
|
update_schedule: "weekly"
|
||||||
|
default_reviewers:
|
||||||
|
- "pranavrajs"
|
||||||
|
- "nithindavid"
|
||||||
|
default_labels:
|
||||||
|
- "dependencies"
|
||||||
|
- "javascript"
|
||||||
|
version_requirement_updates: "auto"
|
||||||
18
research/chatwoot/.devcontainer/Dockerfile
Normal file
18
research/chatwoot/.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# The below image is created out of the Dockerfile.base
|
||||||
|
# It has the dependencies already installed so that codespace will boot up fast
|
||||||
|
FROM ghcr.io/chatwoot/chatwoot_codespace:latest
|
||||||
|
|
||||||
|
# Do the set up required for chatwoot app
|
||||||
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
# Copy dependency files first for better caching
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
COPY Gemfile Gemfile.lock ./
|
||||||
|
|
||||||
|
# Install dependencies (will be cached if files don't change)
|
||||||
|
RUN pnpm install --frozen-lockfile && \
|
||||||
|
gem install bundler && \
|
||||||
|
bundle install --jobs=$(nproc)
|
||||||
|
|
||||||
|
# Copy source code after dependencies are installed
|
||||||
|
COPY . /workspace
|
||||||
98
research/chatwoot/.devcontainer/Dockerfile.base
Normal file
98
research/chatwoot/.devcontainer/Dockerfile.base
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
ARG VARIANT="ubuntu-22.04"
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/vscode/devcontainers/base:0-${VARIANT}
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
ARG NODE_VERSION
|
||||||
|
ARG RUBY_VERSION
|
||||||
|
ARG USER_UID
|
||||||
|
ARG USER_GID
|
||||||
|
ARG PNPM_VERSION="10.2.0"
|
||||||
|
ENV PNPM_VERSION ${PNPM_VERSION}
|
||||||
|
ENV RUBY_CONFIGURE_OPTS=--disable-install-doc
|
||||||
|
|
||||||
|
# Update args in docker-compose.yaml to set the UID/GID of the "vscode" user.
|
||||||
|
RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
|
||||||
|
groupmod --gid $USER_GID vscode \
|
||||||
|
&& usermod --uid $USER_UID --gid $USER_GID vscode \
|
||||||
|
&& chmod -R $USER_UID:$USER_GID /home/vscode; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUN NODE_MAJOR=$(echo $NODE_VERSION | cut -d. -f1) \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash - \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get -y install --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
libssl-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
gnupg \
|
||||||
|
tar \
|
||||||
|
tzdata \
|
||||||
|
postgresql-client \
|
||||||
|
libpq-dev \
|
||||||
|
git \
|
||||||
|
imagemagick \
|
||||||
|
libyaml-dev \
|
||||||
|
curl \
|
||||||
|
ca-certificates \
|
||||||
|
tmux \
|
||||||
|
nodejs \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
# Install rbenv and ruby for root user first
|
||||||
|
RUN git clone --depth 1 https://github.com/rbenv/rbenv.git ~/.rbenv \
|
||||||
|
&& echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \
|
||||||
|
&& echo 'eval "$(rbenv init -)"' >> ~/.bashrc
|
||||||
|
ENV PATH "/root/.rbenv/bin/:/root/.rbenv/shims/:$PATH"
|
||||||
|
RUN git clone --depth 1 https://github.com/rbenv/ruby-build.git && \
|
||||||
|
PREFIX=/usr/local ./ruby-build/install.sh
|
||||||
|
|
||||||
|
RUN rbenv install $RUBY_VERSION && \
|
||||||
|
rbenv global $RUBY_VERSION && \
|
||||||
|
rbenv versions
|
||||||
|
|
||||||
|
# Set up rbenv for vscode user
|
||||||
|
RUN su - vscode -c "git clone --depth 1 https://github.com/rbenv/rbenv.git ~/.rbenv" \
|
||||||
|
&& su - vscode -c "echo 'export PATH=\"\$HOME/.rbenv/bin:\$PATH\"' >> ~/.bashrc" \
|
||||||
|
&& su - vscode -c "echo 'eval \"\$(rbenv init -)\"' >> ~/.bashrc" \
|
||||||
|
&& su - vscode -c "PATH=\"/home/vscode/.rbenv/bin:\$PATH\" rbenv install $RUBY_VERSION" \
|
||||||
|
&& su - vscode -c "PATH=\"/home/vscode/.rbenv/bin:\$PATH\" rbenv global $RUBY_VERSION"
|
||||||
|
|
||||||
|
# Install overmind and gh in single layer
|
||||||
|
RUN curl -L https://github.com/DarthSim/overmind/releases/download/v2.1.0/overmind-v2.1.0-linux-amd64.gz > overmind.gz \
|
||||||
|
&& gunzip overmind.gz \
|
||||||
|
&& mv overmind /usr/local/bin \
|
||||||
|
&& chmod +x /usr/local/bin/overmind \
|
||||||
|
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
|
||||||
|
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends gh \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
|
||||||
|
# Do the set up required for chatwoot app
|
||||||
|
WORKDIR /workspace
|
||||||
|
RUN chown vscode:vscode /workspace
|
||||||
|
|
||||||
|
# set up node js, pnpm and claude code in single layer
|
||||||
|
RUN npm install -g pnpm@${PNPM_VERSION} @anthropic-ai/claude-code \
|
||||||
|
&& npm cache clean --force
|
||||||
|
|
||||||
|
# Switch to vscode user
|
||||||
|
USER vscode
|
||||||
|
ENV PATH="/home/vscode/.rbenv/bin:/home/vscode/.rbenv/shims:$PATH"
|
||||||
|
|
||||||
|
# Copy dependency files first for better caching
|
||||||
|
COPY --chown=vscode:vscode Gemfile Gemfile.lock package.json pnpm-lock.yaml ./
|
||||||
|
|
||||||
|
# Install dependencies as vscode user
|
||||||
|
RUN eval "$(rbenv init -)" \
|
||||||
|
&& gem install bundler -N \
|
||||||
|
&& bundle install --jobs=$(nproc) \
|
||||||
|
&& pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# Copy source code after dependencies are installed
|
||||||
|
COPY --chown=vscode:vscode . /workspace
|
||||||
49
research/chatwoot/.devcontainer/devcontainer.json
Normal file
49
research/chatwoot/.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"name": "Chatwoot Development Codespace",
|
||||||
|
"service": "app",
|
||||||
|
"dockerComposeFile": "docker-compose.yml",
|
||||||
|
|
||||||
|
"settings": {
|
||||||
|
"terminal.integrated.shell.linux": "/bin/zsh",
|
||||||
|
"extensions.showRecommendationsOnlyOnDemand": true,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"files.trimTrailingWhitespace": true,
|
||||||
|
"files.insertFinalNewline": true,
|
||||||
|
"search.exclude": {
|
||||||
|
"**/node_modules": true,
|
||||||
|
"**/tmp": true,
|
||||||
|
"**/log": true,
|
||||||
|
"**/coverage": true,
|
||||||
|
"**/public/packs": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
// Add the IDs of extensions you want installed when the container is created.
|
||||||
|
"extensions": [
|
||||||
|
"Shopify.ruby-lsp",
|
||||||
|
"misogi.ruby-rubocop",
|
||||||
|
"davidpallinder.rails-test-runner",
|
||||||
|
"github.copilot",
|
||||||
|
"mrmlnc.vscode-duplicate"
|
||||||
|
],
|
||||||
|
|
||||||
|
|
||||||
|
// 5432 postgres
|
||||||
|
// 6379 redis
|
||||||
|
// 1025,8025 mailhog
|
||||||
|
"forwardPorts": [8025, 3000, 3036],
|
||||||
|
|
||||||
|
"postCreateCommand": ".devcontainer/scripts/setup.sh && POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rake db:chatwoot_prepare && pnpm install",
|
||||||
|
"portsAttributes": {
|
||||||
|
"3000": {
|
||||||
|
"label": "Rails Server"
|
||||||
|
},
|
||||||
|
"3036": {
|
||||||
|
"label": "Vite Dev Server"
|
||||||
|
},
|
||||||
|
"8025": {
|
||||||
|
"label": "Mailhog UI"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
research/chatwoot/.devcontainer/docker-compose.base.yml
Normal file
18
research/chatwoot/.devcontainer/docker-compose.base.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Docker Compose file for building the base image in GitHub Actions
|
||||||
|
# Usage: docker-compose -f .devcontainer/docker-compose.base.yml build base
|
||||||
|
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
base:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: .devcontainer/Dockerfile.base
|
||||||
|
args:
|
||||||
|
VARIANT: 'ubuntu-22.04'
|
||||||
|
NODE_VERSION: '24.13.0'
|
||||||
|
RUBY_VERSION: '3.4.4'
|
||||||
|
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
|
||||||
|
USER_UID: '1000'
|
||||||
|
USER_GID: '1000'
|
||||||
|
image: ghcr.io/chatwoot/chatwoot_codespace:latest
|
||||||
53
research/chatwoot/.devcontainer/docker-compose.yml
Normal file
53
research/chatwoot/.devcontainer/docker-compose.yml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# https://github.com/microsoft/vscode-dev-containers/blob/master/containers/python-3-postgres/.devcontainer/docker-compose.yml
|
||||||
|
# https://github.com/microsoft/vscode-dev-containers/blob/master/containers/ruby-rails/.devcontainer/devcontainer.json
|
||||||
|
#
|
||||||
|
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: .devcontainer/Dockerfile
|
||||||
|
args:
|
||||||
|
VARIANT: 'ubuntu-22.04'
|
||||||
|
NODE_VERSION: '24.13.0'
|
||||||
|
RUBY_VERSION: '3.4.4'
|
||||||
|
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
|
||||||
|
USER_UID: '1000'
|
||||||
|
USER_GID: '1000'
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- ..:/workspace:cached
|
||||||
|
|
||||||
|
# Overrides default command so things don't shut down after the process ends.
|
||||||
|
command: sleep infinity
|
||||||
|
|
||||||
|
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
|
||||||
|
network_mode: service:db
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: pgvector/pgvector:pg16
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: service:db
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
|
|
||||||
|
mailhog:
|
||||||
|
restart: unless-stopped
|
||||||
|
image: mailhog/mailhog
|
||||||
|
network_mode: service:db
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
redis-data:
|
||||||
16
research/chatwoot/.devcontainer/scripts/setup.sh
Executable file
16
research/chatwoot/.devcontainer/scripts/setup.sh
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
cp .env.example .env
|
||||||
|
sed -i -e '/REDIS_URL/ s/=.*/=redis:\/\/localhost:6379/' .env
|
||||||
|
sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env
|
||||||
|
sed -i -e '/SMTP_ADDRESS/ s/=.*/=localhost/' .env
|
||||||
|
sed -i -e "/FRONTEND_URL/ s/=.*/=https:\/\/$CODESPACE_NAME-3000.app.github.dev/" .env
|
||||||
|
|
||||||
|
# Setup Claude Code API key if available
|
||||||
|
if [ -n "$CLAUDE_CODE_API_KEY" ]; then
|
||||||
|
mkdir -p ~/.claude
|
||||||
|
echo '{"apiKeyHelper": "~/.claude/anthropic_key.sh"}' > ~/.claude/settings.json
|
||||||
|
echo "echo \"$CLAUDE_CODE_API_KEY\"" > ~/.claude/anthropic_key.sh
|
||||||
|
chmod +x ~/.claude/anthropic_key.sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
# codespaces make the ports public
|
||||||
|
gh codespace ports visibility 3000:public 3036:public 8025:public -c $CODESPACE_NAME
|
||||||
17
research/chatwoot/.dockerignore
Normal file
17
research/chatwoot/.dockerignore
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
.bundle
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
docker-compose.*
|
||||||
|
docker/Dockerfile
|
||||||
|
docker/dockerfiles
|
||||||
|
log
|
||||||
|
storage
|
||||||
|
public/system
|
||||||
|
tmp
|
||||||
|
.codeclimate.yml
|
||||||
|
public/packs
|
||||||
|
node_modules
|
||||||
|
vendor/bundle
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
14
research/chatwoot/.editorconfig
Normal file
14
research/chatwoot/.editorconfig
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs
|
||||||
|
# @see http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
tab_width = 2
|
||||||
|
|
||||||
|
[*.{rb,erb,js,coffee,json,yml,css,scss,sh,markdown,md,html}]
|
||||||
|
indent_size = 2
|
||||||
279
research/chatwoot/.env.example
Normal file
279
research/chatwoot/.env.example
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
# Learn about the various environment variables at
|
||||||
|
# https://www.chatwoot.com/docs/self-hosted/configuration/environment-variables/#rails-production-variables
|
||||||
|
|
||||||
|
# Used to verify the integrity of signed cookies. so ensure a secure value is set
|
||||||
|
# SECRET_KEY_BASE should be alphanumeric. Avoid special characters or symbols.
|
||||||
|
# Use `rake secret` to generate this variable
|
||||||
|
SECRET_KEY_BASE=replace_with_lengthy_secure_hex
|
||||||
|
|
||||||
|
# Active Record Encryption keys (required for MFA/2FA functionality)
|
||||||
|
# Generate these keys by running: rails db:encryption:init
|
||||||
|
# IMPORTANT: Use different keys for each environment (development, staging, production)
|
||||||
|
# ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=
|
||||||
|
# ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=
|
||||||
|
# ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=
|
||||||
|
|
||||||
|
# Replace with the URL you are planning to use for your app
|
||||||
|
FRONTEND_URL=http://0.0.0.0:3000
|
||||||
|
# To use a dedicated URL for help center pages
|
||||||
|
# HELPCENTER_URL=http://0.0.0.0:3000
|
||||||
|
|
||||||
|
# If the variable is set, all non-authenticated pages would fallback to the default locale.
|
||||||
|
# Whenever a new account is created, the default language will be DEFAULT_LOCALE instead of en
|
||||||
|
# DEFAULT_LOCALE=en
|
||||||
|
|
||||||
|
# If you plan to use CDN for your assets, set Asset CDN Host
|
||||||
|
ASSET_CDN_HOST=
|
||||||
|
|
||||||
|
# Force all access to the app over SSL, default is set to false
|
||||||
|
FORCE_SSL=false
|
||||||
|
|
||||||
|
# This lets you control new sign ups on your chatwoot installation
|
||||||
|
# true : default option, allows sign ups
|
||||||
|
# false : disables all the end points related to sign ups
|
||||||
|
# api_only: disables the UI for signup, but you can create sign ups via the account apis
|
||||||
|
ENABLE_ACCOUNT_SIGNUP=false
|
||||||
|
|
||||||
|
# Redis config
|
||||||
|
# specify the configs via single URL or individual variables
|
||||||
|
# ref: https://www.iana.org/assignments/uri-schemes/prov/redis
|
||||||
|
# You can also use the following format for the URL: redis://:password@host:port/db_number
|
||||||
|
REDIS_URL=redis://redis:6379
|
||||||
|
# If you are using docker-compose, set this variable's value to be any string,
|
||||||
|
# which will be the password for the redis service running inside the docker-compose
|
||||||
|
# to make it secure
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
# Redis Sentinel can be used by passing list of sentinel host and ports e,g. sentinel_host1:port1,sentinel_host2:port2
|
||||||
|
REDIS_SENTINELS=
|
||||||
|
# Redis sentinel master name is required when using sentinel, default value is "mymaster".
|
||||||
|
# You can find list of master using "SENTINEL masters" command
|
||||||
|
REDIS_SENTINEL_MASTER_NAME=
|
||||||
|
|
||||||
|
# By default Chatwoot will pass REDIS_PASSWORD as the password value for sentinels
|
||||||
|
# Use the following environment variable to customize passwords for sentinels.
|
||||||
|
# Use empty string if sentinels are configured with out passwords
|
||||||
|
# REDIS_SENTINEL_PASSWORD=
|
||||||
|
|
||||||
|
# Redis premium breakage in heroku fix
|
||||||
|
# enable the following configuration
|
||||||
|
# ref: https://github.com/chatwoot/chatwoot/issues/2420
|
||||||
|
# REDIS_OPENSSL_VERIFY_MODE=none
|
||||||
|
|
||||||
|
# Postgres Database config variables
|
||||||
|
# You can leave POSTGRES_DATABASE blank. The default name of
|
||||||
|
# the database in the production environment is chatwoot_production
|
||||||
|
# POSTGRES_DATABASE=
|
||||||
|
POSTGRES_HOST=postgres
|
||||||
|
POSTGRES_USERNAME=postgres
|
||||||
|
POSTGRES_PASSWORD=
|
||||||
|
RAILS_ENV=development
|
||||||
|
# Changes the Postgres query timeout limit. The default is 14 seconds. Modify only when required.
|
||||||
|
# POSTGRES_STATEMENT_TIMEOUT=14s
|
||||||
|
RAILS_MAX_THREADS=5
|
||||||
|
|
||||||
|
# The email from which all outgoing emails are sent
|
||||||
|
# could user either `email@yourdomain.com` or `BrandName <email@yourdomain.com>`
|
||||||
|
MAILER_SENDER_EMAIL=Chatwoot <accounts@chatwoot.com>
|
||||||
|
|
||||||
|
#SMTP domain key is set up for HELO checking
|
||||||
|
SMTP_DOMAIN=chatwoot.com
|
||||||
|
# Set the value to "mailhog" if using docker-compose for development environments,
|
||||||
|
# Set the value as "localhost" or your SMTP address in other environments
|
||||||
|
# If SMTP_ADDRESS is empty, Chatwoot would try to use sendmail(postfix)
|
||||||
|
SMTP_ADDRESS=
|
||||||
|
SMTP_PORT=1025
|
||||||
|
SMTP_USERNAME=
|
||||||
|
SMTP_PASSWORD=
|
||||||
|
# plain,login,cram_md5
|
||||||
|
SMTP_AUTHENTICATION=
|
||||||
|
SMTP_ENABLE_STARTTLS_AUTO=true
|
||||||
|
# Can be: 'none', 'peer', 'client_once', 'fail_if_no_peer_cert', see http://api.rubyonrails.org/classes/ActionMailer/Base.html
|
||||||
|
SMTP_OPENSSL_VERIFY_MODE=peer
|
||||||
|
# Comment out the following environment variables if required by your SMTP server
|
||||||
|
# SMTP_TLS=
|
||||||
|
# SMTP_SSL=
|
||||||
|
# SMTP_OPEN_TIMEOUT
|
||||||
|
# SMTP_READ_TIMEOUT
|
||||||
|
|
||||||
|
# Mail Incoming
|
||||||
|
# This is the domain set for the reply emails when conversation continuity is enabled
|
||||||
|
MAILER_INBOUND_EMAIL_DOMAIN=
|
||||||
|
# Set this to the appropriate ingress channel with regards to incoming emails
|
||||||
|
# Possible values are :
|
||||||
|
# relay for Exim, Postfix, Qmail
|
||||||
|
# mailgun for Mailgun
|
||||||
|
# mandrill for Mandrill
|
||||||
|
# postmark for Postmark
|
||||||
|
# sendgrid for Sendgrid
|
||||||
|
# ses for Amazon SES
|
||||||
|
RAILS_INBOUND_EMAIL_SERVICE=
|
||||||
|
# Use one of the following based on the email ingress service
|
||||||
|
# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html
|
||||||
|
# Set this to a password of your choice and use it in the Inbound webhook
|
||||||
|
RAILS_INBOUND_EMAIL_PASSWORD=
|
||||||
|
|
||||||
|
MAILGUN_INGRESS_SIGNING_KEY=
|
||||||
|
MANDRILL_INGRESS_API_KEY=
|
||||||
|
|
||||||
|
# SNS topic ARN for ActionMailbox (format: arn:aws:sns:region:account-id:topic-name)
|
||||||
|
# Configure only if the rails_inbound_email_service = ses
|
||||||
|
ACTION_MAILBOX_SES_SNS_TOPIC=
|
||||||
|
|
||||||
|
# Creating Your Inbound Webhook Instructions for Postmark and Sendgrid:
|
||||||
|
# Inbound webhook URL format:
|
||||||
|
# https://actionmailbox:[YOUR_RAILS_INBOUND_EMAIL_PASSWORD]@[YOUR_CHATWOOT_DOMAIN.COM]/rails/action_mailbox/[RAILS_INBOUND_EMAIL_SERVICE]/inbound_emails
|
||||||
|
# Note: Replace the values inside the brackets; do not include the brackets themselves.
|
||||||
|
# Example: https://actionmailbox:mYRandomPassword3@chatwoot.example.com/rails/action_mailbox/postmark/inbound_emails
|
||||||
|
# For Postmark
|
||||||
|
# Ensure the 'Include raw email content in JSON payload' checkbox is selected in the inbound webhook section.
|
||||||
|
|
||||||
|
# Storage
|
||||||
|
ACTIVE_STORAGE_SERVICE=local
|
||||||
|
|
||||||
|
# Amazon S3
|
||||||
|
# documentation: https://www.chatwoot.com/docs/configuring-s3-bucket-as-cloud-storage
|
||||||
|
S3_BUCKET_NAME=
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_REGION=
|
||||||
|
|
||||||
|
# Log settings
|
||||||
|
# Disable if you want to write logs to a file
|
||||||
|
RAILS_LOG_TO_STDOUT=true
|
||||||
|
LOG_LEVEL=info
|
||||||
|
LOG_SIZE=500
|
||||||
|
# Configure this environment variable if you want to use lograge instead of rails logger
|
||||||
|
#LOGRAGE_ENABLED=true
|
||||||
|
|
||||||
|
### This environment variables are only required if you are setting up social media channels
|
||||||
|
|
||||||
|
# Facebook
|
||||||
|
# documentation: https://www.chatwoot.com/docs/facebook-setup
|
||||||
|
FB_VERIFY_TOKEN=
|
||||||
|
FB_APP_SECRET=
|
||||||
|
FB_APP_ID=
|
||||||
|
|
||||||
|
# https://developers.facebook.com/docs/messenger-platform/instagram/get-started#app-dashboard
|
||||||
|
IG_VERIFY_TOKEN=
|
||||||
|
|
||||||
|
# Twitter
|
||||||
|
# documentation: https://www.chatwoot.com/docs/twitter-app-setup
|
||||||
|
TWITTER_APP_ID=
|
||||||
|
TWITTER_CONSUMER_KEY=
|
||||||
|
TWITTER_CONSUMER_SECRET=
|
||||||
|
TWITTER_ENVIRONMENT=
|
||||||
|
|
||||||
|
#slack integration
|
||||||
|
SLACK_CLIENT_ID=
|
||||||
|
SLACK_CLIENT_SECRET=
|
||||||
|
|
||||||
|
# Google OAuth
|
||||||
|
GOOGLE_OAUTH_CLIENT_ID=
|
||||||
|
GOOGLE_OAUTH_CLIENT_SECRET=
|
||||||
|
GOOGLE_OAUTH_CALLBACK_URL=
|
||||||
|
|
||||||
|
### Change this env variable only if you are using a custom build mobile app
|
||||||
|
## Mobile app env variables
|
||||||
|
IOS_APP_ID=L7YLMN4634.com.chatwoot.app
|
||||||
|
ANDROID_BUNDLE_ID=com.chatwoot.app
|
||||||
|
|
||||||
|
# https://developers.google.com/android/guides/client-auth (use keytool to print the fingerprint in the first section)
|
||||||
|
ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:D4:5D:D4:53:F8:3B:FB:D3:C6:28:64:1D:AA:08:1E:D8
|
||||||
|
|
||||||
|
### Smart App Banner
|
||||||
|
# https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/PromotingAppswithAppBanners/PromotingAppswithAppBanners.html
|
||||||
|
# You can find your app-id in https://itunesconnect.apple.com
|
||||||
|
#IOS_APP_IDENTIFIER=1495796682
|
||||||
|
|
||||||
|
## Push Notification
|
||||||
|
## generate a new key value here : https://d3v.one/vapid-key-generator/
|
||||||
|
# VAPID_PUBLIC_KEY=
|
||||||
|
# VAPID_PRIVATE_KEY=
|
||||||
|
#
|
||||||
|
# for mobile apps
|
||||||
|
# FCM_SERVER_KEY=
|
||||||
|
|
||||||
|
### APM and Error Monitoring configurations
|
||||||
|
## Elastic APM
|
||||||
|
## https://www.elastic.co/guide/en/apm/agent/ruby/current/getting-started-rails.html
|
||||||
|
# ELASTIC_APM_SERVER_URL=
|
||||||
|
# ELASTIC_APM_SECRET_TOKEN=
|
||||||
|
|
||||||
|
## Sentry
|
||||||
|
# SENTRY_DSN=
|
||||||
|
|
||||||
|
|
||||||
|
## Scout
|
||||||
|
## https://scoutapm.com/docs/ruby/configuration
|
||||||
|
# SCOUT_KEY=YOURKEY
|
||||||
|
# SCOUT_NAME=YOURAPPNAME (Production)
|
||||||
|
# SCOUT_MONITOR=true
|
||||||
|
|
||||||
|
## NewRelic
|
||||||
|
# https://docs.newrelic.com/docs/agents/ruby-agent/configuration/ruby-agent-configuration/
|
||||||
|
# NEW_RELIC_LICENSE_KEY=
|
||||||
|
# Set this to true to allow newrelic apm to send logs.
|
||||||
|
# This is turned off by default.
|
||||||
|
# NEW_RELIC_APPLICATION_LOGGING_ENABLED=
|
||||||
|
|
||||||
|
## Datadog
|
||||||
|
## https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#environment-variables
|
||||||
|
# DD_TRACE_AGENT_URL=
|
||||||
|
|
||||||
|
|
||||||
|
# MaxMindDB API key to download GeoLite2 City database
|
||||||
|
# IP_LOOKUP_API_KEY=
|
||||||
|
|
||||||
|
## Rack Attack configuration
|
||||||
|
## To prevent and throttle abusive requests
|
||||||
|
# ENABLE_RACK_ATTACK=true
|
||||||
|
# RACK_ATTACK_LIMIT=300
|
||||||
|
# ENABLE_RACK_ATTACK_WIDGET_API=true
|
||||||
|
# Comma-separated list of trusted IPs that bypass Rack Attack throttling rules
|
||||||
|
# RACK_ATTACK_ALLOWED_IPS=127.0.0.1,::1,192.168.0.10
|
||||||
|
|
||||||
|
## Running chatwoot as an API only server
|
||||||
|
## setting this value to true will disable the frontend dashboard endpoints
|
||||||
|
# CW_API_ONLY_SERVER=false
|
||||||
|
|
||||||
|
## Development Only Config
|
||||||
|
# if you want to use letter_opener for local emails
|
||||||
|
# LETTER_OPENER=true
|
||||||
|
# meant to be used in github codespaces
|
||||||
|
# WEBPACKER_DEV_SERVER_PUBLIC=
|
||||||
|
|
||||||
|
# If you want to use official mobile app,
|
||||||
|
# the notifications would be relayed via a Chatwoot server
|
||||||
|
ENABLE_PUSH_RELAY_SERVER=true
|
||||||
|
|
||||||
|
# Stripe API key
|
||||||
|
STRIPE_SECRET_KEY=
|
||||||
|
STRIPE_WEBHOOK_SECRET=
|
||||||
|
|
||||||
|
# Set to true if you want to upload files to cloud storage using the signed url
|
||||||
|
# Make sure to follow https://edgeguides.rubyonrails.org/active_storage_overview.html#cross-origin-resource-sharing-cors-configuration on the cloud storage after setting this to true.
|
||||||
|
DIRECT_UPLOADS_ENABLED=
|
||||||
|
|
||||||
|
#MS OAUTH creds
|
||||||
|
AZURE_APP_ID=
|
||||||
|
AZURE_APP_SECRET=
|
||||||
|
|
||||||
|
## Advanced configurations
|
||||||
|
## Change these values to fine tune performance
|
||||||
|
# control the concurrency setting of sidekiq
|
||||||
|
# SIDEKIQ_CONCURRENCY=10
|
||||||
|
# Enable verbose logging each time a job is dequeued in Sidekiq
|
||||||
|
# ENABLE_SIDEKIQ_DEQUEUE_LOGGER=false
|
||||||
|
|
||||||
|
|
||||||
|
# AI powered features
|
||||||
|
## OpenAI key
|
||||||
|
# OPENAI_API_KEY=
|
||||||
|
|
||||||
|
# Housekeeping/Performance related configurations
|
||||||
|
# Set to true if you want to remove stale contact inboxes
|
||||||
|
# contact_inboxes with no conversation older than 90 days will be removed
|
||||||
|
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
|
||||||
|
|
||||||
|
# REDIS_ALFRED_SIZE=10
|
||||||
|
# REDIS_VELMA_SIZE=10
|
||||||
256
research/chatwoot/.eslintrc.js
Normal file
256
research/chatwoot/.eslintrc.js
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: [
|
||||||
|
'airbnb-base/legacy',
|
||||||
|
'prettier',
|
||||||
|
'plugin:vue/vue3-recommended',
|
||||||
|
'plugin:vitest-globals/recommended',
|
||||||
|
// use recommended-legacy when upgrading the plugin to v4
|
||||||
|
'plugin:@intlify/vue-i18n/recommended',
|
||||||
|
],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['**/*.spec.{j,t}s?(x)'],
|
||||||
|
env: {
|
||||||
|
'vitest-globals/env': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.story.vue'],
|
||||||
|
rules: {
|
||||||
|
'vue/no-undef-components': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
ignorePatterns: ['Variant', 'Story'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Story files can have static strings, it doesn't need to handle i18n always.
|
||||||
|
'vue/no-bare-strings-in-template': 'off',
|
||||||
|
'no-console': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
plugins: ['html', 'prettier'],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'prettier/prettier': ['error'],
|
||||||
|
camelcase: 'off',
|
||||||
|
'no-param-reassign': 'off',
|
||||||
|
'import/no-extraneous-dependencies': 'off',
|
||||||
|
'import/prefer-default-export': 'off',
|
||||||
|
'import/no-named-as-default': 'off',
|
||||||
|
'jsx-a11y/no-static-element-interactions': 'off',
|
||||||
|
'jsx-a11y/click-events-have-key-events': 'off',
|
||||||
|
'jsx-a11y/label-has-associated-control': 'off',
|
||||||
|
'jsx-a11y/label-has-for': 'off',
|
||||||
|
'jsx-a11y/anchor-is-valid': 'off',
|
||||||
|
'import/no-unresolved': 'off',
|
||||||
|
'vue/html-indent': 'off',
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
'vue/next-tick-style': ['error', 'callback'],
|
||||||
|
'vue/block-order': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
order: ['script', 'template', 'style'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'vue/component-name-in-template-casing': [
|
||||||
|
'error',
|
||||||
|
'PascalCase',
|
||||||
|
{
|
||||||
|
registeredComponentsOnly: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'vue/component-options-name-casing': ['error', 'PascalCase'],
|
||||||
|
'vue/custom-event-name-casing': ['error', 'camelCase'],
|
||||||
|
'vue/define-emits-declaration': ['error'],
|
||||||
|
'vue/define-macros-order': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
order: ['defineProps', 'defineEmits'],
|
||||||
|
defineExposeLast: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'vue/define-props-declaration': ['error', 'runtime'],
|
||||||
|
'vue/match-component-import-name': ['error'],
|
||||||
|
'vue/no-bare-strings-in-template': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
allowlist: [
|
||||||
|
'(',
|
||||||
|
')',
|
||||||
|
',',
|
||||||
|
'.',
|
||||||
|
'&',
|
||||||
|
'+',
|
||||||
|
'-',
|
||||||
|
'=',
|
||||||
|
'*',
|
||||||
|
'/',
|
||||||
|
'#',
|
||||||
|
'%',
|
||||||
|
'!',
|
||||||
|
'?',
|
||||||
|
':',
|
||||||
|
'[',
|
||||||
|
']',
|
||||||
|
'{',
|
||||||
|
'}',
|
||||||
|
'<',
|
||||||
|
'>',
|
||||||
|
'⌘',
|
||||||
|
'📄',
|
||||||
|
'🎉',
|
||||||
|
'🚀',
|
||||||
|
'💬',
|
||||||
|
'👥',
|
||||||
|
'📥',
|
||||||
|
'🔖',
|
||||||
|
'❌',
|
||||||
|
'✅',
|
||||||
|
'\u00b7',
|
||||||
|
'\u2022',
|
||||||
|
'\u2010',
|
||||||
|
'\u2013',
|
||||||
|
'\u2014',
|
||||||
|
'\u2212',
|
||||||
|
'|',
|
||||||
|
],
|
||||||
|
attributes: {
|
||||||
|
'/.+/': [
|
||||||
|
'title',
|
||||||
|
'aria-label',
|
||||||
|
'aria-placeholder',
|
||||||
|
'aria-roledescription',
|
||||||
|
'aria-valuetext',
|
||||||
|
],
|
||||||
|
input: ['placeholder'],
|
||||||
|
},
|
||||||
|
directives: ['v-text'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'vue/no-empty-component-block': 'error',
|
||||||
|
'vue/no-multiple-objects-in-class': 'error',
|
||||||
|
'vue/no-root-v-if': 'warn',
|
||||||
|
'vue/no-static-inline-styles': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
allowBinding: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'vue/no-template-target-blank': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
allowReferrer: false,
|
||||||
|
enforceDynamicLinks: 'always',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'vue/no-required-prop-with-default': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
autofix: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'vue/no-this-in-before-route-enter': 'error',
|
||||||
|
'vue/no-undef-components': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
ignorePatterns: [
|
||||||
|
'^woot-',
|
||||||
|
'^fluent-',
|
||||||
|
'^multiselect',
|
||||||
|
'^router-link',
|
||||||
|
'^router-view',
|
||||||
|
'^ninja-keys',
|
||||||
|
'^FormulateForm',
|
||||||
|
'^FormulateInput',
|
||||||
|
'^highlightjs',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'vue/no-unused-emit-declarations': 'error',
|
||||||
|
'vue/no-unused-refs': 'error',
|
||||||
|
'vue/no-use-v-else-with-v-for': 'error',
|
||||||
|
'vue/prefer-true-attribute-shorthand': 'error',
|
||||||
|
'vue/no-useless-v-bind': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
ignoreIncludesComment: false,
|
||||||
|
ignoreStringEscape: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'vue/no-v-text': 'error',
|
||||||
|
'vue/padding-line-between-blocks': ['error', 'always'],
|
||||||
|
'vue/prefer-separate-static-class': 'error',
|
||||||
|
'vue/require-explicit-slots': 'error',
|
||||||
|
'vue/require-macro-variable-name': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
defineProps: 'props',
|
||||||
|
defineEmits: 'emit',
|
||||||
|
defineSlots: 'slots',
|
||||||
|
useSlots: 'slots',
|
||||||
|
useAttrs: 'attrs',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'vue/no-unused-properties': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
groups: ['props'],
|
||||||
|
deepData: false,
|
||||||
|
ignorePublicMembers: false,
|
||||||
|
unreferencedOptions: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'vue/max-attributes-per-line': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
singleline: {
|
||||||
|
max: 20,
|
||||||
|
},
|
||||||
|
multiline: {
|
||||||
|
max: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'vue/html-self-closing': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
html: {
|
||||||
|
void: 'always',
|
||||||
|
normal: 'always',
|
||||||
|
component: 'always',
|
||||||
|
},
|
||||||
|
svg: 'always',
|
||||||
|
math: 'always',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'vue/no-v-html': 'off',
|
||||||
|
'vue/component-definition-name-casing': 'off',
|
||||||
|
'vue/singleline-html-element-content-newline': 'off',
|
||||||
|
'import/extensions': ['off'],
|
||||||
|
'no-console': 'error',
|
||||||
|
'@intlify/vue-i18n/no-dynamic-keys': 'warn',
|
||||||
|
'@intlify/vue-i18n/no-unused-keys': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
extensions: ['.js', '.vue'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
'vue-i18n': {
|
||||||
|
localeDir: './app/javascript/*/i18n/**.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
bus: true,
|
||||||
|
vi: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
2
research/chatwoot/.github/CODEOWNERS
vendored
Normal file
2
research/chatwoot/.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
## All enterprise related files should be reviewed by sojan before merging
|
||||||
|
/enterprise/* @sojan-official
|
||||||
2
research/chatwoot/.github/FUNDING.yml
vendored
Normal file
2
research/chatwoot/.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
open_collective: chatwoot
|
||||||
|
github: chatwoot
|
||||||
78
research/chatwoot/.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
78
research/chatwoot/.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
name: 🐞 Bug report
|
||||||
|
description: Create a report to help us improve
|
||||||
|
labels: 'Bug'
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Describe the bug
|
||||||
|
description: A concise description of what you expected to happen along with screenshots if applicable.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: To Reproduce
|
||||||
|
description: Steps to reproduce the behavior.
|
||||||
|
placeholder: |
|
||||||
|
1. In this environment...
|
||||||
|
2. With this config...
|
||||||
|
3. Run '...'
|
||||||
|
4. See error...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
description: A concise description of what you expected to happen.
|
||||||
|
- type: dropdown
|
||||||
|
id: environment
|
||||||
|
attributes:
|
||||||
|
label: Environment
|
||||||
|
description: Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self-hosted installation of Chatwoot. If you are using a self-hosted installation of Chatwoot, describe the type of deployment (Docker/Linux VM installation/Heroku/Kubernetes/Other).
|
||||||
|
options:
|
||||||
|
- app.chatwoot.com
|
||||||
|
- Linux VM
|
||||||
|
- Docker
|
||||||
|
- Kubernetes
|
||||||
|
- Heroku
|
||||||
|
- Other [please specify in the description]
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: provider
|
||||||
|
attributes:
|
||||||
|
label: Cloud Provider
|
||||||
|
description:
|
||||||
|
options:
|
||||||
|
- AWS
|
||||||
|
- GCP
|
||||||
|
- Azure
|
||||||
|
- DigitalOcean
|
||||||
|
- Other [please specify in the description]
|
||||||
|
- type: dropdown
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: Platform
|
||||||
|
description: Describe the platform you are using
|
||||||
|
options:
|
||||||
|
- Browser
|
||||||
|
- Mobile
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Operating system
|
||||||
|
description: The operating system and the version you are using.
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Browser and version
|
||||||
|
description: The name of the browser and version you are using.
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Docker (if applicable)
|
||||||
|
description: |
|
||||||
|
Please share the output of the following.
|
||||||
|
- `docker version`
|
||||||
|
- `docker info`
|
||||||
|
- `docker-compose version`
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Add any other context about the problem here.
|
||||||
8
research/chatwoot/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
research/chatwoot/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Report a security issue
|
||||||
|
url: https://www.chatwoot.com/docs/contributing-guide/security-reports/
|
||||||
|
about: Guidelines and steps to report a security vulnerability. Please report security vulnerabilities here.
|
||||||
|
- name: Product Documentation
|
||||||
|
url: https://www.chatwoot.com/help-center
|
||||||
|
about: If you have questions, are confused, or just want to understand our product better, please check out our documentation.
|
||||||
28
research/chatwoot/.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
28
research/chatwoot/.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: 🧙 Feature request
|
||||||
|
description: Suggest an idea for this project
|
||||||
|
labels: 'feature-request'
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Is your feature or enhancement related to a problem? Please describe.
|
||||||
|
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Describe the solution you'd like
|
||||||
|
description: A clear and concise description of what you want to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Describe alternatives you've considered
|
||||||
|
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Add any other context or screenshots about the feature request here.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
31
research/chatwoot/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
31
research/chatwoot/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Pull Request Template
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Please include a summary of the change and issue(s) fixed. Also, mention relevant motivation, context, and any dependencies that this change requires.
|
||||||
|
Fixes # (issue)
|
||||||
|
|
||||||
|
## Type of change
|
||||||
|
|
||||||
|
Please delete options that are not relevant.
|
||||||
|
|
||||||
|
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||||
|
- [ ] New feature (non-breaking change which adds functionality)
|
||||||
|
- [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected)
|
||||||
|
- [ ] This change requires a documentation update
|
||||||
|
|
||||||
|
## How Has This Been Tested?
|
||||||
|
|
||||||
|
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration.
|
||||||
|
|
||||||
|
|
||||||
|
## Checklist:
|
||||||
|
|
||||||
|
- [ ] My code follows the style guidelines of this project
|
||||||
|
- [ ] I have performed a self-review of my code
|
||||||
|
- [ ] I have commented on my code, particularly in hard-to-understand areas
|
||||||
|
- [ ] I have made corresponding changes to the documentation
|
||||||
|
- [ ] My changes generate no new warnings
|
||||||
|
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||||
|
- [ ] New and existing unit tests pass locally with my changes
|
||||||
|
- [ ] Any dependent changes have been merged and published in downstream modules
|
||||||
BIN
research/chatwoot/.github/screenshots/dashboard-dark.png
vendored
Normal file
BIN
research/chatwoot/.github/screenshots/dashboard-dark.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 966 KiB |
BIN
research/chatwoot/.github/screenshots/dashboard.png
vendored
Normal file
BIN
research/chatwoot/.github/screenshots/dashboard.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 934 KiB |
BIN
research/chatwoot/.github/screenshots/header-dark.png
vendored
Normal file
BIN
research/chatwoot/.github/screenshots/header-dark.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
research/chatwoot/.github/screenshots/header.png
vendored
Normal file
BIN
research/chatwoot/.github/screenshots/header.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
28
research/chatwoot/.github/workflows/auto-assign-pr.yml
vendored
Normal file
28
research/chatwoot/.github/workflows/auto-assign-pr.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: Auto-assign PR to Author
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
auto-assign:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Auto-assign PR to author
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const pull_number = context.payload.pull_request.number;
|
||||||
|
const author = context.payload.pull_request.user.login;
|
||||||
|
|
||||||
|
await github.rest.issues.addAssignees({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: pull_number,
|
||||||
|
assignees: [author]
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Assigned PR #${pull_number} to ${author}`);
|
||||||
50
research/chatwoot/.github/workflows/deploy_check.yml
vendored
Normal file
50
research/chatwoot/.github/workflows/deploy_check.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
## github action to check deployment success
|
||||||
|
## curl the deployment url and check for 200 status
|
||||||
|
## deployment url will be of the form chatwoot-pr-<pr_number>.herokuapp.com
|
||||||
|
name: Deploy Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
# If two pushes happen within a short time in the same PR, cancel the run of the oldest push
|
||||||
|
concurrency:
|
||||||
|
group: pr-${{ github.workflow }}-${{ github.head_ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deployment_check:
|
||||||
|
name: Check Deployment
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Install jq
|
||||||
|
run: sudo apt-get install -y jq
|
||||||
|
- name: Print Deployment URL
|
||||||
|
run: echo "https://chatwoot-pr-${{ github.event.pull_request.number }}.herokuapp.com"
|
||||||
|
- name: Check Deployment Status
|
||||||
|
run: |
|
||||||
|
max_attempts=10
|
||||||
|
attempt=1
|
||||||
|
status_code=0
|
||||||
|
echo "Waiting for review app to be deployed/redeployed, trying in 10 minutes..."
|
||||||
|
sleep 600
|
||||||
|
while [ $attempt -le $max_attempts ]; do
|
||||||
|
response=$(curl -s -o /dev/null -w "%{http_code}" https://chatwoot-pr-${{ github.event.pull_request.number }}.herokuapp.com/api)
|
||||||
|
status_code=$(echo $response | head -n 1)
|
||||||
|
if [ $status_code -eq 200 ]; then
|
||||||
|
body=$(curl -s https://chatwoot-pr-${{ github.event.pull_request.number }}.herokuapp.com/api)
|
||||||
|
if echo "$body" | jq -e '.version and .timestamp and .queue_services == "ok" and .data_services == "ok"' > /dev/null; then
|
||||||
|
echo "Deployment successful"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Deployment status unknown, retrying in 3 minutes..."
|
||||||
|
sleep 180
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Waiting for review app to be ready, retrying in 3 minutes..."
|
||||||
|
sleep 180
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "Deployment failed after $max_attempts attempts"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
41
research/chatwoot/.github/workflows/frontend-fe.yml
vendored
Normal file
41
research/chatwoot/.github/workflows/frontend-fe.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
name: Frontend Lint & Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
|
- uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
bundler-cache: true
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Install pnpm dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Run eslint
|
||||||
|
run: pnpm run eslint
|
||||||
|
|
||||||
|
- name: Run frontend tests with coverage
|
||||||
|
run: |
|
||||||
|
mkdir -p coverage
|
||||||
|
pnpm run test:coverage
|
||||||
23
research/chatwoot/.github/workflows/lint_pr.yml
vendored
Normal file
23
research/chatwoot/.github/workflows/lint_pr.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# ref: https://github.com/amannn/action-semantic-pull-request
|
||||||
|
# ensure PR title is in semantic format
|
||||||
|
|
||||||
|
name: "Lint PR"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- edited
|
||||||
|
- synchronize
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
main:
|
||||||
|
name: Validate PR title
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: amannn/action-semantic-pull-request@v5
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
29
research/chatwoot/.github/workflows/lock.yml
vendored
Normal file
29
research/chatwoot/.github/workflows/lock.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# We often have cases where users would comment over stale closed Github Issues.
|
||||||
|
# This creates unnecessary noise for the original reporter and makes it harder for triaging.
|
||||||
|
# This action locks the closed threads once it is inactive for over a month.
|
||||||
|
|
||||||
|
name: 'Lock Threads'
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 * * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: lock
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
action:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.repository == 'chatwoot/chatwoot' }}
|
||||||
|
steps:
|
||||||
|
- uses: dessant/lock-threads@v3
|
||||||
|
with:
|
||||||
|
issue-inactive-days: '30'
|
||||||
|
issue-lock-reason: 'resolved'
|
||||||
|
pr-inactive-days: '30'
|
||||||
|
pr-lock-reason: 'resolved'
|
||||||
60
research/chatwoot/.github/workflows/logging_percentage_check.yml
vendored
Normal file
60
research/chatwoot/.github/workflows/logging_percentage_check.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
name: Log Lines Percentage Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
|
||||||
|
# If two pushes happen within a short time in the same PR, cancel the run of the oldest push
|
||||||
|
concurrency:
|
||||||
|
group: pr-${{ github.workflow }}-${{ github.head_ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
log_lines_check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check for log lines and calculate percentage
|
||||||
|
run: |
|
||||||
|
# Define the log line pattern
|
||||||
|
LOG_LINE_PATTERN="Rails\.logger"
|
||||||
|
|
||||||
|
# Get the list of changed files in the pull request
|
||||||
|
CHANGED_FILES=$(git diff --name-only)
|
||||||
|
|
||||||
|
# Initialize a flag to track if any files have insufficient log lines
|
||||||
|
INSUFFICIENT_LOGS=0
|
||||||
|
|
||||||
|
for file in $CHANGED_FILES; do
|
||||||
|
if [[ $file =~ \.rb$ && ! $file =~ _spec\.rb$ ]]; then
|
||||||
|
# Count the total number of lines in the file
|
||||||
|
total_lines=$(wc -l < "$file")
|
||||||
|
|
||||||
|
# Count the number of log lines in the file
|
||||||
|
log_lines=$(grep -c "$LOG_LINE_PATTERN" "$file")
|
||||||
|
|
||||||
|
# Calculate the percentage of log lines
|
||||||
|
if [ "$total_lines" -gt 0 ]; then
|
||||||
|
percentage=$(awk "BEGIN { pc=100*${log_lines}/${total_lines}; i=int(pc); print (pc-i<0.5)?i:i+1 }")
|
||||||
|
else
|
||||||
|
percentage=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the percentage is less than 5%
|
||||||
|
if [ "$percentage" -lt 5 ]; then
|
||||||
|
echo "Error: Log lines percentage is less than 5% ($percentage%) in $file. Please add more log lines using Rails.logger statements."
|
||||||
|
INSUFFICIENT_LOGS=1
|
||||||
|
else
|
||||||
|
echo "Log lines percentage is $percentage% in $file. Code looks good!"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# If any files have insufficient log lines, fail the action
|
||||||
|
if [ "$INSUFFICIENT_LOGS" -eq 1 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
52
research/chatwoot/.github/workflows/nightly_installer.yml
vendored
Normal file
52
research/chatwoot/.github/workflows/nightly_installer.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# #
|
||||||
|
# #
|
||||||
|
# # Linux nightly installer action
|
||||||
|
# # This action will try to install and setup
|
||||||
|
# # chatwoot on an Ubuntu 22.04 machine using
|
||||||
|
# # the linux installer script.
|
||||||
|
# #
|
||||||
|
# # This is set to run daily at midnight.
|
||||||
|
# #
|
||||||
|
|
||||||
|
name: Run Linux nightly installer
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
nightly:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: get installer
|
||||||
|
run: |
|
||||||
|
wget https://get.chatwoot.app/linux/install.sh
|
||||||
|
chmod +x install.sh
|
||||||
|
#fix for postgtres not starting automatically in gh action env
|
||||||
|
sed -i '/function configure_db() {/a sudo service postgresql start' install.sh
|
||||||
|
|
||||||
|
- name: create input file
|
||||||
|
run: |
|
||||||
|
echo "no" > input
|
||||||
|
echo "yes" >> input
|
||||||
|
|
||||||
|
- name: Run the installer
|
||||||
|
run: |
|
||||||
|
sudo ./install.sh --install < input
|
||||||
|
|
||||||
|
# disabling http verify for now as http
|
||||||
|
# access to port 3000 fails in gh action env
|
||||||
|
# - name: Verify
|
||||||
|
# if: always()
|
||||||
|
# run: |
|
||||||
|
# sudo netstat -ntlp | grep 3000
|
||||||
|
# sudo systemctl restart chatwoot.target
|
||||||
|
# curl http://localhost:3000/api
|
||||||
|
|
||||||
|
- name: Upload chatwoot setup log file as an artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: chatwoot-setup-log-file
|
||||||
|
path: /var/log/chatwoot-setup.log
|
||||||
23
research/chatwoot/.github/workflows/publish_codespace_image.yml
vendored
Normal file
23
research/chatwoot/.github/workflows/publish_codespace_image.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name: Publish Codespace Base Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-code-space-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build the Codespace Base Image
|
||||||
|
run: |
|
||||||
|
docker compose -f .devcontainer/docker-compose.base.yml build base
|
||||||
|
docker push ghcr.io/chatwoot/chatwoot_codespace:latest
|
||||||
140
research/chatwoot/.github/workflows/publish_ee_docker.yml
vendored
Normal file
140
research/chatwoot/.github/workflows/publish_ee_docker.yml
vendored
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# #
|
||||||
|
# # This action will publish Chatwoot EE docker image.
|
||||||
|
# # This is set to run against merges to develop, master
|
||||||
|
# # and when tags are created.
|
||||||
|
# #
|
||||||
|
|
||||||
|
name: Publish Chatwoot EE docker images
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- master
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
DOCKER_REPO: chatwoot/chatwoot
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- platform: linux/amd64
|
||||||
|
runner: ubuntu-latest
|
||||||
|
- platform: linux/arm64
|
||||||
|
runner: ubuntu-22.04-arm
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
env:
|
||||||
|
GIT_REF: ${{ github.head_ref || github.ref_name }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
platform=${{ matrix.platform }}
|
||||||
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set Chatwoot edition
|
||||||
|
run: |
|
||||||
|
echo -en '\nENV CW_EDITION="ee"' >> docker/Dockerfile
|
||||||
|
|
||||||
|
- name: Set Docker Tags
|
||||||
|
run: |
|
||||||
|
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
|
||||||
|
if [ "${{ github.ref_name }}" = "master" ]; then
|
||||||
|
echo "DOCKER_TAG=${DOCKER_REPO}:latest" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "DOCKER_TAG=${DOCKER_REPO}:${SANITIZED_REF}" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push by digest
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker/Dockerfile
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
push: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
|
||||||
|
outputs: type=image,name=${{ env.DOCKER_REPO }},push-by-digest=true,name-canonical=true,push=true
|
||||||
|
|
||||||
|
- name: Export digest
|
||||||
|
run: |
|
||||||
|
mkdir -p ${{ runner.temp }}/digests
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
|
- name: Upload digest
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: digests-${{ env.PLATFORM_PAIR }}
|
||||||
|
path: ${{ runner.temp }}/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
merge:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
steps:
|
||||||
|
- name: Download digests
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: ${{ runner.temp }}/digests
|
||||||
|
pattern: digests-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Create manifest list and push
|
||||||
|
working-directory: ${{ runner.temp }}/digests
|
||||||
|
env:
|
||||||
|
GIT_REF: ${{ github.head_ref || github.ref_name }}
|
||||||
|
run: |
|
||||||
|
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
|
||||||
|
if [ "${{ github.ref_name }}" = "master" ]; then
|
||||||
|
TAG="${DOCKER_REPO}:latest"
|
||||||
|
else
|
||||||
|
TAG="${DOCKER_REPO}:${SANITIZED_REF}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker buildx imagetools create -t $TAG \
|
||||||
|
$(printf '${{ env.DOCKER_REPO }}@sha256:%s ' *)
|
||||||
|
|
||||||
|
- name: Inspect image
|
||||||
|
env:
|
||||||
|
GIT_REF: ${{ github.head_ref || github.ref_name }}
|
||||||
|
run: |
|
||||||
|
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
|
||||||
|
if [ "${{ github.ref_name }}" = "master" ]; then
|
||||||
|
TAG="${DOCKER_REPO}:latest"
|
||||||
|
else
|
||||||
|
TAG="${DOCKER_REPO}:${SANITIZED_REF}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker buildx imagetools inspect $TAG
|
||||||
145
research/chatwoot/.github/workflows/publish_foss_docker.yml
vendored
Normal file
145
research/chatwoot/.github/workflows/publish_foss_docker.yml
vendored
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# #
|
||||||
|
# # This action will publish Chatwoot CE docker image.
|
||||||
|
# # This is set to run against merges to develop, master
|
||||||
|
# # and when tags are created.
|
||||||
|
# #
|
||||||
|
|
||||||
|
name: Publish Chatwoot CE docker images
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- master
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
DOCKER_REPO: chatwoot/chatwoot
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- platform: linux/amd64
|
||||||
|
runner: ubuntu-latest
|
||||||
|
- platform: linux/arm64
|
||||||
|
runner: ubuntu-22.04-arm
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
env:
|
||||||
|
GIT_REF: ${{ github.head_ref || github.ref_name }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
platform=${{ matrix.platform }}
|
||||||
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Strip enterprise code
|
||||||
|
run: |
|
||||||
|
rm -rf enterprise
|
||||||
|
rm -rf spec/enterprise
|
||||||
|
|
||||||
|
- name: Set Chatwoot edition
|
||||||
|
run: |
|
||||||
|
echo -en '\nENV CW_EDITION="ce"' >> docker/Dockerfile
|
||||||
|
|
||||||
|
- name: Set Docker Tags
|
||||||
|
run: |
|
||||||
|
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
|
||||||
|
if [ "${{ github.ref_name }}" = "master" ]; then
|
||||||
|
echo "DOCKER_TAG=${DOCKER_REPO}:latest-ce" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "DOCKER_TAG=${DOCKER_REPO}:${SANITIZED_REF}-ce" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push by digest
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker/Dockerfile
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
push: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
|
||||||
|
outputs: type=image,name=${{ env.DOCKER_REPO }},push-by-digest=true,name-canonical=true,push=true
|
||||||
|
|
||||||
|
- name: Export digest
|
||||||
|
run: |
|
||||||
|
mkdir -p ${{ runner.temp }}/digests
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
|
- name: Upload digest
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: digests-${{ env.PLATFORM_PAIR }}
|
||||||
|
path: ${{ runner.temp }}/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
merge:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
steps:
|
||||||
|
- name: Download digests
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: ${{ runner.temp }}/digests
|
||||||
|
pattern: digests-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Create manifest list and push
|
||||||
|
working-directory: ${{ runner.temp }}/digests
|
||||||
|
env:
|
||||||
|
GIT_REF: ${{ github.head_ref || github.ref_name }}
|
||||||
|
run: |
|
||||||
|
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
|
||||||
|
if [ "${{ github.ref_name }}" = "master" ]; then
|
||||||
|
TAG="${DOCKER_REPO}:latest-ce"
|
||||||
|
else
|
||||||
|
TAG="${DOCKER_REPO}:${SANITIZED_REF}-ce"
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker buildx imagetools create -t $TAG \
|
||||||
|
$(printf '${{ env.DOCKER_REPO }}@sha256:%s ' *)
|
||||||
|
|
||||||
|
- name: Inspect image
|
||||||
|
env:
|
||||||
|
GIT_REF: ${{ github.head_ref || github.ref_name }}
|
||||||
|
run: |
|
||||||
|
SANITIZED_REF=$(echo "$GIT_REF" | sed 's/\//-/g')
|
||||||
|
if [ "${{ github.ref_name }}" = "master" ]; then
|
||||||
|
TAG="${DOCKER_REPO}:latest-ce"
|
||||||
|
else
|
||||||
|
TAG="${DOCKER_REPO}:${SANITIZED_REF}-ce"
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker buildx imagetools inspect $TAG
|
||||||
146
research/chatwoot/.github/workflows/run_foss_spec.yml
vendored
Normal file
146
research/chatwoot/.github/workflows/run_foss_spec.yml
vendored
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
name: Run Chatwoot CE spec
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Separate linting jobs for faster feedback
|
||||||
|
lint-backend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
bundler-cache: true
|
||||||
|
- name: Run Rubocop
|
||||||
|
run: bundle exec rubocop --parallel
|
||||||
|
|
||||||
|
lint-frontend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: 'pnpm'
|
||||||
|
- name: Install pnpm dependencies
|
||||||
|
run: pnpm i
|
||||||
|
- name: Run ESLint
|
||||||
|
run: pnpm run eslint
|
||||||
|
|
||||||
|
# Frontend tests run in parallel with backend
|
||||||
|
frontend-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: 'pnpm'
|
||||||
|
- name: Install pnpm dependencies
|
||||||
|
run: pnpm i
|
||||||
|
- name: Run frontend tests
|
||||||
|
run: pnpm run test:coverage
|
||||||
|
|
||||||
|
# Backend tests with parallelization
|
||||||
|
backend-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
ci_node_total: [16]
|
||||||
|
ci_node_index: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: pgvector/pgvector:pg16
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: ''
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
options: >-
|
||||||
|
--mount type=tmpfs,destination=/var/lib/postgresql/data
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
ports:
|
||||||
|
- 6379:6379
|
||||||
|
options: --entrypoint redis-server
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
|
- uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
bundler-cache: true
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Install pnpm dependencies
|
||||||
|
run: pnpm i
|
||||||
|
|
||||||
|
- name: Strip enterprise code
|
||||||
|
run: |
|
||||||
|
rm -rf enterprise
|
||||||
|
rm -rf spec/enterprise
|
||||||
|
|
||||||
|
- name: Create database
|
||||||
|
run: bundle exec rake db:create
|
||||||
|
|
||||||
|
- name: Seed database
|
||||||
|
run: bundle exec rake db:schema:load
|
||||||
|
|
||||||
|
- name: Run backend tests (parallelized)
|
||||||
|
run: |
|
||||||
|
# Get all spec files and split them using round-robin distribution
|
||||||
|
# This ensures slow tests are distributed evenly across all nodes
|
||||||
|
SPEC_FILES=($(find spec -name '*_spec.rb' | sort))
|
||||||
|
TESTS=""
|
||||||
|
|
||||||
|
for i in "${!SPEC_FILES[@]}"; do
|
||||||
|
# Assign spec to this node if: index % total == node_index
|
||||||
|
if [ $(( i % ${{ matrix.ci_node_total }} )) -eq ${{ matrix.ci_node_index }} ]; then
|
||||||
|
TESTS="$TESTS ${SPEC_FILES[$i]}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$TESTS" ]; then
|
||||||
|
bundle exec rspec --profile=10 --format progress --format json --out tmp/rspec_results.json $TESTS
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --openssl-legacy-provider
|
||||||
|
|
||||||
|
- name: Upload test results
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: rspec-results-${{ matrix.ci_node_index }}
|
||||||
|
path: tmp/rspec_results.json
|
||||||
|
|
||||||
|
- name: Upload rails log folder
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: rails-log-folder-${{ matrix.ci_node_index }}
|
||||||
|
path: log
|
||||||
100
research/chatwoot/.github/workflows/run_mfa_spec.yml
vendored
Normal file
100
research/chatwoot/.github/workflows/run_mfa_spec.yml
vendored
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
name: Run MFA Tests
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
# If two pushes happen within a short time in the same PR, cancel the run of the oldest push
|
||||||
|
concurrency:
|
||||||
|
group: pr-${{ github.workflow }}-${{ github.head_ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
# Only run if MFA test keys are available
|
||||||
|
if: github.event_name == 'workflow_dispatch' || (github.repository == 'chatwoot/chatwoot' && github.actor != 'dependabot[bot]')
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: pgvector/pgvector:pg15
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: ''
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
options: >-
|
||||||
|
--mount type=tmpfs,destination=/var/lib/postgresql/data
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
redis:
|
||||||
|
image: redis
|
||||||
|
ports:
|
||||||
|
- 6379:6379
|
||||||
|
options: --entrypoint redis-server
|
||||||
|
|
||||||
|
env:
|
||||||
|
RAILS_ENV: test
|
||||||
|
POSTGRES_HOST: localhost
|
||||||
|
# Active Record encryption keys required for MFA - test keys only, not for production use
|
||||||
|
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY: 'test_key_a6cde8f7b9c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7'
|
||||||
|
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: 'test_key_b7def9a8c0d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d8'
|
||||||
|
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: 'test_salt_c8efa0b9d1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d9'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
bundler-cache: true
|
||||||
|
|
||||||
|
- name: Create database
|
||||||
|
run: bundle exec rake db:create
|
||||||
|
|
||||||
|
- name: Install pgvector extension
|
||||||
|
run: |
|
||||||
|
PGPASSWORD="" psql -h localhost -U postgres -d chatwoot_test -c "CREATE EXTENSION IF NOT EXISTS vector;"
|
||||||
|
|
||||||
|
- name: Seed database
|
||||||
|
run: bundle exec rake db:schema:load
|
||||||
|
|
||||||
|
- name: Run MFA-related backend tests
|
||||||
|
run: |
|
||||||
|
bundle exec rspec \
|
||||||
|
spec/services/mfa/token_service_spec.rb \
|
||||||
|
spec/services/mfa/authentication_service_spec.rb \
|
||||||
|
spec/requests/api/v1/profile/mfa_controller_spec.rb \
|
||||||
|
spec/controllers/devise_overrides/sessions_controller_spec.rb \
|
||||||
|
spec/models/application_record_external_credentials_encryption_spec.rb \
|
||||||
|
--profile=10 \
|
||||||
|
--format documentation
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --openssl-legacy-provider
|
||||||
|
|
||||||
|
- name: Run MFA-related tests in user_spec
|
||||||
|
run: |
|
||||||
|
# Run specific MFA-related tests from user_spec
|
||||||
|
bundle exec rspec spec/models/user_spec.rb \
|
||||||
|
-e "two factor" \
|
||||||
|
-e "2FA" \
|
||||||
|
-e "MFA" \
|
||||||
|
-e "otp" \
|
||||||
|
-e "backup code" \
|
||||||
|
--profile=10 \
|
||||||
|
--format documentation
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --openssl-legacy-provider
|
||||||
|
|
||||||
|
- name: Upload test logs
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: mfa-test-logs
|
||||||
|
path: |
|
||||||
|
log/test.log
|
||||||
|
tmp/screenshots/
|
||||||
52
research/chatwoot/.github/workflows/size-limit.yml
vendored
Normal file
52
research/chatwoot/.github/workflows/size-limit.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
name: Run Size Limit Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
|
||||||
|
# If two pushes happen within a short time in the same PR, cancel the run of the oldest push
|
||||||
|
concurrency:
|
||||||
|
group: pr-${{ github.workflow }}-${{ github.head_ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
|
- uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: pnpm
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Strip enterprise code
|
||||||
|
run: |
|
||||||
|
rm -rf enterprise
|
||||||
|
rm -rf spec/enterprise
|
||||||
|
|
||||||
|
- name: setup env
|
||||||
|
run: |
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
- name: Run asset compile
|
||||||
|
run: bundle exec rake assets:precompile
|
||||||
|
env:
|
||||||
|
RAILS_ENV: production
|
||||||
|
|
||||||
|
- name: Size Check
|
||||||
|
run: pnpm run size
|
||||||
28
research/chatwoot/.github/workflows/stale.yml
vendored
Normal file
28
research/chatwoot/.github/workflows/stale.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# This workflow warns and then closes PRs that have had no activity for a specified amount of time.
|
||||||
|
#
|
||||||
|
# You can adjust the behavior by modifying this file.
|
||||||
|
# For more information, see:
|
||||||
|
# https://github.com/actions/stale
|
||||||
|
name: Mark stale issues and pull requests
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '28 3 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v5
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
days-before-issue-close: -1,
|
||||||
|
days-before-issue-stale: -1
|
||||||
|
days-before-pr-close: -1,
|
||||||
|
days-before-pr-stale: 30,
|
||||||
|
stale-pr-message: '🐢 Turtley slow progress alert! This pull request has been idle for over 30 days. Can we please speed things up and either merge it or release it back into the wild?'
|
||||||
|
stale-pr-label: 'stale'
|
||||||
40
research/chatwoot/.github/workflows/test_docker_build.yml
vendored
Normal file
40
research/chatwoot/.github/workflows/test_docker_build.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: Test Docker Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- master
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-build:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- platform: linux/amd64
|
||||||
|
runner: ubuntu-latest
|
||||||
|
- platform: linux/arm64
|
||||||
|
runner: ubuntu-22.04-arm
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker/Dockerfile
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
push: false
|
||||||
|
load: false
|
||||||
|
cache-from: type=gha,scope=${{ matrix.platform }}
|
||||||
|
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
|
||||||
106
research/chatwoot/.gitignore
vendored
Normal file
106
research/chatwoot/.gitignore
vendored
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
|
||||||
|
#
|
||||||
|
# If you find yourself ignoring temporary files generated by your text editor
|
||||||
|
# or operating system, you probably want to add a global ignore instead:
|
||||||
|
# git config --global core.excludesfile '~/.gitignore_global'
|
||||||
|
|
||||||
|
# Ignore bundler config.
|
||||||
|
/.bundle
|
||||||
|
|
||||||
|
# Ignore the default SQLite database.
|
||||||
|
/db/*.sqlite3
|
||||||
|
/db/*.sqlite3-journal
|
||||||
|
|
||||||
|
# Ignore all logfiles and tempfiles.
|
||||||
|
/log/*
|
||||||
|
/tmp/*
|
||||||
|
!/log/.keep
|
||||||
|
!/tmp/.keep
|
||||||
|
*.mmdb
|
||||||
|
|
||||||
|
# Ignore Byebug command history file.
|
||||||
|
.byebug_history
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
# Ignore application configuration
|
||||||
|
node_modules
|
||||||
|
master.key
|
||||||
|
*.rdb
|
||||||
|
|
||||||
|
# Ignore env files
|
||||||
|
.env
|
||||||
|
|
||||||
|
public/uploads
|
||||||
|
public/packs*
|
||||||
|
public/assets/administrate*
|
||||||
|
public/assets/action*.js
|
||||||
|
public/assets/activestorage*.js
|
||||||
|
public/assets/trix*
|
||||||
|
public/assets/belongs_to*.js
|
||||||
|
public/assets/manifest*.js
|
||||||
|
public/assets/manifest*.js
|
||||||
|
public/assets/*.js.gz
|
||||||
|
public/assets/secretField*
|
||||||
|
public/assets/.sprockets-manifest-*.json
|
||||||
|
|
||||||
|
# VIM files
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.un~
|
||||||
|
.jest-cache
|
||||||
|
|
||||||
|
# ignore jetbrains IDE files
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# coverage report
|
||||||
|
buildreports
|
||||||
|
coverage
|
||||||
|
|
||||||
|
/storage
|
||||||
|
|
||||||
|
# ignore packages
|
||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
*.dump
|
||||||
|
|
||||||
|
|
||||||
|
# cypress
|
||||||
|
test/cypress/videos/*
|
||||||
|
|
||||||
|
/config/master.key
|
||||||
|
/config/*.enc
|
||||||
|
|
||||||
|
|
||||||
|
# yalc for local testing
|
||||||
|
.yalc
|
||||||
|
yalc.lock
|
||||||
|
|
||||||
|
/public/packs
|
||||||
|
/public/packs-test
|
||||||
|
/node_modules
|
||||||
|
/yarn-error.log
|
||||||
|
yarn-debug.log*
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# Vite Ruby
|
||||||
|
/public/vite*
|
||||||
|
# Vite uses dotenv and suggests to ignore local-only env files. See
|
||||||
|
# https://vitejs.dev/guide/env-and-mode.html#env-files
|
||||||
|
*.local
|
||||||
|
|
||||||
|
|
||||||
|
# TextEditors & AI Agents config files
|
||||||
|
.vscode
|
||||||
|
.claude/settings.local.json
|
||||||
|
.cursor
|
||||||
|
.codex/
|
||||||
|
.claude/
|
||||||
|
CLAUDE.local.md
|
||||||
|
|
||||||
|
# Histoire deployment
|
||||||
|
.netlify
|
||||||
|
.histoire
|
||||||
|
.pnpm-store/*
|
||||||
|
local/
|
||||||
|
Procfile.worktree
|
||||||
11
research/chatwoot/.husky/pre-commit
Executable file
11
research/chatwoot/.husky/pre-commit
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
# lint js and vue files
|
||||||
|
npx --no-install lint-staged
|
||||||
|
|
||||||
|
# lint only staged ruby files that still exist (not deleted)
|
||||||
|
git diff --name-only --cached | xargs -I {} sh -c 'test -f "{}" && echo "{}"' | grep '\.rb$' | xargs -I {} bundle exec rubocop --force-exclusion -a "{}" || true
|
||||||
|
|
||||||
|
# stage rubocop changes to files
|
||||||
|
git diff --name-only --cached | xargs -I {} sh -c 'test -f "{}" && git add "{}"' || true
|
||||||
4
research/chatwoot/.husky/pre-push
Executable file
4
research/chatwoot/.husky/pre-push
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
sh bin/validate_push
|
||||||
1
research/chatwoot/.nvmrc
Normal file
1
research/chatwoot/.nvmrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
24.13.0
|
||||||
6
research/chatwoot/.prettierrc
Normal file
6
research/chatwoot/.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 80,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"arrowParens": "avoid"
|
||||||
|
}
|
||||||
7
research/chatwoot/.qlty/.gitignore
vendored
Normal file
7
research/chatwoot/.qlty/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
*
|
||||||
|
!configs
|
||||||
|
!configs/**
|
||||||
|
!hooks
|
||||||
|
!hooks/**
|
||||||
|
!qlty.toml
|
||||||
|
!.gitignore
|
||||||
2
research/chatwoot/.qlty/configs/.hadolint.yaml
Normal file
2
research/chatwoot/.qlty/configs/.hadolint.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ignored:
|
||||||
|
- DL3008
|
||||||
1
research/chatwoot/.qlty/configs/.shellcheckrc
Normal file
1
research/chatwoot/.qlty/configs/.shellcheckrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
source-path=SCRIPTDIR
|
||||||
8
research/chatwoot/.qlty/configs/.yamllint.yaml
Normal file
8
research/chatwoot/.qlty/configs/.yamllint.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
rules:
|
||||||
|
document-start: disable
|
||||||
|
quoted-strings:
|
||||||
|
required: only-when-needed
|
||||||
|
extra-allowed: ["{|}"]
|
||||||
|
key-duplicates: {}
|
||||||
|
octal-values:
|
||||||
|
forbid-implicit-octal: true
|
||||||
84
research/chatwoot/.qlty/qlty.toml
Normal file
84
research/chatwoot/.qlty/qlty.toml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# This file was automatically generated by `qlty init`.
|
||||||
|
# You can modify it to suit your needs.
|
||||||
|
# We recommend you to commit this file to your repository.
|
||||||
|
#
|
||||||
|
# This configuration is used by both Qlty CLI and Qlty Cloud.
|
||||||
|
#
|
||||||
|
# Qlty CLI -- Code quality toolkit for developers
|
||||||
|
# Qlty Cloud -- Fully automated Code Health Platform
|
||||||
|
#
|
||||||
|
# Try Qlty Cloud: https://qlty.sh
|
||||||
|
#
|
||||||
|
# For a guide to configuration, visit https://qlty.sh/d/config
|
||||||
|
# Or for a full reference, visit https://qlty.sh/d/qlty-toml
|
||||||
|
config_version = "0"
|
||||||
|
|
||||||
|
exclude_patterns = [
|
||||||
|
"*_min.*",
|
||||||
|
"*-min.*",
|
||||||
|
"*.min.*",
|
||||||
|
"**/.yarn/**",
|
||||||
|
"**/*.d.ts",
|
||||||
|
"**/assets/**",
|
||||||
|
"**/bower_components/**",
|
||||||
|
"**/build/**",
|
||||||
|
"**/cache/**",
|
||||||
|
"**/config/**",
|
||||||
|
"**/db/**",
|
||||||
|
"**/deps/**",
|
||||||
|
"**/dist/**",
|
||||||
|
"**/extern/**",
|
||||||
|
"**/external/**",
|
||||||
|
"**/generated/**",
|
||||||
|
"**/Godeps/**",
|
||||||
|
"**/gradlew/**",
|
||||||
|
"**/mvnw/**",
|
||||||
|
"**/node_modules/**",
|
||||||
|
"**/protos/**",
|
||||||
|
"**/seed/**",
|
||||||
|
"**/target/**",
|
||||||
|
"**/templates/**",
|
||||||
|
"**/testdata/**",
|
||||||
|
"**/vendor/**", "spec/", "**/specs/**/**", "**/spec/**/**", "db/*", "bin/**/*", "db/**/*", "config/**/*", "public/**/*", "vendor/**/*", "node_modules/**/*", "lib/tasks/auto_annotate_models.rake", "app/test-matchers.js", "docs/*", "**/*.md", "**/*.yml", "app/javascript/dashboard/i18n/locale", "**/*.stories.js", "stories/", "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js", "app/javascript/shared/constants/countries.js", "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/languages.js", "app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js", "app/javascript/dashboard/routes/dashboard/settings/automation/constants.js", "app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js", "app/javascript/dashboard/routes/dashboard/settings/reports/constants.js", "app/javascript/dashboard/store/storeFactory.js", "app/javascript/dashboard/i18n/index.js", "app/javascript/widget/i18n/index.js", "app/javascript/survey/i18n/index.js", "app/javascript/shared/constants/locales.js", "app/javascript/dashboard/helper/specs/macrosFixtures.js", "app/javascript/dashboard/routes/dashboard/settings/macros/constants.js", "**/fixtures/**", "**/*/fixtures.js",
|
||||||
|
]
|
||||||
|
|
||||||
|
test_patterns = [
|
||||||
|
"**/test/**",
|
||||||
|
"**/spec/**",
|
||||||
|
"**/*.test.*",
|
||||||
|
"**/*.spec.*",
|
||||||
|
"**/*_test.*",
|
||||||
|
"**/*_spec.*",
|
||||||
|
"**/test_*.*",
|
||||||
|
"**/spec_*.*",
|
||||||
|
]
|
||||||
|
|
||||||
|
[smells]
|
||||||
|
mode = "comment"
|
||||||
|
|
||||||
|
[smells.boolean_logic]
|
||||||
|
threshold = 4
|
||||||
|
|
||||||
|
[smells.file_complexity]
|
||||||
|
threshold = 66
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[smells.return_statements]
|
||||||
|
threshold = 4
|
||||||
|
|
||||||
|
[smells.nested_control_flow]
|
||||||
|
threshold = 4
|
||||||
|
|
||||||
|
[smells.function_parameters]
|
||||||
|
threshold = 4
|
||||||
|
|
||||||
|
[smells.function_complexity]
|
||||||
|
threshold = 5
|
||||||
|
|
||||||
|
[smells.duplication]
|
||||||
|
enabled = true
|
||||||
|
threshold = 20
|
||||||
|
|
||||||
|
[[source]]
|
||||||
|
name = "default"
|
||||||
|
default = true
|
||||||
1
research/chatwoot/.rspec
Normal file
1
research/chatwoot/.rspec
Normal file
@@ -0,0 +1 @@
|
|||||||
|
--require spec_helper
|
||||||
350
research/chatwoot/.rubocop.yml
Normal file
350
research/chatwoot/.rubocop.yml
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
plugins:
|
||||||
|
- rubocop-performance
|
||||||
|
- rubocop-rails
|
||||||
|
- rubocop-rspec
|
||||||
|
- rubocop-factory_bot
|
||||||
|
|
||||||
|
require:
|
||||||
|
- ./rubocop/use_from_email.rb
|
||||||
|
- ./rubocop/custom_cop_location.rb
|
||||||
|
- ./rubocop/attachment_download.rb
|
||||||
|
- ./rubocop/one_class_per_file.rb
|
||||||
|
|
||||||
|
Layout/LineLength:
|
||||||
|
Max: 150
|
||||||
|
|
||||||
|
Metrics/ClassLength:
|
||||||
|
Max: 175
|
||||||
|
Exclude:
|
||||||
|
- 'app/models/message.rb'
|
||||||
|
- 'app/models/conversation.rb'
|
||||||
|
|
||||||
|
Metrics/MethodLength:
|
||||||
|
Max: 19
|
||||||
|
Exclude:
|
||||||
|
- 'enterprise/lib/captain/agent.rb'
|
||||||
|
|
||||||
|
RSpec/ExampleLength:
|
||||||
|
Max: 50
|
||||||
|
|
||||||
|
Style/Documentation:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/ExponentialNotation:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/FrozenStringLiteralComment:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/SymbolArray:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/OpenStructUse:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Chatwoot/AttachmentDownload:
|
||||||
|
Enabled: true
|
||||||
|
Exclude:
|
||||||
|
- 'spec/**/*'
|
||||||
|
- 'test/**/*'
|
||||||
|
|
||||||
|
Style/OptionalBooleanParameter:
|
||||||
|
Exclude:
|
||||||
|
- 'app/services/email_templates/db_resolver_service.rb'
|
||||||
|
- 'app/dispatchers/dispatcher.rb'
|
||||||
|
|
||||||
|
Style/GlobalVars:
|
||||||
|
Exclude:
|
||||||
|
- 'config/initializers/01_redis.rb'
|
||||||
|
- 'config/initializers/rack_attack.rb'
|
||||||
|
- 'lib/redis/alfred.rb'
|
||||||
|
- 'lib/global_config.rb'
|
||||||
|
|
||||||
|
Style/ClassVars:
|
||||||
|
Exclude:
|
||||||
|
- 'app/services/email_templates/db_resolver_service.rb'
|
||||||
|
|
||||||
|
Lint/MissingSuper:
|
||||||
|
Exclude:
|
||||||
|
- 'app/drops/base_drop.rb'
|
||||||
|
|
||||||
|
Lint/SymbolConversion:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Lint/EmptyBlock:
|
||||||
|
Exclude:
|
||||||
|
- 'app/views/api/v1/accounts/conversations/toggle_status.json.jbuilder'
|
||||||
|
|
||||||
|
Lint/OrAssignmentToConstant:
|
||||||
|
Exclude:
|
||||||
|
- 'lib/redis/config.rb'
|
||||||
|
|
||||||
|
Metrics/BlockLength:
|
||||||
|
Max: 30
|
||||||
|
Exclude:
|
||||||
|
- spec/**/*
|
||||||
|
- '**/routes.rb'
|
||||||
|
- 'config/environments/*'
|
||||||
|
- db/schema.rb
|
||||||
|
|
||||||
|
Metrics/ModuleLength:
|
||||||
|
Exclude:
|
||||||
|
- lib/seeders/message_seeder.rb
|
||||||
|
- spec/support/slack_stubs.rb
|
||||||
|
|
||||||
|
Rails/HelperInstanceVariable:
|
||||||
|
Exclude:
|
||||||
|
- enterprise/app/helpers/captain/chat_helper.rb
|
||||||
|
- enterprise/app/helpers/captain/chat_response_helper.rb
|
||||||
|
Rails/ApplicationController:
|
||||||
|
Exclude:
|
||||||
|
- 'app/controllers/api/v1/widget/messages_controller.rb'
|
||||||
|
- 'app/controllers/dashboard_controller.rb'
|
||||||
|
- 'app/controllers/widget_tests_controller.rb'
|
||||||
|
- 'app/controllers/widgets_controller.rb'
|
||||||
|
- 'app/controllers/platform_controller.rb'
|
||||||
|
- 'app/controllers/public_controller.rb'
|
||||||
|
- 'app/controllers/survey/responses_controller.rb'
|
||||||
|
|
||||||
|
Rails/FindEach:
|
||||||
|
Enabled: true
|
||||||
|
Include:
|
||||||
|
- 'app/**/*.rb'
|
||||||
|
|
||||||
|
Rails/CompactBlank:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/EnvironmentVariableAccess:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/TimeZoneAssignment:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/RedundantPresenceValidationOnBelongsTo:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/InverseOf:
|
||||||
|
Exclude:
|
||||||
|
- enterprise/app/models/captain/assistant.rb
|
||||||
|
|
||||||
|
Rails/UniqueValidationWithoutIndex:
|
||||||
|
Exclude:
|
||||||
|
- app/models/canned_response.rb
|
||||||
|
- app/models/telegram_bot.rb
|
||||||
|
- enterprise/app/models/captain_inbox.rb
|
||||||
|
- 'app/models/channel/twitter_profile.rb'
|
||||||
|
- 'app/models/webhook.rb'
|
||||||
|
- 'app/models/contact.rb'
|
||||||
|
|
||||||
|
Style/ClassAndModuleChildren:
|
||||||
|
EnforcedStyle: compact
|
||||||
|
Exclude:
|
||||||
|
- 'config/application.rb'
|
||||||
|
- 'config/initializers/monkey_patches/*'
|
||||||
|
|
||||||
|
Style/MapToHash:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/HashSyntax:
|
||||||
|
Enabled: true
|
||||||
|
EnforcedStyle: no_mixed_keys
|
||||||
|
EnforcedShorthandSyntax: never
|
||||||
|
|
||||||
|
RSpec/NestedGroups:
|
||||||
|
Enabled: true
|
||||||
|
Max: 4
|
||||||
|
|
||||||
|
RSpec/MessageSpies:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
RSpec/StubbedMock:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Naming/VariableNumber:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Naming/MemoizedInstanceVariableName:
|
||||||
|
Exclude:
|
||||||
|
- 'app/models/message.rb'
|
||||||
|
|
||||||
|
Style/GuardClause:
|
||||||
|
Exclude:
|
||||||
|
- 'app/builders/account_builder.rb'
|
||||||
|
- 'app/models/attachment.rb'
|
||||||
|
- 'app/models/message.rb'
|
||||||
|
|
||||||
|
Metrics/AbcSize:
|
||||||
|
Max: 26
|
||||||
|
Exclude:
|
||||||
|
- 'app/controllers/concerns/auth_helper.rb'
|
||||||
|
|
||||||
|
- 'app/models/integrations/hook.rb'
|
||||||
|
- 'app/models/canned_response.rb'
|
||||||
|
- 'app/models/telegram_bot.rb'
|
||||||
|
|
||||||
|
Rails/RenderInline:
|
||||||
|
Exclude:
|
||||||
|
- 'app/controllers/swagger_controller.rb'
|
||||||
|
|
||||||
|
Rails/ThreeStateBooleanColumn:
|
||||||
|
Exclude:
|
||||||
|
- 'db/migrate/20230503101201_create_sla_policies.rb'
|
||||||
|
|
||||||
|
RSpec/IndexedLet:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
RSpec/NamedSubject:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
# we should bring this down
|
||||||
|
RSpec/MultipleExpectations:
|
||||||
|
Max: 7
|
||||||
|
|
||||||
|
RSpec/MultipleMemoizedHelpers:
|
||||||
|
Max: 14
|
||||||
|
|
||||||
|
# custom rules
|
||||||
|
UseFromEmail:
|
||||||
|
Enabled: true
|
||||||
|
Exclude:
|
||||||
|
- 'app/models/user.rb'
|
||||||
|
- 'app/models/contact.rb'
|
||||||
|
|
||||||
|
CustomCopLocation:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Style/OneClassPerFile:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
AllCops:
|
||||||
|
NewCops: enable
|
||||||
|
Exclude:
|
||||||
|
- 'bin/**/*'
|
||||||
|
- 'db/schema.rb'
|
||||||
|
- 'public/**/*'
|
||||||
|
- 'config/initializers/bot.rb'
|
||||||
|
- 'vendor/**/*'
|
||||||
|
- 'node_modules/**/*'
|
||||||
|
- 'lib/tasks/auto_annotate_models.rake'
|
||||||
|
- 'config/environments/**/*'
|
||||||
|
- 'tmp/**/*'
|
||||||
|
- 'storage/**/*'
|
||||||
|
- 'db/migrate/20230426130150_init_schema.rb'
|
||||||
|
|
||||||
|
FactoryBot/SyntaxMethods:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
# Disable new rules causing errors
|
||||||
|
Layout/LeadingCommentSpace:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/ReturnNilInPredicateMethodDefinition:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/RedundantParentheses:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Performance/StringIdentifierArgument:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Layout/EmptyLinesAroundExceptionHandlingKeywords:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Lint/LiteralAsCondition:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/RedundantReturn:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Layout/SpaceAroundOperators:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/EnvLocal:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/WhereRange:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Lint/UselessConstantScoping:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/MultipleComparison:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Bundler/OrderedGems:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
RSpec/ExampleWording:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
RSpec/ReceiveMessages:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
FactoryBot/AssociationStyle:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/EnumSyntax:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Lint/RedundantTypeConversion:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
# Additional rules to disable
|
||||||
|
Rails/RedundantActiveRecordAllMethod:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Layout/TrailingEmptyLines:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
|
Style/SafeNavigationChainLength:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Lint/SafeNavigationConsistency:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Lint/CopDirectiveSyntax:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
# Final set of rules to disable
|
||||||
|
FactoryBot/ExcessiveCreateList:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
RSpec/MissingExpectationTargetMethod:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Performance/InefficientHashSearch:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/RedundantSelfAssignmentBranch:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/YAMLFileRead:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Layout/ExtraSpacing:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/RedundantFilterChain:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Performance/MapMethodChain:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/RootPathnameMethods:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/SuperArguments:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
# Final remaining rules to disable
|
||||||
|
Rails/Delegate:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/CaseLikeIf:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
FactoryBot/RedundantFactoryOption:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
FactoryBot/FactoryAssociationWithStrategy:
|
||||||
|
Enabled: false
|
||||||
1
research/chatwoot/.ruby-version
Normal file
1
research/chatwoot/.ruby-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.4.4
|
||||||
286
research/chatwoot/.scss-lint.yml
Normal file
286
research/chatwoot/.scss-lint.yml
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
# Default application configuration that all configurations inherit from.
|
||||||
|
|
||||||
|
scss_files: '**/*.scss'
|
||||||
|
plugin_directories: ['.scss-linters']
|
||||||
|
|
||||||
|
# List of gem names to load custom linters from (make sure they are already
|
||||||
|
# installed)
|
||||||
|
plugin_gems: []
|
||||||
|
|
||||||
|
# Default severity of all linters.
|
||||||
|
severity: warning
|
||||||
|
|
||||||
|
linters:
|
||||||
|
BangFormat:
|
||||||
|
enabled: true
|
||||||
|
space_before_bang: true
|
||||||
|
space_after_bang: false
|
||||||
|
|
||||||
|
BemDepth:
|
||||||
|
enabled: false
|
||||||
|
max_elements: 1
|
||||||
|
|
||||||
|
BorderZero:
|
||||||
|
enabled: true
|
||||||
|
convention: zero # or `none`
|
||||||
|
|
||||||
|
ChainedClasses:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
ColorKeyword:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
ColorVariable:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
Comment:
|
||||||
|
enabled: true
|
||||||
|
style: silent
|
||||||
|
|
||||||
|
DebugStatement:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
DeclarationOrder:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
DisableLinterReason:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
DuplicateProperty:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
ElsePlacement:
|
||||||
|
enabled: true
|
||||||
|
style: new_line
|
||||||
|
|
||||||
|
EmptyLineBetweenBlocks:
|
||||||
|
enabled: true
|
||||||
|
ignore_single_line_blocks: true
|
||||||
|
|
||||||
|
EmptyRule:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
ExtendDirective:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
FinalNewline:
|
||||||
|
enabled: true
|
||||||
|
present: true
|
||||||
|
|
||||||
|
HexLength:
|
||||||
|
enabled: true
|
||||||
|
style: short # or 'long'
|
||||||
|
|
||||||
|
HexNotation:
|
||||||
|
enabled: true
|
||||||
|
style: lowercase # or 'uppercase'
|
||||||
|
|
||||||
|
HexValidation:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
IdSelector:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
ImportantRule:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
ImportPath:
|
||||||
|
enabled: true
|
||||||
|
leading_underscore: false
|
||||||
|
filename_extension: false
|
||||||
|
|
||||||
|
Indentation:
|
||||||
|
enabled: true
|
||||||
|
allow_non_nested_indentation: false
|
||||||
|
character: space # or 'tab'
|
||||||
|
width: 2
|
||||||
|
|
||||||
|
LeadingZero:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
MergeableSelector:
|
||||||
|
enabled: true
|
||||||
|
force_nesting: true
|
||||||
|
|
||||||
|
NameFormat:
|
||||||
|
enabled: true
|
||||||
|
allow_leading_underscore: true
|
||||||
|
convention: hyphenated_lowercase # or 'camel_case', or 'snake_case', or a regex pattern
|
||||||
|
|
||||||
|
NestingDepth:
|
||||||
|
enabled: true
|
||||||
|
max_depth: 6
|
||||||
|
ignore_parent_selectors: false
|
||||||
|
|
||||||
|
PlaceholderInExtend:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
PrivateNamingConvention:
|
||||||
|
enabled: false
|
||||||
|
prefix: _
|
||||||
|
|
||||||
|
PropertyCount:
|
||||||
|
enabled: false
|
||||||
|
include_nested: false
|
||||||
|
max_properties: 10
|
||||||
|
|
||||||
|
PropertySortOrder:
|
||||||
|
enabled: true
|
||||||
|
ignore_unspecified: false
|
||||||
|
min_properties: 2
|
||||||
|
separate_groups: false
|
||||||
|
|
||||||
|
PropertySpelling:
|
||||||
|
enabled: true
|
||||||
|
extra_properties: []
|
||||||
|
disabled_properties: []
|
||||||
|
|
||||||
|
PropertyUnits:
|
||||||
|
enabled: true
|
||||||
|
global: [
|
||||||
|
'ch',
|
||||||
|
'em',
|
||||||
|
'ex',
|
||||||
|
'rem', # Font-relative lengths
|
||||||
|
'cm',
|
||||||
|
'in',
|
||||||
|
'mm',
|
||||||
|
'pc',
|
||||||
|
'pt',
|
||||||
|
'px',
|
||||||
|
'q', # Absolute lengths
|
||||||
|
'vh',
|
||||||
|
'vw',
|
||||||
|
'vmin',
|
||||||
|
'vmax', # Viewport-percentage lengths
|
||||||
|
'fr', # Grid fractional lengths
|
||||||
|
'deg',
|
||||||
|
'grad',
|
||||||
|
'rad',
|
||||||
|
'turn', # Angle
|
||||||
|
'ms',
|
||||||
|
's', # Duration
|
||||||
|
'Hz',
|
||||||
|
'kHz', # Frequency
|
||||||
|
'dpi',
|
||||||
|
'dpcm',
|
||||||
|
'dppx', # Resolution
|
||||||
|
'%',
|
||||||
|
] # Other
|
||||||
|
properties: {}
|
||||||
|
|
||||||
|
PseudoElement:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
QualifyingElement:
|
||||||
|
enabled: true
|
||||||
|
allow_element_with_attribute: false
|
||||||
|
allow_element_with_class: false
|
||||||
|
allow_element_with_id: false
|
||||||
|
exclude:
|
||||||
|
- 'app/assets/stylesheets/administrate/components/_buttons.scss'
|
||||||
|
|
||||||
|
SelectorDepth:
|
||||||
|
enabled: true
|
||||||
|
max_depth: 5
|
||||||
|
|
||||||
|
SelectorFormat:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
Shorthand:
|
||||||
|
enabled: true
|
||||||
|
allowed_shorthands: [1, 2, 3, 4]
|
||||||
|
|
||||||
|
SingleLinePerProperty:
|
||||||
|
enabled: true
|
||||||
|
allow_single_line_rule_sets: true
|
||||||
|
|
||||||
|
SingleLinePerSelector:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
SpaceAfterComma:
|
||||||
|
enabled: true
|
||||||
|
style: one_space # or 'no_space', or 'at_least_one_space'
|
||||||
|
|
||||||
|
SpaceAfterComment:
|
||||||
|
enabled: false
|
||||||
|
style: one_space # or 'no_space', or 'at_least_one_space'
|
||||||
|
allow_empty_comments: true
|
||||||
|
|
||||||
|
SpaceAfterPropertyColon:
|
||||||
|
enabled: true
|
||||||
|
style: one_space # or 'no_space', or 'at_least_one_space', or 'aligned'
|
||||||
|
|
||||||
|
SpaceAfterPropertyName:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
SpaceAfterVariableColon:
|
||||||
|
enabled: false
|
||||||
|
style: one_space # or 'no_space', 'at_least_one_space' or 'one_space_or_newline'
|
||||||
|
|
||||||
|
SpaceAfterVariableName:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
SpaceAroundOperator:
|
||||||
|
enabled: true
|
||||||
|
style: one_space # or 'at_least_one_space', or 'no_space'
|
||||||
|
|
||||||
|
SpaceBeforeBrace:
|
||||||
|
enabled: true
|
||||||
|
style: space # or 'new_line'
|
||||||
|
allow_single_line_padding: false
|
||||||
|
|
||||||
|
SpaceBetweenParens:
|
||||||
|
enabled: true
|
||||||
|
spaces: 0
|
||||||
|
|
||||||
|
StringQuotes:
|
||||||
|
enabled: true
|
||||||
|
style: single_quotes # or double_quotes
|
||||||
|
|
||||||
|
TrailingSemicolon:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
TrailingWhitespace:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
TrailingZero:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
TransitionAll:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
UnnecessaryMantissa:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
UnnecessaryParentReference:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
UrlFormat:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
UrlQuotes:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
VariableForProperty:
|
||||||
|
enabled: false
|
||||||
|
properties: []
|
||||||
|
|
||||||
|
VendorPrefix:
|
||||||
|
enabled: true
|
||||||
|
identifier_list: base
|
||||||
|
additional_identifiers: []
|
||||||
|
excluded_identifiers: []
|
||||||
|
|
||||||
|
ZeroUnit:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
Compass::*:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
exclude:
|
||||||
|
- 'app/javascript/widget/assets/scss/_reset.scss'
|
||||||
|
- 'app/javascript/widget/assets/scss/sdk.css'
|
||||||
|
- 'app/assets/stylesheets/administrate/reset/_normalize.scss'
|
||||||
|
- 'app/javascript/shared/assets/stylesheets/*.scss'
|
||||||
|
- 'app/javascript/dashboard/assets/scss/_woot.scss'
|
||||||
1
research/chatwoot/.slugignore
Normal file
1
research/chatwoot/.slugignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/spec
|
||||||
1
research/chatwoot/.windsurf/rules/chatwoot.md
Symbolic link
1
research/chatwoot/.windsurf/rules/chatwoot.md
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../AGENTS.md
|
||||||
104
research/chatwoot/AGENTS.md
Normal file
104
research/chatwoot/AGENTS.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# Chatwoot Development Guidelines
|
||||||
|
|
||||||
|
## Build / Test / Lint
|
||||||
|
|
||||||
|
- **Setup**: `bundle install && pnpm install`
|
||||||
|
- **Run Dev**: `pnpm dev` or `overmind start -f ./Procfile.dev`
|
||||||
|
- **Seed Local Test Data**: `bundle exec rails db:seed` (quickly populates minimal data for standard feature verification)
|
||||||
|
- **Seed Search Test Data**: `bundle exec rails search:setup_test_data` (bulk fixture generation for search/performance/manual load scenarios)
|
||||||
|
- **Seed Account Sample Data (richer test data)**: `Seeders::AccountSeeder` is available as an internal utility and is exposed through Super Admin `Accounts#seed`, but can be used directly in dev workflows too:
|
||||||
|
- UI path: Super Admin → Accounts → Seed (enqueues `Internal::SeedAccountJob`).
|
||||||
|
- CLI path: `bundle exec rails runner "Internal::SeedAccountJob.perform_now(Account.find(<id>))"` (or call `Seeders::AccountSeeder.new(account: Account.find(<id>)).perform!` directly).
|
||||||
|
- **Lint JS/Vue**: `pnpm eslint` / `pnpm eslint:fix`
|
||||||
|
- **Lint Ruby**: `bundle exec rubocop -a`
|
||||||
|
- **Test JS**: `pnpm test` or `pnpm test:watch`
|
||||||
|
- **Test Ruby**: `bundle exec rspec spec/path/to/file_spec.rb`
|
||||||
|
- **Single Test**: `bundle exec rspec spec/path/to/file_spec.rb:LINE_NUMBER`
|
||||||
|
- **Run Project**: `overmind start -f Procfile.dev`
|
||||||
|
- **Ruby Version**: Manage Ruby via `rbenv` and install the version listed in `.ruby-version` (e.g., `rbenv install $(cat .ruby-version)`)
|
||||||
|
- **rbenv setup**: Before running any `bundle` or `rspec` commands, init rbenv in your shell (`eval "$(rbenv init -)"`) so the correct Ruby/Bundler versions are used
|
||||||
|
- Always prefer `bundle exec` for Ruby CLI tasks (rspec, rake, rubocop, etc.)
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- **Ruby**: Follow RuboCop rules (150 character max line length)
|
||||||
|
- **Vue/JS**: Use ESLint (Airbnb base + Vue 3 recommended)
|
||||||
|
- **Vue Components**: Use PascalCase
|
||||||
|
- **Events**: Use camelCase
|
||||||
|
- **I18n**: No bare strings in templates; use i18n
|
||||||
|
- **Error Handling**: Use custom exceptions (`lib/custom_exceptions/`)
|
||||||
|
- **Models**: Validate presence/uniqueness, add proper indexes
|
||||||
|
- **Type Safety**: Use PropTypes in Vue, strong params in Rails
|
||||||
|
- **Naming**: Use clear, descriptive names with consistent casing
|
||||||
|
- **Vue API**: Always use Composition API with `<script setup>` at the top
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
- **Tailwind Only**:
|
||||||
|
- Do not write custom CSS
|
||||||
|
- Do not use scoped CSS
|
||||||
|
- Do not use inline styles
|
||||||
|
- Always use Tailwind utility classes
|
||||||
|
- **Colors**: Refer to `tailwind.config.js` for color definitions
|
||||||
|
|
||||||
|
## General Guidelines
|
||||||
|
|
||||||
|
- MVP focus: Least code change, happy-path only
|
||||||
|
- No unnecessary defensive programming
|
||||||
|
- Ship the happy path first: limit guards/fallbacks to what production has proven necessary, then iterate
|
||||||
|
- Prefer minimal, readable code over elaborate abstractions; clarity beats cleverness
|
||||||
|
- Break down complex tasks into small, testable units
|
||||||
|
- Iterate after confirmation
|
||||||
|
- Avoid writing specs unless explicitly asked
|
||||||
|
- Remove dead/unreachable/unused code
|
||||||
|
- Don’t write multiple versions or backups for the same logic — pick the best approach and implement it
|
||||||
|
- Prefer `with_modified_env` (from spec helpers) over stubbing `ENV` directly in specs
|
||||||
|
- Specs in parallel/reloading environments: prefer comparing `error.class.name` over constant class equality when asserting raised errors
|
||||||
|
|
||||||
|
## Codex Worktree Workflow
|
||||||
|
|
||||||
|
- Use a separate git worktree + branch per task to keep changes isolated.
|
||||||
|
- Keep Codex-specific local setup under `.codex/` and use `Procfile.worktree` for worktree process orchestration.
|
||||||
|
- The setup workflow in `.codex/environments/environment.toml` should dynamically generate per-worktree DB/port values (Rails, Vite, Redis DB index) to avoid collisions.
|
||||||
|
- Start each worktree with its own Overmind socket/title so multiple instances can run at the same time.
|
||||||
|
|
||||||
|
## Commit Messages
|
||||||
|
|
||||||
|
- Prefer Conventional Commits: `type(scope): subject` (scope optional)
|
||||||
|
- Example: `feat(auth): add user authentication`
|
||||||
|
- Don't reference Claude in commit messages
|
||||||
|
|
||||||
|
## Project-Specific
|
||||||
|
|
||||||
|
- **Translations**:
|
||||||
|
- Only update `en.yml` and `en.json`
|
||||||
|
- Other languages are handled by the community
|
||||||
|
- Backend i18n → `en.yml`, Frontend i18n → `en.json`
|
||||||
|
- **Frontend**:
|
||||||
|
- Use `components-next/` for message bubbles (the rest is being deprecated)
|
||||||
|
|
||||||
|
## Ruby Best Practices
|
||||||
|
|
||||||
|
- Use compact `module/class` definitions; avoid nested styles
|
||||||
|
|
||||||
|
## Enterprise Edition Notes
|
||||||
|
|
||||||
|
- Chatwoot has an Enterprise overlay under `enterprise/` that extends/overrides OSS code.
|
||||||
|
- When you add or modify core functionality, always check for corresponding files in `enterprise/` and keep behavior compatible.
|
||||||
|
- Follow the Enterprise development practices documented here:
|
||||||
|
- https://chatwoot.help/hc/handbook/articles/developing-enterprise-edition-features-38
|
||||||
|
|
||||||
|
Practical checklist for any change impacting core logic or public APIs
|
||||||
|
- Search for related files in both trees before editing (e.g., `rg -n "FooService|ControllerName|ModelName" app enterprise`).
|
||||||
|
- If adding new endpoints, services, or models, consider whether Enterprise needs:
|
||||||
|
- An override (e.g., `enterprise/app/...`), or
|
||||||
|
- An extension point (e.g., `prepend_mod_with`, hooks, configuration) to avoid hard forks.
|
||||||
|
- Avoid hardcoding instance- or plan-specific behavior in OSS; prefer configuration, feature flags, or extension points consumed by Enterprise.
|
||||||
|
- Keep request/response contracts stable across OSS and Enterprise; update both sets of routes/controllers when introducing new APIs.
|
||||||
|
- When renaming/moving shared code, mirror the change in `enterprise/` to prevent drift.
|
||||||
|
- Tests: Add Enterprise-specific specs under `spec/enterprise`, mirroring OSS spec layout where applicable.
|
||||||
|
- When modifying existing OSS features for Enterprise-only behavior, add an Enterprise module (via `prepend_mod_with`/`include_mod_with`) instead of editing OSS files directly—especially for policies, controllers, and services. For Enterprise-exclusive features, place code directly under `enterprise/`.
|
||||||
|
|
||||||
|
## Branding / White-labeling note
|
||||||
|
|
||||||
|
- For user-facing strings that currently contain "Chatwoot" but should adapt to branded/self-hosted installs, prefer applying `replaceInstallationName` from `shared/composables/useBranding` in the UI layer (for example tooltip and suggestion labels) instead of adding hardcoded brand-specific copy.
|
||||||
1
research/chatwoot/CLAUDE.md
Symbolic link
1
research/chatwoot/CLAUDE.md
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
AGENTS.md
|
||||||
128
research/chatwoot/CODE_OF_CONDUCT.md
Normal file
128
research/chatwoot/CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
hello@chatwoot.com.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
||||||
5
research/chatwoot/CONTRIBUTING.md
Normal file
5
research/chatwoot/CONTRIBUTING.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Contributing to Chatwoot
|
||||||
|
|
||||||
|
Thanks for taking the time to contribute! :tada::+1:
|
||||||
|
|
||||||
|
Please refer to our [Contributing Guide](https://www.chatwoot.com/docs/contributing-guide) for detailed instructions on how to contribute.
|
||||||
12
research/chatwoot/Capfile
Normal file
12
research/chatwoot/Capfile
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Load DSL and Setup Up Stages
|
||||||
|
require 'capistrano/setup'
|
||||||
|
require 'capistrano/deploy'
|
||||||
|
|
||||||
|
require 'capistrano/rails'
|
||||||
|
require 'capistrano/bundler'
|
||||||
|
require 'capistrano/rvm'
|
||||||
|
require 'capistrano/puma'
|
||||||
|
install_plugin Capistrano::Puma
|
||||||
|
|
||||||
|
# Loads custom tasks from `lib/capistrano/tasks' if you have any defined.
|
||||||
|
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }
|
||||||
273
research/chatwoot/Gemfile
Normal file
273
research/chatwoot/Gemfile
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
|
ruby '3.4.4'
|
||||||
|
|
||||||
|
##-- base gems for rails --##
|
||||||
|
gem 'rack-cors', '2.0.0', require: 'rack/cors'
|
||||||
|
gem 'rails', '~> 7.1'
|
||||||
|
# Reduces boot times through caching; required in config/boot.rb
|
||||||
|
gem 'bootsnap', require: false
|
||||||
|
|
||||||
|
##-- rails application helper gems --##
|
||||||
|
gem 'acts-as-taggable-on'
|
||||||
|
gem 'attr_extras'
|
||||||
|
gem 'browser'
|
||||||
|
gem 'hashie'
|
||||||
|
gem 'jbuilder'
|
||||||
|
gem 'kaminari'
|
||||||
|
gem 'responders', '>= 3.1.1'
|
||||||
|
gem 'rest-client'
|
||||||
|
gem 'telephone_number'
|
||||||
|
gem 'time_diff'
|
||||||
|
gem 'tzinfo-data'
|
||||||
|
gem 'valid_email2'
|
||||||
|
gem 'email-provider-info'
|
||||||
|
# compress javascript config.assets.js_compressor
|
||||||
|
gem 'uglifier'
|
||||||
|
##-- used for single column multiple binary flags in notification settings/feature flagging --##
|
||||||
|
gem 'flag_shih_tzu'
|
||||||
|
# Random name generator for user names
|
||||||
|
gem 'haikunator'
|
||||||
|
# Template parsing safely
|
||||||
|
gem 'liquid'
|
||||||
|
# Parse Markdown to HTML
|
||||||
|
gem 'commonmarker'
|
||||||
|
# Validate Data against JSON Schema
|
||||||
|
gem 'json_schemer'
|
||||||
|
# used in swagger build
|
||||||
|
gem 'json_refs'
|
||||||
|
# Rack middleware for blocking & throttling abusive requests
|
||||||
|
gem 'rack-attack', '>= 6.7.0'
|
||||||
|
# a utility tool for streaming, flexible and safe downloading of remote files
|
||||||
|
gem 'down'
|
||||||
|
# authentication type to fetch and send mail over oauth2.0
|
||||||
|
gem 'gmail_xoauth'
|
||||||
|
# Lock net-smtp to 0.3.4 to avoid issues with gmail_xoauth2
|
||||||
|
gem 'net-smtp', '~> 0.3.4'
|
||||||
|
# Prevent CSV injection
|
||||||
|
gem 'csv-safe'
|
||||||
|
|
||||||
|
##-- for active storage --##
|
||||||
|
gem 'aws-sdk-s3', require: false
|
||||||
|
# original gem isn't maintained actively
|
||||||
|
# we wanted updated version of faraday which is a dependency for slack-ruby-client
|
||||||
|
gem 'azure-storage-blob', git: 'https://github.com/chatwoot/azure-storage-ruby', branch: 'chatwoot', require: false
|
||||||
|
gem 'google-cloud-storage', '>= 1.48.0', require: false
|
||||||
|
gem 'image_processing'
|
||||||
|
|
||||||
|
##-- for actionmailbox --##
|
||||||
|
gem 'aws-actionmailbox-ses', '~> 0'
|
||||||
|
|
||||||
|
##-- gems for database --#
|
||||||
|
gem 'groupdate'
|
||||||
|
gem 'pg'
|
||||||
|
gem 'redis'
|
||||||
|
gem 'redis-namespace'
|
||||||
|
# super fast record imports in bulk
|
||||||
|
gem 'activerecord-import'
|
||||||
|
|
||||||
|
gem 'searchkick'
|
||||||
|
gem 'opensearch-ruby'
|
||||||
|
gem 'faraday_middleware-aws-sigv4'
|
||||||
|
|
||||||
|
##--- gems for server & infra configuration ---##
|
||||||
|
gem 'dotenv-rails', '>= 3.0.0'
|
||||||
|
gem 'foreman'
|
||||||
|
gem 'puma'
|
||||||
|
gem 'vite_rails'
|
||||||
|
# metrics on heroku
|
||||||
|
gem 'barnes'
|
||||||
|
|
||||||
|
##--- gems for authentication & authorization ---##
|
||||||
|
gem 'devise', '>= 4.9.4'
|
||||||
|
gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot'
|
||||||
|
gem 'devise_token_auth', '>= 1.2.3'
|
||||||
|
# two-factor authentication
|
||||||
|
gem 'devise-two-factor', '>= 5.0.0'
|
||||||
|
# authorization
|
||||||
|
gem 'jwt'
|
||||||
|
gem 'pundit'
|
||||||
|
|
||||||
|
# super admin
|
||||||
|
gem 'administrate', '>= 0.20.1'
|
||||||
|
gem 'administrate-field-active_storage', '>= 1.0.3'
|
||||||
|
gem 'administrate-field-belongs_to_search', '>= 0.9.0'
|
||||||
|
|
||||||
|
##--- gems for pubsub service ---##
|
||||||
|
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
|
||||||
|
gem 'wisper', '2.0.0'
|
||||||
|
|
||||||
|
##--- gems for channels ---##
|
||||||
|
gem 'facebook-messenger'
|
||||||
|
gem 'line-bot-api'
|
||||||
|
gem 'twilio-ruby'
|
||||||
|
# twitty will handle subscription of twitter account events
|
||||||
|
# gem 'twitty', git: 'https://github.com/chatwoot/twitty'
|
||||||
|
gem 'twitty', '~> 0.1.5'
|
||||||
|
# facebook client
|
||||||
|
gem 'koala'
|
||||||
|
# slack client
|
||||||
|
gem 'slack-ruby-client', '~> 2.7.0'
|
||||||
|
# for dialogflow integrations
|
||||||
|
gem 'google-cloud-dialogflow-v2', '>= 0.24.0'
|
||||||
|
gem 'grpc'
|
||||||
|
# Translate integrations
|
||||||
|
# 'google-cloud-translate' gem depends on faraday 2.0 version
|
||||||
|
# this dependency breaks the slack-ruby-client gem
|
||||||
|
gem 'google-cloud-translate-v3', '>= 0.7.0'
|
||||||
|
|
||||||
|
##-- apm and error monitoring ---#
|
||||||
|
# loaded only when environment variables are set.
|
||||||
|
# ref application.rb
|
||||||
|
gem 'datadog', '~> 2.0', require: false
|
||||||
|
gem 'elastic-apm', require: false
|
||||||
|
gem 'newrelic_rpm', require: false
|
||||||
|
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
|
||||||
|
gem 'scout_apm', require: false
|
||||||
|
gem 'sentry-rails', '>= 5.19.0', require: false
|
||||||
|
gem 'sentry-ruby', require: false
|
||||||
|
gem 'sentry-sidekiq', '>= 5.19.0', require: false
|
||||||
|
|
||||||
|
##-- background job processing --##
|
||||||
|
gem 'sidekiq', '>= 7.3.1'
|
||||||
|
# We want cron jobs
|
||||||
|
gem 'sidekiq-cron', '>= 1.12.0'
|
||||||
|
# for sidekiq healthcheck
|
||||||
|
gem 'sidekiq_alive'
|
||||||
|
|
||||||
|
##-- Push notification service --##
|
||||||
|
gem 'fcm'
|
||||||
|
gem 'web-push', '>= 3.0.1'
|
||||||
|
|
||||||
|
##-- geocoding / parse location from ip --##
|
||||||
|
# http://www.rubygeocoder.com/
|
||||||
|
gem 'geocoder'
|
||||||
|
# to parse maxmind db
|
||||||
|
gem 'maxminddb'
|
||||||
|
|
||||||
|
# to create db triggers
|
||||||
|
gem 'hairtrigger'
|
||||||
|
|
||||||
|
gem 'procore-sift'
|
||||||
|
|
||||||
|
# parse email
|
||||||
|
gem 'email_reply_trimmer'
|
||||||
|
|
||||||
|
gem 'html2text'
|
||||||
|
|
||||||
|
# to calculate working hours
|
||||||
|
gem 'working_hours'
|
||||||
|
|
||||||
|
# full text search for articles
|
||||||
|
gem 'pg_search'
|
||||||
|
|
||||||
|
# Subscriptions, Billing
|
||||||
|
gem 'stripe', '~> 18.0'
|
||||||
|
|
||||||
|
## - helper gems --##
|
||||||
|
## to populate db with sample data
|
||||||
|
gem 'faker'
|
||||||
|
|
||||||
|
# Include logrange conditionally in intializer using env variable
|
||||||
|
gem 'lograge', '~> 0.14.0', require: false
|
||||||
|
|
||||||
|
# worked with microsoft refresh token
|
||||||
|
gem 'omniauth-oauth2'
|
||||||
|
|
||||||
|
gem 'audited', '~> 5.4', '>= 5.4.1'
|
||||||
|
|
||||||
|
# need for google auth
|
||||||
|
gem 'omniauth', '>= 2.1.2'
|
||||||
|
gem 'omniauth-saml'
|
||||||
|
gem 'omniauth-google-oauth2', '>= 1.1.3'
|
||||||
|
gem 'omniauth-rails_csrf_protection', '~> 1.0', '>= 1.0.2'
|
||||||
|
|
||||||
|
## Gems for reponse bot
|
||||||
|
# adds cosine similarity to postgres using vector extension
|
||||||
|
gem 'neighbor'
|
||||||
|
gem 'pgvector'
|
||||||
|
# Convert Website HTML to Markdown
|
||||||
|
gem 'reverse_markdown'
|
||||||
|
|
||||||
|
gem 'iso-639'
|
||||||
|
gem 'ruby-openai'
|
||||||
|
gem 'ai-agents'
|
||||||
|
|
||||||
|
# TODO: Move this gem as a dependency of ai-agents
|
||||||
|
gem 'ruby_llm', '>= 1.8.2'
|
||||||
|
gem 'ruby_llm-schema'
|
||||||
|
|
||||||
|
gem 'cld3', '~> 3.7'
|
||||||
|
|
||||||
|
# OpenTelemetry for LLM observability
|
||||||
|
gem 'opentelemetry-sdk'
|
||||||
|
gem 'opentelemetry-exporter-otlp'
|
||||||
|
|
||||||
|
gem 'shopify_api'
|
||||||
|
|
||||||
|
### Gems required only in specific deployment environments ###
|
||||||
|
##############################################################
|
||||||
|
|
||||||
|
group :production do
|
||||||
|
# we dont want request timing out in development while using byebug
|
||||||
|
gem 'rack-timeout'
|
||||||
|
# for heroku autoscaling
|
||||||
|
gem 'judoscale-rails', require: false
|
||||||
|
gem 'judoscale-sidekiq', require: false
|
||||||
|
end
|
||||||
|
|
||||||
|
group :development do
|
||||||
|
gem 'annotaterb'
|
||||||
|
gem 'bullet'
|
||||||
|
gem 'letter_opener'
|
||||||
|
gem 'scss_lint', require: false
|
||||||
|
gem 'web-console', '>= 4.2.1'
|
||||||
|
|
||||||
|
# When we want to squash migrations
|
||||||
|
gem 'squasher'
|
||||||
|
|
||||||
|
# profiling
|
||||||
|
gem 'rack-mini-profiler', '>= 3.2.0', require: false
|
||||||
|
gem 'stackprof'
|
||||||
|
# Should install the associated chrome extension to view query logs
|
||||||
|
gem 'meta_request', '>= 0.8.3'
|
||||||
|
|
||||||
|
gem 'tidewave'
|
||||||
|
end
|
||||||
|
|
||||||
|
group :test do
|
||||||
|
# fast cleaning of database
|
||||||
|
gem 'database_cleaner'
|
||||||
|
# mock http calls
|
||||||
|
gem 'webmock'
|
||||||
|
# test profiling
|
||||||
|
gem 'test-prof'
|
||||||
|
gem 'simplecov_json_formatter', require: false
|
||||||
|
end
|
||||||
|
|
||||||
|
group :development, :test do
|
||||||
|
gem 'active_record_query_trace'
|
||||||
|
##--- gems for debugging and error reporting ---##
|
||||||
|
# static analysis
|
||||||
|
gem 'brakeman'
|
||||||
|
gem 'bundle-audit', require: false
|
||||||
|
gem 'byebug', platform: :mri
|
||||||
|
gem 'climate_control'
|
||||||
|
gem 'debug', '~> 1.8'
|
||||||
|
gem 'factory_bot_rails', '>= 6.4.3'
|
||||||
|
gem 'listen'
|
||||||
|
gem 'mock_redis'
|
||||||
|
gem 'pry-rails'
|
||||||
|
gem 'rspec_junit_formatter'
|
||||||
|
gem 'rspec-rails', '>= 6.1.5'
|
||||||
|
gem 'rubocop', require: false
|
||||||
|
gem 'rubocop-performance', require: false
|
||||||
|
gem 'rubocop-rails', require: false
|
||||||
|
gem 'rubocop-rspec', require: false
|
||||||
|
gem 'rubocop-factory_bot', require: false
|
||||||
|
gem 'seed_dump'
|
||||||
|
gem 'shoulda-matchers'
|
||||||
|
gem 'simplecov', '>= 0.21', require: false
|
||||||
|
gem 'spring'
|
||||||
|
gem 'spring-watcher-listen'
|
||||||
|
end
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user