add channel-specific build variants

This commit is contained in:
lpf
2026-03-10 14:18:29 +08:00
parent c7b159d2ed
commit 4a1b5f27e4
23 changed files with 608 additions and 173 deletions

View File

@@ -10,6 +10,14 @@ on:
description: 'Release tag (e.g. v1.2.3). If empty, uses current ref name.'
required: false
type: string
build_targets:
description: 'Space-separated GOOS/GOARCH targets. Leave empty to use Makefile defaults.'
required: false
type: string
channel_variants:
description: 'Space-separated package variants. Leave empty to use full none and all single-channel variants.'
required: false
type: string
permissions:
contents: write
@@ -17,6 +25,9 @@ permissions:
jobs:
build-and-package:
runs-on: ubuntu-latest
env:
BUILD_TARGETS: ${{ inputs.build_targets }}
CHANNEL_PACKAGE_VARIANTS: ${{ inputs.channel_variants }}
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -58,7 +69,15 @@ jobs:
run: |
set -euo pipefail
make clean
make package-all VERSION="${{ steps.ver.outputs.version }}"
if [ -n "${BUILD_TARGETS:-}" ] && [ -n "${CHANNEL_PACKAGE_VARIANTS:-}" ]; then
make package-all VERSION="${{ steps.ver.outputs.version }}" BUILD_TARGETS="${BUILD_TARGETS}" CHANNEL_PACKAGE_VARIANTS="${CHANNEL_PACKAGE_VARIANTS}"
elif [ -n "${BUILD_TARGETS:-}" ]; then
make package-all VERSION="${{ steps.ver.outputs.version }}" BUILD_TARGETS="${BUILD_TARGETS}"
elif [ -n "${CHANNEL_PACKAGE_VARIANTS:-}" ]; then
make package-all VERSION="${{ steps.ver.outputs.version }}" CHANNEL_PACKAGE_VARIANTS="${CHANNEL_PACKAGE_VARIANTS}"
else
make package-all VERSION="${{ steps.ver.outputs.version }}"
fi
- name: Upload artifacts
uses: actions/upload-artifact@v4

110
Makefile
View File

@@ -1,4 +1,4 @@
.PHONY: all build build-linux-slim build-all build-webui package-all install install-win uninstall clean help test test-docker install-bootstrap-docs sync-embed-workspace sync-embed-workspace-base sync-embed-webui cleanup-embed-workspace test-only clean-test-artifacts dev
.PHONY: all build build-variants build-linux-slim build-all build-all-variants build-webui package-all install install-win uninstall clean help test test-docker install-bootstrap-docs sync-embed-workspace sync-embed-workspace-base sync-embed-webui cleanup-embed-workspace test-only clean-test-artifacts dev
# Build variables
BINARY_NAME=clawgo
@@ -33,6 +33,12 @@ LINUX_SLIM_PATH=$(BUILD_DIR)/$(BINARY_NAME)-linux-$(ARCH)-slim
# Cross-platform build matrix (space-separated GOOS/GOARCH pairs)
BUILD_TARGETS?=linux/amd64 linux/arm64 linux/riscv64 darwin/amd64 darwin/arm64 windows/amd64 windows/arm64
CHANNELS?=telegram discord feishu maixcam qq dingtalk whatsapp
CHANNEL_PACKAGE_VARIANTS?=full none $(CHANNELS)
empty:=
space:=$(empty) $(empty)
comma:=,
ALL_CHANNEL_OMIT_TAGS=$(subst $(space),$(comma),$(addprefix omit_,$(CHANNELS)))
# Installation
INSTALL_PREFIX?=/usr/local
@@ -108,6 +114,38 @@ build: sync-embed-workspace
@echo "Build complete: $(BINARY_PATH)"
@ln -sf $(BINARY_NAME)-$(PLATFORM)-$(ARCH) $(BUILD_DIR)/$(BINARY_NAME)
## build-variants: Build current-platform full, no-channel, and per-channel binaries
build-variants: sync-embed-workspace
@echo "Building channel variants for $(PLATFORM)/$(ARCH): $(CHANNEL_PACKAGE_VARIANTS)"
@mkdir -p $(BUILD_DIR)
@set -e; trap '$(MAKE) cleanup-embed-workspace' EXIT; \
for variant in $(CHANNEL_PACKAGE_VARIANTS); do \
tags=""; \
suffix=""; \
if [ "$$variant" = "none" ]; then \
tags="$(ALL_CHANNEL_OMIT_TAGS)"; \
suffix="-nochannels"; \
elif [ "$$variant" != "full" ]; then \
for ch in $(CHANNELS); do \
if [ "$$ch" != "$$variant" ]; then \
tags="$${tags:+$$tags,}omit_$$ch"; \
fi; \
done; \
suffix="-$$variant"; \
fi; \
out="$(BUILD_DIR)/$(BINARY_NAME)-$(PLATFORM)-$(ARCH)$$suffix"; \
echo " -> $$variant"; \
if [ -n "$$tags" ]; then \
$(GO) build $(GOFLAGS) $(BUILD_FLAGS) -tags "$$tags" $(LDFLAGS) -o "$$out" ./$(CMD_DIR); \
else \
$(GO) build $(GOFLAGS) $(BUILD_FLAGS) $(LDFLAGS) -o "$$out" ./$(CMD_DIR); \
fi; \
if [ "$(COMPRESS_BINARY)" = "1" ] && command -v upx >/dev/null 2>&1; then \
upx $(UPX_FLAGS) "$$out" >/dev/null; \
fi; \
done
@echo "Variant builds complete: $(BUILD_DIR)"
## build-linux-slim: Build a Linux-only slim binary (no feature trimming, no channel disabling)
build-linux-slim: sync-embed-workspace
@echo "Building $(BINARY_NAME) slim profile for linux/$(ARCH)..."
@@ -142,6 +180,43 @@ build-all: sync-embed-workspace
done
@echo "All builds complete"
## build-all-variants: Build full, no-channel, and per-channel binaries for all configured platforms
build-all-variants: sync-embed-workspace
@echo "Building all channel variants for multiple platforms: $(BUILD_TARGETS)"
@mkdir -p $(BUILD_DIR)
@set -e; trap '$(MAKE) cleanup-embed-workspace' EXIT; \
for target in $(BUILD_TARGETS); do \
goos="$${target%/*}"; \
goarch="$${target#*/}"; \
for variant in $(CHANNEL_PACKAGE_VARIANTS); do \
tags=""; \
suffix=""; \
if [ "$$variant" = "none" ]; then \
tags="$(ALL_CHANNEL_OMIT_TAGS)"; \
suffix="-nochannels"; \
elif [ "$$variant" != "full" ]; then \
for ch in $(CHANNELS); do \
if [ "$$ch" != "$$variant" ]; then \
tags="$${tags:+$$tags,}omit_$$ch"; \
fi; \
done; \
suffix="-$$variant"; \
fi; \
out="$(BUILD_DIR)/$(BINARY_NAME)-$$goos-$$goarch$$suffix"; \
if [ "$$goos" = "windows" ]; then out="$$out.exe"; fi; \
echo " -> $$goos/$$goarch [$$variant]"; \
if [ -n "$$tags" ]; then \
CGO_ENABLED=0 GOOS=$$goos GOARCH=$$goarch $(GO) build $(GOFLAGS) $(BUILD_FLAGS) -tags "$$tags" $(LDFLAGS) -o "$$out" ./$(CMD_DIR); \
else \
CGO_ENABLED=0 GOOS=$$goos GOARCH=$$goarch $(GO) build $(GOFLAGS) $(BUILD_FLAGS) $(LDFLAGS) -o "$$out" ./$(CMD_DIR); \
fi; \
if [ "$(COMPRESS_BINARY)" = "1" ] && command -v upx >/dev/null 2>&1; then \
upx $(UPX_FLAGS) "$$out" >/dev/null; \
fi; \
done; \
done
@echo "All variant builds complete"
## build-webui: Install WebUI dependencies when needed and build dist assets
build-webui:
@echo "Building WebUI..."
@@ -164,22 +239,33 @@ build-webui:
fi; \
(cd "$(DEV_WEBUI_DIR)" && "$(NPM)" run build)
## package-all: Create compressed archives and checksums for all build targets
package-all: build-all
## package-all: Create compressed archives and checksums for full, no-channel, and per-channel build variants
package-all: build-all-variants
@echo "Packaging build artifacts..."
@set -e; cd $(BUILD_DIR); \
for target in $(BUILD_TARGETS); do \
goos="$${target%/*}"; \
goarch="$${target#*/}"; \
bin="$(BINARY_NAME)-$$goos-$$goarch"; \
if [ "$$goos" = "windows" ]; then \
bin="$$bin.exe"; \
archive="$(BINARY_NAME)-$$goos-$$goarch.zip"; \
zip -q -j "$$archive" "$$bin"; \
else \
archive="$(BINARY_NAME)-$$goos-$$goarch.tar.gz"; \
tar -czf "$$archive" "$$bin"; \
fi; \
for variant in $(CHANNEL_PACKAGE_VARIANTS); do \
suffix=""; \
archive_suffix=""; \
if [ "$$variant" = "none" ]; then \
suffix="-nochannels"; \
archive_suffix="-nochannels"; \
elif [ "$$variant" != "full" ]; then \
suffix="-$$variant"; \
archive_suffix="-$$variant"; \
fi; \
bin="$(BINARY_NAME)-$$goos-$$goarch$$suffix"; \
if [ "$$goos" = "windows" ]; then \
bin="$$bin.exe"; \
archive="$(BINARY_NAME)-$$goos-$$goarch$$archive_suffix.zip"; \
zip -q -j "$$archive" "$$bin"; \
else \
archive="$(BINARY_NAME)-$$goos-$$goarch$$archive_suffix.tar.gz"; \
tar -czf "$$archive" "$$bin"; \
fi; \
done; \
done
@set -e; cd $(BUILD_DIR); \
if command -v sha256sum >/dev/null 2>&1; then \

View File

@@ -5,6 +5,9 @@ OWNER="YspCoder"
REPO="clawgo"
BIN="clawgo"
INSTALL_DIR="/usr/local/bin"
VARIANT="${CLAWGO_CHANNEL_VARIANT:-full}"
VARIANT_EXPLICIT=0
CHANNEL_VARIANTS=(full none telegram discord feishu maixcam qq dingtalk whatsapp)
CONFIG_DIR="$HOME/.clawgo"
CONFIG_PATH="$CONFIG_DIR/config.json"
WORKSPACE_DIR="$HOME/.clawgo/workspace"
@@ -12,12 +15,13 @@ LEGACY_WORKSPACE_DIR="$HOME/.openclaw/workspace"
usage() {
cat <<EOF
Usage: $0
Usage: $0 [--variant full|none|telegram|discord|feishu|maixcam|qq|dingtalk|whatsapp]
Install or upgrade ClawGo from the latest GitHub release.
Notes:
- WebUI is embedded in the binary and initialized when you run 'clawgo onboard'.
- Variant 'none' installs the no-channel build.
- OpenClaw migration is offered only when a legacy workspace is detected.
EOF
}
@@ -77,6 +81,101 @@ tty_read() {
printf -v "$__var_name" '%s' "$__reply"
}
is_valid_variant() {
local candidate="$1"
local item
for item in "${CHANNEL_VARIANTS[@]}"; do
if [[ "$item" == "$candidate" ]]; then
return 0
fi
done
return 1
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--variant)
if [[ $# -lt 2 ]]; then
warn "--variant requires a value"
exit 1
fi
VARIANT="$2"
VARIANT_EXPLICIT=1
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
warn "Unknown argument: $1"
usage
exit 1
;;
esac
done
}
choose_variant() {
if ! is_valid_variant "$VARIANT"; then
warn "Unsupported variant: $VARIANT"
exit 1
fi
if [[ "$VARIANT_EXPLICIT" == "1" ]]; then
log "Selected variant: $VARIANT"
return
fi
if [[ ! -r /dev/tty ]]; then
log "Selected variant: $VARIANT"
return
fi
log "Choose install variant:"
log " 1. full Full build with all channels"
log " 2. none No-channel build"
log " 3. telegram Telegram-only build"
log " 4. discord Discord-only build"
log " 5. feishu Feishu-only build"
log " 6. maixcam MaixCam-only build"
log " 7. qq QQ-only build"
log " 8. dingtalk DingTalk-only build"
log " 9. whatsapp WhatsApp-only build"
local choice default_choice
case "$VARIANT" in
full) default_choice="1" ;;
none) default_choice="2" ;;
telegram) default_choice="3" ;;
discord) default_choice="4" ;;
feishu) default_choice="5" ;;
maixcam) default_choice="6" ;;
qq) default_choice="7" ;;
dingtalk) default_choice="8" ;;
whatsapp) default_choice="9" ;;
*) default_choice="1" ;;
esac
tty_read choice "Enter your choice (1-9, default $default_choice): " "$default_choice"
case "$choice" in
1) VARIANT="full" ;;
2) VARIANT="none" ;;
3) VARIANT="telegram" ;;
4) VARIANT="discord" ;;
5) VARIANT="feishu" ;;
6) VARIANT="maixcam" ;;
7) VARIANT="qq" ;;
8) VARIANT="dingtalk" ;;
9) VARIANT="whatsapp" ;;
*)
warn "Invalid variant selection: $choice"
exit 1
;;
esac
log "Selected variant: $VARIANT"
}
detect_platform() {
OS="$(uname | tr '[:upper:]' '[:lower:]')"
ARCH="$(uname -m)"
@@ -110,7 +209,14 @@ fetch_latest_tag() {
}
install_binary() {
local file="${BIN}-${OS}-${ARCH}.tar.gz"
local suffix=""
if [[ "$VARIANT" == "none" ]]; then
suffix="-nochannels"
elif [[ "$VARIANT" != "full" ]]; then
suffix="-$VARIANT"
fi
local file="${BIN}-${OS}-${ARCH}${suffix}.tar.gz"
local url="https://github.com/$OWNER/$REPO/releases/download/$TAG/$file"
local out="$TMPDIR/$file"
@@ -121,10 +227,10 @@ install_binary() {
local extracted_bin=""
if [[ -f "$TMPDIR/$BIN" ]]; then
extracted_bin="$TMPDIR/$BIN"
elif [[ -f "$TMPDIR/${BIN}-${OS}-${ARCH}" ]]; then
extracted_bin="$TMPDIR/${BIN}-${OS}-${ARCH}"
elif [[ -f "$TMPDIR/${BIN}-${OS}-${ARCH}${suffix}" ]]; then
extracted_bin="$TMPDIR/${BIN}-${OS}-${ARCH}${suffix}"
else
extracted_bin="$(find "$TMPDIR" -maxdepth 3 -type f \( -name "$BIN" -o -name "${BIN}-${OS}-${ARCH}" -o -name "${BIN}-*" \) ! -name "*.tar.gz" ! -name "*.zip" | head -n1)"
extracted_bin="$(find "$TMPDIR" -maxdepth 3 -type f \( -name "$BIN" -o -name "${BIN}-${OS}-${ARCH}${suffix}" -o -name "${BIN}-*" \) ! -name "*.tar.gz" ! -name "*.zip" | head -n1)"
fi
if [[ -z "$extracted_bin" || ! -f "$extracted_bin" ]]; then
@@ -290,13 +396,11 @@ offer_onboard() {
}
main() {
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi
parse_args "$@"
detect_platform
fetch_latest_tag
choose_variant
TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' EXIT

View File

@@ -0,0 +1,44 @@
package channels
import (
"context"
"fmt"
"github.com/YspCoder/clawgo/pkg/bus"
)
func errChannelDisabled(name string) error {
return fmt.Errorf("%s channel is disabled at build time", name)
}
type disabledChannel struct {
name string
}
func (c disabledChannel) Name() string {
return c.name
}
func (c disabledChannel) Start(ctx context.Context) error {
return errChannelDisabled(c.name)
}
func (c disabledChannel) Stop(ctx context.Context) error {
return nil
}
func (c disabledChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
return errChannelDisabled(c.name)
}
func (c disabledChannel) IsRunning() bool {
return false
}
func (c disabledChannel) IsAllowed(senderID string) bool {
return false
}
func (c disabledChannel) HealthCheck(ctx context.Context) error {
return errChannelDisabled(c.name)
}

View File

@@ -1,3 +1,5 @@
//go:build !omit_dingtalk
// ClawGo - Ultra-lightweight personal AI agent
// DingTalk channel implementation using Stream Mode

View File

@@ -0,0 +1,14 @@
//go:build omit_dingtalk
package channels
import (
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
)
type DingTalkChannel struct{ disabledChannel }
func NewDingTalkChannel(cfg config.DingTalkConfig, bus *bus.MessageBus) (*DingTalkChannel, error) {
return nil, errChannelDisabled("dingtalk")
}

View File

@@ -1,3 +1,5 @@
//go:build !omit_discord
package channels
import (

View File

@@ -0,0 +1,14 @@
//go:build omit_discord
package channels
import (
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
)
type DiscordChannel struct{ disabledChannel }
func NewDiscordChannel(cfg config.DiscordConfig, bus *bus.MessageBus) (*DiscordChannel, error) {
return nil, errChannelDisabled("discord")
}

View File

@@ -1,3 +1,5 @@
//go:build !omit_feishu
package channels
import (

View File

@@ -0,0 +1,14 @@
//go:build omit_feishu
package channels
import (
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
)
type FeishuChannel struct{ disabledChannel }
func NewFeishuChannel(cfg config.FeishuConfig, bus *bus.MessageBus) (*FeishuChannel, error) {
return nil, errChannelDisabled("feishu")
}

View File

@@ -1,3 +1,5 @@
//go:build !omit_maixcam
package channels
import (

View File

@@ -0,0 +1,14 @@
//go:build omit_maixcam
package channels
import (
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
)
type MaixCamChannel struct{ disabledChannel }
func NewMaixCamChannel(cfg config.MaixCamConfig, bus *bus.MessageBus) (*MaixCamChannel, error) {
return nil, errChannelDisabled("maixcam")
}

View File

@@ -1,3 +1,5 @@
//go:build !omit_qq
package channels
import (

14
pkg/channels/qq_stub.go Normal file
View File

@@ -0,0 +1,14 @@
//go:build omit_qq
package channels
import (
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
)
type QQChannel struct{ disabledChannel }
func NewQQChannel(cfg config.QQConfig, bus *bus.MessageBus) (*QQChannel, error) {
return nil, errChannelDisabled("qq")
}

View File

@@ -1,3 +1,5 @@
//go:build !omit_telegram
package channels
import (

View File

@@ -0,0 +1,14 @@
//go:build omit_telegram
package channels
import (
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
)
type TelegramChannel struct{ disabledChannel }
func NewTelegramChannel(cfg config.TelegramConfig, bus *bus.MessageBus) (*TelegramChannel, error) {
return nil, errChannelDisabled("telegram")
}

View File

@@ -1,3 +1,5 @@
//go:build !omit_whatsapp
package channels
import (

View File

@@ -1,3 +1,5 @@
//go:build !omit_whatsapp
package channels
import (
@@ -9,7 +11,6 @@ import (
"mime"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
@@ -27,31 +28,6 @@ import (
_ "modernc.org/sqlite"
)
type WhatsAppBridgeStatus struct {
State string `json:"state"`
Connected bool `json:"connected"`
LoggedIn bool `json:"logged_in"`
BridgeAddr string `json:"bridge_addr"`
UserJID string `json:"user_jid,omitempty"`
PushName string `json:"push_name,omitempty"`
Platform string `json:"platform,omitempty"`
QRCode string `json:"qr_code,omitempty"`
QRAvailable bool `json:"qr_available"`
LastEvent string `json:"last_event,omitempty"`
LastError string `json:"last_error,omitempty"`
UpdatedAt string `json:"updated_at"`
InboundCount int `json:"inbound_count"`
OutboundCount int `json:"outbound_count"`
ReadReceiptCount int `json:"read_receipt_count"`
LastInboundAt string `json:"last_inbound_at,omitempty"`
LastOutboundAt string `json:"last_outbound_at,omitempty"`
LastReadAt string `json:"last_read_at,omitempty"`
LastInboundFrom string `json:"last_inbound_from,omitempty"`
LastOutboundTo string `json:"last_outbound_to,omitempty"`
LastInboundText string `json:"last_inbound_text,omitempty"`
LastOutboundText string `json:"last_outbound_text,omitempty"`
}
type WhatsAppBridgeService struct {
addr string
stateDir string
@@ -818,36 +794,10 @@ func (s *WhatsAppBridgeService) closeWSClients() {
}
}
func ParseWhatsAppBridgeListenAddr(raw string) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", fmt.Errorf("bridge url is required")
}
if strings.Contains(raw, "://") {
u, err := url.Parse(raw)
if err != nil {
return "", fmt.Errorf("parse bridge url: %w", err)
}
if strings.TrimSpace(u.Host) == "" {
return "", fmt.Errorf("bridge url host is required")
}
return u.Host, nil
}
return raw, nil
}
func (s *WhatsAppBridgeService) ServeLogout(w http.ResponseWriter, r *http.Request) {
s.wrapHandler(s.handleLogout)(w, r)
}
func BridgeStatusURL(raw string) (string, error) {
return bridgeEndpointURL(raw, "status")
}
func BridgeLogoutURL(raw string) (string, error) {
return bridgeEndpointURL(raw, "logout")
}
func normalizeWhatsAppRecipientJID(raw string) (types.JID, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
@@ -895,102 +845,3 @@ func extractWhatsAppMessageText(msg *waProto.Message) string {
return ""
}
}
func bridgeEndpointURL(raw, endpoint string) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", fmt.Errorf("bridge url is required")
}
if !strings.Contains(raw, "://") {
raw = "ws://" + raw
}
u, err := url.Parse(raw)
if err != nil {
return "", fmt.Errorf("parse bridge url: %w", err)
}
switch u.Scheme {
case "wss":
u.Scheme = "https"
default:
u.Scheme = "http"
}
u.Path = bridgeSiblingPath(u.Path, endpoint)
u.RawQuery = ""
u.Fragment = ""
return u.String(), nil
}
func bridgeSiblingPath(pathValue, endpoint string) string {
pathValue = strings.TrimSpace(pathValue)
if endpoint == "" {
endpoint = "status"
}
if pathValue == "" || pathValue == "/" {
return "/" + endpoint
}
trimmed := strings.TrimSuffix(pathValue, "/")
if strings.HasSuffix(trimmed, "/ws") {
return strings.TrimSuffix(trimmed, "/ws") + "/" + endpoint
}
return trimmed + "/" + endpoint
}
func normalizeBridgeBasePath(basePath string) string {
basePath = strings.TrimSpace(basePath)
if basePath == "" || basePath == "/" {
return "/"
}
if !strings.HasPrefix(basePath, "/") {
basePath = "/" + basePath
}
return strings.TrimSuffix(basePath, "/")
}
func joinBridgeRoute(basePath, endpoint string) string {
basePath = normalizeBridgeBasePath(basePath)
if basePath == "/" {
return "/" + strings.TrimPrefix(endpoint, "/")
}
return basePath + "/" + strings.TrimPrefix(endpoint, "/")
}
func isLocalRequest(r *http.Request) bool {
if r == nil {
return false
}
addrs, err := net.InterfaceAddrs()
if err != nil {
return false
}
return isLocalRemoteAddr(strings.TrimSpace(r.RemoteAddr), addrs)
}
func isLocalRemoteAddr(remoteAddr string, localAddrs []net.Addr) bool {
host, _, err := net.SplitHostPort(strings.TrimSpace(remoteAddr))
if err != nil {
host = strings.TrimSpace(remoteAddr)
}
ip := net.ParseIP(host)
if ip == nil {
return false
}
if ip.IsLoopback() {
return true
}
for _, addr := range localAddrs {
if addr == nil {
continue
}
switch v := addr.(type) {
case *net.IPNet:
if v.IP != nil && v.IP.Equal(ip) {
return true
}
case *net.IPAddr:
if v.IP != nil && v.IP.Equal(ip) {
return true
}
}
}
return false
}

View File

@@ -0,0 +1,56 @@
//go:build omit_whatsapp
package channels
import (
"context"
"net/http"
"strings"
)
type WhatsAppBridgeService struct {
addr string
stateDir string
printQR bool
status WhatsAppBridgeStatus
}
func NewWhatsAppBridgeService(addr, stateDir string, printQR bool) *WhatsAppBridgeService {
return &WhatsAppBridgeService{
addr: strings.TrimSpace(addr),
stateDir: strings.TrimSpace(stateDir),
printQR: printQR,
status: WhatsAppBridgeStatus{
State: "disabled",
BridgeAddr: strings.TrimSpace(addr),
},
}
}
func (s *WhatsAppBridgeService) Start(ctx context.Context) error {
return errChannelDisabled("whatsapp")
}
func (s *WhatsAppBridgeService) StartEmbedded(ctx context.Context) error {
return errChannelDisabled("whatsapp")
}
func (s *WhatsAppBridgeService) Stop() {}
func (s *WhatsAppBridgeService) RegisterRoutes(mux *http.ServeMux, basePath string) {}
func (s *WhatsAppBridgeService) StatusSnapshot() WhatsAppBridgeStatus {
return s.status
}
func (s *WhatsAppBridgeService) ServeWS(w http.ResponseWriter, r *http.Request) {
http.Error(w, errChannelDisabled("whatsapp").Error(), http.StatusNotImplemented)
}
func (s *WhatsAppBridgeService) ServeStatus(w http.ResponseWriter, r *http.Request) {
http.Error(w, errChannelDisabled("whatsapp").Error(), http.StatusNotImplemented)
}
func (s *WhatsAppBridgeService) ServeLogout(w http.ResponseWriter, r *http.Request) {
http.Error(w, errChannelDisabled("whatsapp").Error(), http.StatusNotImplemented)
}

View File

@@ -1,3 +1,5 @@
//go:build !omit_whatsapp
package channels
import (

View File

@@ -0,0 +1,159 @@
package channels
import (
"fmt"
"net"
"net/http"
"net/url"
"strings"
)
type WhatsAppBridgeStatus struct {
State string `json:"state"`
Connected bool `json:"connected"`
LoggedIn bool `json:"logged_in"`
BridgeAddr string `json:"bridge_addr"`
UserJID string `json:"user_jid,omitempty"`
PushName string `json:"push_name,omitempty"`
Platform string `json:"platform,omitempty"`
QRCode string `json:"qr_code,omitempty"`
QRAvailable bool `json:"qr_available"`
LastEvent string `json:"last_event,omitempty"`
LastError string `json:"last_error,omitempty"`
UpdatedAt string `json:"updated_at"`
InboundCount int `json:"inbound_count"`
OutboundCount int `json:"outbound_count"`
ReadReceiptCount int `json:"read_receipt_count"`
LastInboundAt string `json:"last_inbound_at,omitempty"`
LastOutboundAt string `json:"last_outbound_at,omitempty"`
LastReadAt string `json:"last_read_at,omitempty"`
LastInboundFrom string `json:"last_inbound_from,omitempty"`
LastOutboundTo string `json:"last_outbound_to,omitempty"`
LastInboundText string `json:"last_inbound_text,omitempty"`
LastOutboundText string `json:"last_outbound_text,omitempty"`
}
func ParseWhatsAppBridgeListenAddr(raw string) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", fmt.Errorf("bridge url is required")
}
if strings.Contains(raw, "://") {
u, err := url.Parse(raw)
if err != nil {
return "", fmt.Errorf("parse bridge url: %w", err)
}
if strings.TrimSpace(u.Host) == "" {
return "", fmt.Errorf("bridge url host is required")
}
return u.Host, nil
}
return raw, nil
}
func BridgeStatusURL(raw string) (string, error) {
return bridgeEndpointURL(raw, "status")
}
func BridgeLogoutURL(raw string) (string, error) {
return bridgeEndpointURL(raw, "logout")
}
func bridgeEndpointURL(raw, endpoint string) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", fmt.Errorf("bridge url is required")
}
if !strings.Contains(raw, "://") {
raw = "ws://" + raw
}
u, err := url.Parse(raw)
if err != nil {
return "", fmt.Errorf("parse bridge url: %w", err)
}
switch u.Scheme {
case "wss":
u.Scheme = "https"
default:
u.Scheme = "http"
}
u.Path = bridgeSiblingPath(u.Path, endpoint)
u.RawQuery = ""
u.Fragment = ""
return u.String(), nil
}
func bridgeSiblingPath(pathValue, endpoint string) string {
pathValue = strings.TrimSpace(pathValue)
if endpoint == "" {
endpoint = "status"
}
if pathValue == "" || pathValue == "/" {
return "/" + endpoint
}
trimmed := strings.TrimSuffix(pathValue, "/")
if strings.HasSuffix(trimmed, "/ws") {
return strings.TrimSuffix(trimmed, "/ws") + "/" + endpoint
}
return trimmed + "/" + endpoint
}
func normalizeBridgeBasePath(basePath string) string {
basePath = strings.TrimSpace(basePath)
if basePath == "" || basePath == "/" {
return "/"
}
if !strings.HasPrefix(basePath, "/") {
basePath = "/" + basePath
}
return strings.TrimSuffix(basePath, "/")
}
func joinBridgeRoute(basePath, endpoint string) string {
basePath = normalizeBridgeBasePath(basePath)
if basePath == "/" {
return "/" + strings.TrimPrefix(endpoint, "/")
}
return basePath + "/" + strings.TrimPrefix(endpoint, "/")
}
func isLocalRequest(r *http.Request) bool {
if r == nil {
return false
}
addrs, err := net.InterfaceAddrs()
if err != nil {
return false
}
return isLocalRemoteAddr(strings.TrimSpace(r.RemoteAddr), addrs)
}
func isLocalRemoteAddr(remoteAddr string, localAddrs []net.Addr) bool {
host, _, err := net.SplitHostPort(strings.TrimSpace(remoteAddr))
if err != nil {
host = strings.TrimSpace(remoteAddr)
}
ip := net.ParseIP(host)
if ip == nil {
return false
}
if ip.IsLoopback() {
return true
}
for _, addr := range localAddrs {
if addr == nil {
continue
}
switch v := addr.(type) {
case *net.IPNet:
if v.IP != nil && v.IP.Equal(ip) {
return true
}
case *net.IPAddr:
if v.IP != nil && v.IP.Equal(ip) {
return true
}
}
}
return false
}

View File

@@ -0,0 +1,14 @@
//go:build omit_whatsapp
package channels
import (
"github.com/YspCoder/clawgo/pkg/bus"
"github.com/YspCoder/clawgo/pkg/config"
)
type WhatsAppChannel struct{ disabledChannel }
func NewWhatsAppChannel(cfg config.WhatsAppConfig, bus *bus.MessageBus) (*WhatsAppChannel, error) {
return nil, errChannelDisabled("whatsapp")
}

View File

@@ -1,3 +1,5 @@
//go:build !omit_whatsapp
package channels
import (