Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 40 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ FROM golang:1.25-bookworm as builder
RUN apt-get update && apt-get install -y \
build-essential \
protobuf-compiler \
libvips-dev
libvips-dev \
gcc \
cmake \
git \
&& rm -rf /var/lib/apt/lists/*

# Set the working directory
WORKDIR /app
Expand All @@ -17,16 +21,48 @@ RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest && \
# Copy the application source code
COPY . .

# Build the binary
# Build libopus_mlow.so when not vendored in native/ (Linux Docker builds).
RUN mkdir -p native && \
if [ ! -f native/libopus_mlow.so ]; then \
git clone --depth 1 https://github.com/edgardmessias/opus_mlow.git /tmp/opus_mlow && \
cmake -S /tmp/opus_mlow -B /tmp/opus_build \
-DOPUS_BUILD_SHARED_LIBRARY=ON \
-DOPUS_BUILD_TESTING=OFF \
-DOPUS_BUILD_PROGRAMS=OFF \
-DCMAKE_BUILD_TYPE=Release && \
cmake --build /tmp/opus_build -j"$(nproc)" && \
cp /tmp/opus_build/libopus.so native/libopus_mlow.so && \
ln -sf libopus_mlow.so native/libopus.so.0 && \
ln -sf libopus_mlow.so native/libopus-0.so; \
fi

# Build the binary with MLow codec when Linux libs are available
ENV CGO_ENABLED=1
ENV LD_LIBRARY_PATH=/app/native:${LD_LIBRARY_PATH}
RUN export GOPATH=$HOME/go && \
export PATH=$PATH:$GOPATH/bin && \
export PATH=$PATH:/usr/local/go/bin && \
make all
make build-proto && \
cd src && \
if [ -f ../native/libopus_mlow.so ]; then \
go build -tags mlow -o ../bin/gows .; \
else \
echo "WARN: native/libopus_mlow.so missing — building signaling-only (no live audio)" && \
go build -o ../bin/gows .; \
fi

# Create a new minimal container to hold the binary
FROM debian:bookworm-slim

WORKDIR /release

# Copy the compiled binary from the builder stage
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
libstdc++6 \
&& rm -rf /var/lib/apt/lists/*

# Copy the compiled binary and native MLow libraries from the builder stage
COPY --from=builder /app/bin/gows /release/gows
COPY --from=builder /app/native/ /release/native/

ENV LD_LIBRARY_PATH=/release/native
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,11 @@ tidy: build-proto
build:
cd src && \
go build -o ../bin/gows .

build-mlow:
cd src && \
CGO_ENABLED=1 go build -tags mlow -o ../bin/gows .

build-mlow-docker:
cd src && \
CGO_ENABLED=1 GOOS=linux go build -tags mlow -o ../bin/gows .
70 changes: 70 additions & 0 deletions docs/calls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Chamadas VoIP nativas (GOWS)

## Visão geral

O gows-plus integra o stack VoIP do WaCalls (`src/voip/`) no mesmo processo e `whatsmeow.Client` por sessão WAHA.

- **Signaling**: stanzas `<call>` via `DangerousInternalClient`
- **Mídia**: MLow 16 kHz + SRTP + relay SCTP (Meta)
- **Browser**: bridge WebRTC Opus 48 kHz (`src/callbridge/`)

## Build

### Sem áudio (signaling only)

```bash
cd src && go build -o ../bin/gows .
```

### Com áudio (MLow via CGO)

Requer compilador C e bibliotecas em `native/`:

```bash
# Windows (MSYS2)
$env:PATH = "C:\msys64\mingw64\bin;$PWD\native;$env:PATH"
$env:CGO_ENABLED = "1"
go build -tags mlow -o bin/gows ./src

# Linux / Docker
CGO_ENABLED=1 go build -tags mlow -o bin/gows ./src
```

### Docker

A imagem usa `-tags mlow` quando `native/libopus_mlow.so` está presente. Para Linux, obtenha o `.so` a partir do projeto [opus_mlow](https://github.com/edgardmessias/opus_mlow) e coloque em `native/` junto com `libopus-0.so`.

```bash
docker build -t gows-plus .
```

## Requisitos de rede (produção)

| Requisito | Detalhe |
|-----------|---------|
| UDP saída | Relays STUN/SCTP da Meta (porta ~3480) |
| NAT | Container/VM deve conseguir binding UDP |
| Linked device | Sessão pareada como dispositivo vinculado |

Sem conectividade UDP aos relays, a chamada pode chegar a `ringing` mas não a `active`.

## Limitações

- Uma chamada ativa por sessão
- 1:1 voz (vídeo: signaling apenas, sem pipeline VP8)
- Cliente WebRTC necessário para áudio (bots headless precisam de integração futura)

## Teste E2E

1. Suba WAHA com engine GOWS e este binário
2. Implemente REST conforme [`integrations/waha/README.md`](../integrations/waha/README.md)
3. Abra [`tools/call-test-client/index.html`](../tools/call-test-client/index.html) no browser
4. Inicie chamada para um número WhatsApp real

## Variáveis de ambiente

| Variável | Efeito |
|----------|--------|
| `WAHA_GOWS_DEVICE_HISTORY_SYNC_SUPPORT_CALL_LOG_HISTORY` | Registra capability de call log no device |

`PatchDeviceProps()` é chamado no startup de `main.go`.
80 changes: 80 additions & 0 deletions integrations/waha/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Integração WAHA — Chamadas VoIP (GOWS)

Este diretório descreve como o [WAHA](https://github.com/devlikeapro/waha) deve consumir os novos RPCs do gows-plus.

## Novos RPCs gRPC (`MessageService`)

| RPC | Request | Response |
|-----|---------|----------|
| `StartCall` | `session`, `jid` (telefone ou JID), `video` | `call_id` |
| `AcceptCall` | `session`, `call_id`, `owner_id?` | `Empty` |
| `RejectCall` | `session`, `from`, `id` | `Empty` |
| `EndCall` | `session`, `call_id` | `Empty` |
| `ExchangeCallWebRTC` | `session`, `call_id`, `sdp_offer` | `sdp_answer` |
| `GetCallState` | `session` | `active`, `call_id`, `from`, `direction`, `status`, `event` |

## REST WAHA (a implementar no NestJS)

Espelhar as rotas do WaCalls:

```
POST /api/{session}/calls → StartCall
POST /api/{session}/calls/{id}/accept → AcceptCall (+ header X-Client-Id → owner_id)
POST /api/{session}/calls/reject → RejectCall (já existe)
DELETE /api/{session}/calls/{id} → EndCall
POST /api/{session}/calls/{id}/webrtc → ExchangeCallWebRTC
GET /api/{session}/calls/state → GetCallState (opcional)
```

### Exemplo `calls.controller.ts`

```typescript
@Post(':session/calls')
start(@Param('session') session: string, @Body() body: { phone?: string; jid?: string; video?: boolean }) {
const jid = body.jid ?? `${normalizePhone(body.phone)}@c.us`;
return this.gows.startCall(session, { jid, video: body.video ?? false });
}

@Post(':session/calls/:id/webrtc')
webrtc(@Param('session') session: string, @Param('id') id: string, @Body() body: { sdp_offer: string }) {
return this.gows.exchangeCallWebRTC(session, { callId: id, sdpOffer: body.sdp_offer });
}
```

## Webhooks / eventos

O `EventStream` gRPC emite `CallLifecycleEvent` com o campo `event` já no formato WAHA:

| `event` (stream) | Quando |
|------------------|--------|
| `call.received` | Chamada entrante (`OnIncoming`) |
| `call.ringing` | Estado ringing |
| `call.connecting` | Aceite / negociação |
| `call.active` | Mídia ativa |
| `call.ended` | Encerramento |

Payload JSON:

```json
{
"event": "call.active",
"id": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"from": "5511999999999@s.whatsapp.net",
"direction": "inbound",
"status": "active",
"reason": "",
"timestamp": 1710000000000
}
```

No consumidor NestJS, mapear `EventJson.event === "call.active"` (ou tipo `gows.CallLifecycleEvent`) para webhook HTTP `call.active`.

## Fluxo do cliente de áudio

1. `POST /calls` ou receber webhook `call.received`
2. Browser/app cria `RTCPeerConnection`, gera SDP offer
3. `POST /calls/{id}/webrtc` com `{ "sdp_offer": "..." }`
4. Aplicar `sdp_answer` no peer connection
5. Áudio bidirecional via Opus 48 kHz

Ver [`tools/call-test-client/`](../tools/call-test-client/) para teste manual.
40 changes: 40 additions & 0 deletions integrations/waha/gows-calls.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Referência de cliente gRPC para os RPCs de chamada do gows-plus.
* Integrar no GowsGrpcClient existente do WAHA (NestJS).
*/
export interface StartCallRequest {
session: { id: string };
jid: string;
video?: boolean;
}

export interface AcceptCallRequest {
session: { id: string };
call_id: string;
owner_id?: string;
}

export interface ExchangeCallWebRTCRequest {
session: { id: string };
call_id: string;
sdp_offer: string;
}

export interface CallLifecyclePayload {
event: 'call.received' | 'call.ringing' | 'call.connecting' | 'call.active' | 'call.ended' | 'call.rejected';
id: string;
from: string;
direction: 'inbound' | 'outbound';
status: string;
reason?: string;
timestamp: number;
}

export const GOWS_CALL_RPCS = {
startCall: 'StartCall',
acceptCall: 'AcceptCall',
rejectCall: 'RejectCall',
endCall: 'EndCall',
exchangeCallWebRTC: 'ExchangeCallWebRTC',
getCallState: 'GetCallState',
} as const;
27 changes: 27 additions & 0 deletions native/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Bibliotecas nativas MLow (opus_mlow)

## Windows (desenvolvimento)

Incluídas no repositório:

- `opus_mlow.dll`
- `libopus-0.dll`

Build: `make build-mlow` ou `go build -tags mlow` com `CGO_ENABLED=1` e MinGW no PATH.

## Linux (Docker / produção)

O repositório **não inclui** `.so` por padrão (binários são específicos de plataforma).

Para build Docker com áudio:

1. Compile ou baixe `libopus_mlow.so` e `libopus-0.so` para linux/amd64 (ou arm64)
2. Coloque em `native/`:
- `native/libopus_mlow.so`
- `native/libopus.so.0` → symlink para `libopus_mlow.so`
- `native/libopus-0.so` → symlink para `libopus_mlow.so`
3. `docker build -t gows-plus .`

Sem os `.so`, use build sem tag `mlow` — chamadas funcionam em modo **signaling-only** (sem áudio bidirecional).

Fonte: [github.com/edgardmessias/opus_mlow](https://github.com/edgardmessias/opus_mlow)
Binary file added native/libopus-0.dll
Binary file not shown.
Binary file added native/opus_mlow.dll
Binary file not shown.
45 changes: 45 additions & 0 deletions proto/gows.proto
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ service MessageService {
// Calls
//
rpc RejectCall(RejectCallRequest) returns (Empty);
rpc StartCall(StartCallRequest) returns (StartCallResponse);
rpc AcceptCall(AcceptCallRequest) returns (Empty);
rpc EndCall(EndCallRequest) returns (Empty);
rpc ExchangeCallWebRTC(ExchangeCallWebRTCRequest) returns (ExchangeCallWebRTCResponse);
rpc GetCallState(Session) returns (CallStateResponse);

//
// Storage
Expand Down Expand Up @@ -783,6 +788,46 @@ message RejectCallRequest {
string id = 3;
}

message StartCallRequest {
Session session = 1;
string jid = 2;
bool video = 3;
}

message StartCallResponse {
string call_id = 1;
}

message AcceptCallRequest {
Session session = 1;
string call_id = 2;
optional string owner_id = 3;
}

message EndCallRequest {
Session session = 1;
string call_id = 2;
}

message ExchangeCallWebRTCRequest {
Session session = 1;
string call_id = 2;
string sdp_offer = 3;
}

message ExchangeCallWebRTCResponse {
string sdp_answer = 1;
}

message CallStateResponse {
bool active = 1;
string call_id = 2;
string from = 3;
string direction = 4;
string status = 5;
string event = 6;
}

//
// Poll
//
Expand Down
28 changes: 28 additions & 0 deletions scripts/wsl-build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail

GO_ROOT="$HOME/.local/go"
export PATH="$GO_ROOT/bin:$HOME/go/bin:$PATH"

if [ ! -x "$GO_ROOT/bin/go" ]; then
mkdir -p "$HOME/.local"
curl -fsSL https://go.dev/dl/go1.26.2.linux-amd64.tar.gz -o /tmp/go.tar.gz
rm -rf "$GO_ROOT"
tar -C "$HOME/.local" -xzf /tmp/go.tar.gz
fi

if ! pkg-config --exists vips 2>/dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq build-essential libvips-dev protobuf-compiler pkg-config
fi

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
make build-proto
cd src
go mod tidy
go build -o ../bin/gows .
go test ./voip/...
6 changes: 6 additions & 0 deletions scripts/wsl-compile.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
export PATH="$HOME/.local/go/bin:$HOME/go/bin:$PATH"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT/src"
go build -o ../bin/gows .
Loading