webexmessagehandler

package module
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 19, 2026 License: MIT Imports: 21 Imported by: 1

README

webex-message-handler-go

Lightweight Webex Mercury WebSocket + KMS decryption for receiving bot messages — no Webex SDK required.

Go port of the TypeScript webex-message-handler.

Install

go get github.com/3rg0n/webex-message-handler/go

Requires Go 1.21+.

Quick Start

package main

import (
	"context"
	"fmt"
	"log/slog"
	"os"

	webex "github.com/3rg0n/webex-message-handler/go"
)

func main() {
	handler, err := webex.New(webex.Config{
		Token:  os.Getenv("WEBEX_BOT_TOKEN"),
		Logger: webex.NewSlogLogger(slog.Default()),
	})
	if err != nil {
		panic(err)
	}

	handler.OnMessageCreated(func(msg webex.DecryptedMessage) {
		fmt.Printf("[%s] %s\n", msg.PersonEmail, msg.Text)
	})

	handler.OnError(func(err error) {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
	})

	if err := handler.Connect(context.Background()); err != nil {
		panic(err)
	}

	select {} // block forever
}

Important: Implementing Loop Detection

This library only handles the receive side of messaging — it decrypts incoming messages from the Mercury WebSocket. It has no visibility into messages your bot sends via the REST API. This means it cannot detect message loops on its own.

If your bot replies to incoming messages, you must implement loop detection in your wrapper code. Without it, a bug or misconfiguration could cause your bot to endlessly reply to its own messages. Webex enforces a server-side rate limit (approximately 11 consecutive messages before throttling), but that still results in spam before the cutoff.

Recommended approach: Track your bot's outgoing message rate. If it exceeds a threshold (e.g., 5 messages in 3 seconds to the same room), pause sending and log a warning.

The IgnoreSelfMessages option (default: true) provides a first line of defense by filtering out messages sent by this bot's own identity. If the library cannot verify the bot's identity during Connect() (e.g., /people/me API failure), connection will fail rather than silently running without protection. Set IgnoreSelfMessages to false to opt out, but only if you have your own loop prevention in place.

Proxy Support (Enterprise)

For corporate environments behind a proxy, pass a configured HTTP client:

package main

import (
	"context"
	"fmt"
	"net/http"
	"net/url"
	"os"

	webex "github.com/3rg0n/webex-message-handler/go"
)

func main() {
	// Configure proxy
	var httpClient *http.Client
	if proxyURL := os.Getenv("HTTPS_PROXY"); proxyURL != "" {
		proxy, _ := url.Parse(proxyURL)
		httpClient = &http.Client{
			Transport: &http.Transport{
				Proxy: http.ProxyURL(proxy),
			},
		}
	}

	handler, err := webex.New(webex.Config{
		Token:      os.Getenv("WEBEX_BOT_TOKEN"),
		HTTPClient: httpClient, // Pass configured client
	})
	if err != nil {
		panic(err)
	}

	// ... set up callbacks and connect
}

API

New(cfg Config) (*WebexMessageHandler, error)

Creates a new handler. Config fields:

Field Type Default Description
Token string required Webex bot access token
Logger Logger noop Logger implementation
IgnoreSelfMessages *bool true Filter out messages sent by this bot
HTTPClient *http.Client http.DefaultClient HTTP client for proxy support
PingInterval float64 15 Ping interval (seconds)
PongTimeout float64 14 Pong timeout (seconds)
ReconnectBackoffMax float64 32 Max reconnect backoff (seconds)
MaxReconnectAttempts int 10 Max reconnect attempts
Methods
  • Connect(ctx) error — Connect to Webex
  • Disconnect(ctx) error — Graceful disconnect
  • Reconnect(ctx, newToken) error — Update token and reconnect
  • Connected() bool — Connection status
  • Status() HandlerStatus — Health check
Event Callbacks
handler.OnMessageCreated(func(msg DecryptedMessage) { ... })
handler.OnMessageDeleted(func(data DeletedMessage) { ... })
handler.OnConnected(func() { ... })
handler.OnDisconnected(func(reason string) { ... })
handler.OnReconnecting(func(attempt int) { ... })
handler.OnError(func(err error) { ... })
Error Types
  • AuthError — Token invalid/expired
  • DeviceRegistrationError — WDM operations failed (has .StatusCode)
  • MercuryConnectionError — WebSocket failed (has .CloseCode)
  • KmsError — KMS operations failed
  • DecryptionError — Message decryption failed

All implement error and extend WebexError. Use errors.As() for type checking.

Architecture

WebexMessageHandler (orchestrator)
├── DeviceManager    — WDM registration
├── MercurySocket    — WebSocket + ping/pong + reconnect (goroutines)
├── KmsClient        — ECDH handshake + key retrieval
└── MessageDecryptor — JWE decryption

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AuthError

type AuthError struct {
	WebexError
}

AuthError indicates the token is invalid, expired, or unauthorized.

func NewAuthError

func NewAuthError(message string) *AuthError

type Config

type Config struct {
	// Token is the Webex bot or user access token (required).
	Token string

	// Mode is the networking mode: "native" or "injected" (default: "native").
	Mode NetworkMode

	// Logger is an optional logger implementation (silent by default).
	Logger Logger

	// HTTPClient is an optional HTTP client for proxy support (native mode only).
	// If nil, http.DefaultClient is used.
	HTTPClient *http.Client

	// Fetch is a custom fetch function for all HTTP requests (injected mode).
	Fetch FetchFunc

	// WebSocketFactory is a custom WebSocket factory (injected mode).
	WebSocketFactory WebSocketFactory

	// PingInterval is the Mercury ping interval in seconds (default: 15).
	PingInterval float64

	// PongTimeout is the pong response timeout in seconds (default: 14).
	PongTimeout float64

	// ReconnectBackoffMax is the max reconnect backoff in seconds (default: 32).
	ReconnectBackoffMax float64

	// MaxReconnectAttempts is the max consecutive reconnection attempts (default: 10).
	MaxReconnectAttempts int

	// IgnoreSelfMessages filters out messages sent by this bot to prevent loops (default: true).
	// Set to false explicitly via IgnoreSelfMessagesPtr if you need to receive bot's own messages.
	IgnoreSelfMessages *bool
}

Config holds configuration for WebexMessageHandler.

type ConnectionStatus

type ConnectionStatus string

ConnectionStatus represents the overall connection state.

const (
	StatusConnected    ConnectionStatus = "connected"
	StatusConnecting   ConnectionStatus = "connecting"
	StatusReconnecting ConnectionStatus = "reconnecting"
	StatusDisconnected ConnectionStatus = "disconnected"
)

type DecryptedMessage

type DecryptedMessage struct {
	// ID is the unique message ID.
	ID string

	// RoomID is the conversation/space ID.
	RoomID string

	// PersonID is the sender's user ID.
	PersonID string

	// PersonEmail is the sender's email address.
	PersonEmail string

	// Text is the decrypted plain text.
	Text string

	// HTML is the decrypted HTML content (rich text messages).
	HTML string

	// Created is the ISO 8601 timestamp.
	Created string

	// RoomType is "direct", "group", or empty.
	RoomType string

	// Raw is the full decrypted activity for advanced use.
	Raw *MercuryActivity
}

DecryptedMessage is a decrypted Webex message.

type DecryptionError

type DecryptionError struct {
	WebexError
}

DecryptionError indicates message decryption failure.

func NewDecryptionError

func NewDecryptionError(message string) *DecryptionError

func NewDecryptionErrorWithCause

func NewDecryptionErrorWithCause(message string, cause error) *DecryptionError

type DeletedMessage

type DeletedMessage struct {
	MessageID string
	RoomID    string
	PersonID  string
}

DeletedMessage represents a deleted Webex message notification.

type DeviceManager

type DeviceManager struct {
	// contains filtered or unexported fields
}

DeviceManager manages WDM device registration lifecycle.

func NewDeviceManager

func NewDeviceManager(logger Logger, httpDo fetchDoFn) *DeviceManager

NewDeviceManager creates a new DeviceManager.

func (*DeviceManager) Refresh

func (dm *DeviceManager) Refresh(ctx context.Context, token string) (*DeviceRegistration, error)

Refresh refreshes an existing device registration.

func (*DeviceManager) Register

func (dm *DeviceManager) Register(ctx context.Context, token string) (*DeviceRegistration, error)

Register registers a new device with WDM.

func (*DeviceManager) Unregister

func (dm *DeviceManager) Unregister(ctx context.Context, token string) error

Unregister unregisters the device from WDM.

type DeviceRegistration

type DeviceRegistration struct {
	// WebSocketURL is the Mercury WebSocket URL.
	WebSocketURL string `json:"webSocketUrl"`

	// DeviceURL is the device URL (used as clientId for KMS).
	DeviceURL string `json:"url"`

	// UserID is the bot's user ID.
	UserID string `json:"userId"`

	// Services is the service catalog from WDM.
	Services map[string]string `json:"services"`

	// EncryptionServiceURL is extracted from Services.
	EncryptionServiceURL string `json:"-"`
}

DeviceRegistration holds the result of WDM device registration.

type DeviceRegistrationError

type DeviceRegistrationError struct {
	WebexError
	StatusCode int
}

DeviceRegistrationError indicates WDM device operations failed.

func NewDeviceRegistrationError

func NewDeviceRegistrationError(message string, statusCode int) *DeviceRegistrationError

type FetchFunc

type FetchFunc func(ctx context.Context, req FetchRequest) (*FetchResponse, error)

FetchFunc is a custom fetch function for injected mode.

type FetchRequest

type FetchRequest struct {
	URL     string
	Method  string
	Headers map[string]string
	Body    string
}

FetchRequest represents an HTTP request for injected fetch function.

type FetchResponse

type FetchResponse struct {
	Status int
	OK     bool
	Body   io.ReadCloser
}

FetchResponse represents an HTTP response from injected fetch function.

type HandlerStatus

type HandlerStatus struct {
	// Status is the overall connection state.
	Status ConnectionStatus

	// WebSocketOpen indicates whether the Mercury WebSocket is currently open.
	WebSocketOpen bool

	// KmsInitialized indicates whether the KMS encryption context has been established.
	KmsInitialized bool

	// DeviceRegistered indicates whether the device is registered with WDM.
	DeviceRegistered bool

	// ReconnectAttempt is the current auto-reconnect attempt number (0 if not reconnecting).
	ReconnectAttempt int
}

HandlerStatus is a structured health check of all connection subsystems.

type KmsClient

type KmsClient struct {
	// contains filtered or unexported fields
}

KmsClient handles KMS ECDH key exchange and encryption key retrieval.

func NewKmsClient

func NewKmsClient(cfg KmsClientConfig) *KmsClient

NewKmsClient creates a new KmsClient.

func (*KmsClient) GetKey

func (kc *KmsClient) GetKey(ctx context.Context, keyURI string) (*jose.JSONWebKey, error)

GetKey retrieves an encryption key from KMS.

func (*KmsClient) HandleKmsMessage

func (kc *KmsClient) HandleKmsMessage(data map[string]interface{})

HandleKmsMessage handles a KMS response that arrived via Mercury WebSocket.

func (*KmsClient) Initialize

func (kc *KmsClient) Initialize(ctx context.Context) error

Initialize performs the ECDH handshake with KMS.

type KmsClientConfig

type KmsClientConfig struct {
	Token                string
	DeviceURL            string
	UserID               string
	EncryptionServiceURL string
	Logger               Logger
	HTTPDo               fetchDoFn
}

KmsClientConfig holds the configuration for KmsClient.

type KmsError

type KmsError struct {
	WebexError
}

KmsError indicates KMS key exchange or key retrieval failure.

func NewKmsError

func NewKmsError(message string) *KmsError

func NewKmsErrorWithCause

func NewKmsErrorWithCause(message string, cause error) *KmsError

type Logger

type Logger interface {
	Debug(msg string, args ...any)
	Info(msg string, args ...any)
	Warn(msg string, args ...any)
	Error(msg string, args ...any)
}

Logger defines the logging interface used by the handler. Compatible with *slog.Logger via the SlogAdapter.

func NewSlogLogger

func NewSlogLogger(l *slog.Logger) Logger

NewSlogLogger creates a Logger from a *slog.Logger.

func NoopLogger

func NoopLogger() Logger

NoopLogger returns a silent logger.

type MembershipActivity

type MembershipActivity struct {
	// ID is the activity ID.
	ID string

	// ActorID is the ID of the person who performed the action.
	ActorID string

	// PersonID is the ID of the member affected.
	PersonID string

	// RoomID is the conversation/space ID.
	RoomID string

	// Action is the membership action: "add", "leave", "assignModerator", or "unassignModerator".
	Action string

	// Created is the ISO 8601 timestamp.
	Created string

	// RoomType is "direct", "group", or empty.
	RoomType string

	// Raw is the full raw activity for advanced use.
	Raw *MercuryActivity
}

MembershipActivity represents a membership event from Mercury.

type MercuryActivity

type MercuryActivity struct {
	ID               string        `json:"id"`
	Verb             string        `json:"verb"`
	Actor            MercuryActor  `json:"actor"`
	Object           MercuryObject `json:"object"`
	Target           MercuryTarget `json:"target"`
	Published        string        `json:"published"`
	EncryptionKeyURL string        `json:"encryptionKeyUrl,omitempty"`
}

MercuryActivity represents a conversation activity from Mercury.

type MercuryActor

type MercuryActor struct {
	ID           string `json:"id"`
	ObjectType   string `json:"objectType"`
	EmailAddress string `json:"emailAddress,omitempty"`
}

MercuryActor represents the actor in a Mercury activity.

type MercuryConnectionError

type MercuryConnectionError struct {
	WebexError
	CloseCode int
}

MercuryConnectionError indicates WebSocket connection failure.

func NewMercuryConnectionError

func NewMercuryConnectionError(message string, closeCode int) *MercuryConnectionError

type MercuryEnvelope

type MercuryEnvelope struct {
	ID             string                 `json:"id"`
	Data           map[string]interface{} `json:"data"`
	Timestamp      int64                  `json:"timestamp"`
	TrackingID     string                 `json:"trackingId"`
	SequenceNumber *int64                 `json:"sequenceNumber,omitempty"`
}

MercuryEnvelope is the wire format envelope from Mercury WebSocket.

type MercuryObject

type MercuryObject struct {
	ID               string `json:"id"`
	ObjectType       string `json:"objectType"`
	DisplayName      string `json:"displayName,omitempty"`
	Content          string `json:"content,omitempty"`
	EncryptionKeyURL string `json:"encryptionKeyUrl,omitempty"`
}

MercuryObject represents the object in a Mercury activity.

type MercurySocket

type MercurySocket struct {
	// contains filtered or unexported fields
}

MercurySocket manages the Mercury WebSocket connection.

func NewMercurySocket

func NewMercurySocket(cfg MercurySocketConfig) *MercurySocket

NewMercurySocket creates a new MercurySocket.

func (*MercurySocket) Connect

func (ms *MercurySocket) Connect(ctx context.Context, wsURL, token string) error

Connect connects to Mercury WebSocket.

func (*MercurySocket) Connected

func (ms *MercurySocket) Connected() bool

Connected returns whether the WebSocket is currently open.

func (*MercurySocket) CurrentReconnectAttempts

func (ms *MercurySocket) CurrentReconnectAttempts() int

CurrentReconnectAttempts returns the current reconnection attempt count.

func (*MercurySocket) Disconnect

func (ms *MercurySocket) Disconnect()

Disconnect disconnects from Mercury.

func (*MercurySocket) OnActivity

func (ms *MercurySocket) OnActivity(fn func(activity MercuryActivity))

OnActivity sets the activity event callback.

func (*MercurySocket) OnConnected

func (ms *MercurySocket) OnConnected(fn func())

OnConnected sets the connected event callback.

func (*MercurySocket) OnDisconnected

func (ms *MercurySocket) OnDisconnected(fn func(reason string))

OnDisconnected sets the disconnected event callback.

func (*MercurySocket) OnError

func (ms *MercurySocket) OnError(fn func(err error))

OnError sets the error event callback.

func (*MercurySocket) OnKmsResponse

func (ms *MercurySocket) OnKmsResponse(fn func(data map[string]interface{}))

OnKmsResponse sets the kms:response event callback.

func (*MercurySocket) OnReconnecting

func (ms *MercurySocket) OnReconnecting(fn func(attempt int))

OnReconnecting sets the reconnecting event callback.

type MercurySocketConfig

type MercurySocketConfig struct {
	Logger               Logger
	WSFactory            wsFactoryFn
	PingInterval         time.Duration
	PongTimeout          time.Duration
	ReconnectBackoffMax  time.Duration
	MaxReconnectAttempts int
}

MercurySocketConfig holds options for MercurySocket.

type MercuryTarget

type MercuryTarget struct {
	ID               string   `json:"id"`
	ObjectType       string   `json:"objectType"`
	EncryptionKeyURL string   `json:"encryptionKeyUrl,omitempty"`
	Tags             []string `json:"tags,omitempty"`
}

MercuryTarget represents the target in a Mercury activity.

type MessageDecryptor

type MessageDecryptor struct {
	// contains filtered or unexported fields
}

MessageDecryptor decrypts encrypted Webex message activities using KMS keys.

func NewMessageDecryptor

func NewMessageDecryptor(kmsClient *KmsClient, logger Logger) *MessageDecryptor

NewMessageDecryptor creates a new MessageDecryptor.

func (*MessageDecryptor) DecryptActivity

func (md *MessageDecryptor) DecryptActivity(ctx context.Context, activity MercuryActivity) (MercuryActivity, error)

DecryptActivity decrypts an encrypted Mercury activity. Returns a copy with decrypted DisplayName and Content fields. If the activity is not encrypted (no EncryptionKeyURL), returns as-is.

type NetworkMode

type NetworkMode string

NetworkMode defines the networking mode for the handler.

const (
	// NetworkModeNative uses built-in HTTP and WebSocket libraries.
	NetworkModeNative NetworkMode = "native"
	// NetworkModeInjected uses provided fetch and WebSocket factory functions.
	NetworkModeInjected NetworkMode = "injected"
)

type SlogLogger

type SlogLogger struct {
	L *slog.Logger
}

SlogLogger wraps a *slog.Logger to implement the Logger interface.

func (*SlogLogger) Debug

func (s *SlogLogger) Debug(msg string, args ...any)

func (*SlogLogger) Error

func (s *SlogLogger) Error(msg string, args ...any)

func (*SlogLogger) Info

func (s *SlogLogger) Info(msg string, args ...any)

func (*SlogLogger) Warn

func (s *SlogLogger) Warn(msg string, args ...any)

type WebSocket

type WebSocket interface {
	Send(data string) error
	Receive() (string, error)
	Close() error
	Done() <-chan struct{}
}

WebSocket represents a WebSocket connection interface.

type WebSocketFactory

type WebSocketFactory func(ctx context.Context, url string) (WebSocket, error)

WebSocketFactory creates WebSocket connections for injected mode.

type WebexError

type WebexError struct {
	Message string
	Code    string
	Cause   error
}

WebexError is the base error type for all webex-message-handler errors.

func (*WebexError) Error

func (e *WebexError) Error() string

func (*WebexError) Unwrap

func (e *WebexError) Unwrap() error

type WebexMessageHandler

type WebexMessageHandler struct {
	// contains filtered or unexported fields
}

WebexMessageHandler receives and decrypts Webex messages over Mercury WebSocket.

func New

func New(cfg Config) (*WebexMessageHandler, error)

New creates a new WebexMessageHandler.

func (*WebexMessageHandler) Connect

func (h *WebexMessageHandler) Connect(ctx context.Context) error

Connect establishes the full connection pipeline.

func (*WebexMessageHandler) Connected

func (h *WebexMessageHandler) Connected() bool

Connected returns whether the handler is fully connected.

func (*WebexMessageHandler) Disconnect

func (h *WebexMessageHandler) Disconnect(ctx context.Context) error

Disconnect tears down the connection cleanly.

func (*WebexMessageHandler) OnConnected

func (h *WebexMessageHandler) OnConnected(fn func())

OnConnected sets the callback for connection events.

func (*WebexMessageHandler) OnDisconnected

func (h *WebexMessageHandler) OnDisconnected(fn func(reason string))

OnDisconnected sets the callback for disconnection events.

func (*WebexMessageHandler) OnError

func (h *WebexMessageHandler) OnError(fn func(err error))

OnError sets the callback for error events.

func (*WebexMessageHandler) OnMembershipCreated

func (h *WebexMessageHandler) OnMembershipCreated(fn func(activity MembershipActivity))

OnMembershipCreated sets the callback for membership events.

func (*WebexMessageHandler) OnMessageCreated

func (h *WebexMessageHandler) OnMessageCreated(fn func(msg DecryptedMessage))

OnMessageCreated sets the callback for new messages.

func (*WebexMessageHandler) OnMessageDeleted

func (h *WebexMessageHandler) OnMessageDeleted(fn func(data DeletedMessage))

OnMessageDeleted sets the callback for deleted messages.

func (*WebexMessageHandler) OnReconnecting

func (h *WebexMessageHandler) OnReconnecting(fn func(attempt int))

OnReconnecting sets the callback for reconnection events.

func (*WebexMessageHandler) Reconnect

func (h *WebexMessageHandler) Reconnect(ctx context.Context, newToken string) error

Reconnect updates the access token and re-establishes the connection.

func (*WebexMessageHandler) Status

func (h *WebexMessageHandler) Status() HandlerStatus

Status returns a structured health check of all connection subsystems.

Directories

Path Synopsis
examples
basic-bot command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL