fix: signal connector send-only via JSON-RPC (OpenClaw owns receive loop)
This commit is contained in:
parent
b69af43840
commit
3f1259f288
|
|
@ -0,0 +1,200 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SignalConfig holds signal-cli connector configuration
|
||||
type SignalConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
APIURL string `yaml:"api_url"` // e.g. http://localhost:8080
|
||||
Number string `yaml:"number"` // bot number, e.g. +31634481877
|
||||
Contacts map[string]string `yaml:"contacts"` // phone -> display name
|
||||
PollInterval int `yaml:"poll_interval"` // seconds, default 5
|
||||
}
|
||||
|
||||
// SignalConnector listens for incoming Signal messages and posts them to Fully.
|
||||
// Outgoing sync messages (Johan replying from his phone) auto-clear the alert.
|
||||
type SignalConnector struct {
|
||||
cfg SignalConfig
|
||||
alerts map[string][]string // sender number -> []Fully alertIDs
|
||||
mu sync.Mutex
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
func NewSignalConnector(cfg SignalConfig) *SignalConnector {
|
||||
if cfg.APIURL == "" {
|
||||
cfg.APIURL = "http://localhost:8080"
|
||||
}
|
||||
if cfg.PollInterval <= 0 {
|
||||
cfg.PollInterval = 5
|
||||
}
|
||||
return &SignalConnector{
|
||||
cfg: cfg,
|
||||
alerts: make(map[string][]string),
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SignalConnector) Start() {
|
||||
// Incoming Signal messages are consumed by OpenClaw's daemon — we can't poll.
|
||||
// Incoming→Fully flow is handled by James session logic when messages arrive.
|
||||
// This connector is used for outgoing sends only (via Send()).
|
||||
log.Printf("[signal] Started (send-only mode, incoming handled by OpenClaw)")
|
||||
}
|
||||
|
||||
func (c *SignalConnector) Stop() {
|
||||
close(c.stopCh)
|
||||
}
|
||||
|
||||
// --- signal-cli envelope types ---
|
||||
|
||||
type signalEnvelope struct {
|
||||
Envelope struct {
|
||||
Source string `json:"source"`
|
||||
SourceName string `json:"sourceName"`
|
||||
// Incoming message
|
||||
DataMessage *signalDataMessage `json:"dataMessage"`
|
||||
// Outgoing sync (Johan replied from his own device)
|
||||
SyncMessage *struct {
|
||||
SentMessage *struct {
|
||||
Destination string `json:"destination"`
|
||||
Message string `json:"message"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
} `json:"sentMessage"`
|
||||
} `json:"syncMessage"`
|
||||
} `json:"envelope"`
|
||||
}
|
||||
|
||||
type signalDataMessage struct {
|
||||
Message string `json:"message"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// receive fetches pending messages from signal-cli HTTP daemon
|
||||
func (c *SignalConnector) receive() []signalEnvelope {
|
||||
url := c.cfg.APIURL + "/v1/receive/" + c.cfg.Number
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
log.Printf("[signal] receive error: %v", err)
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 204 {
|
||||
return nil // no messages
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if len(body) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var envelopes []signalEnvelope
|
||||
if err := json.Unmarshal(body, &envelopes); err != nil {
|
||||
log.Printf("[signal] parse error: %v", err)
|
||||
return nil
|
||||
}
|
||||
return envelopes
|
||||
}
|
||||
|
||||
func (c *SignalConnector) processMessages() {
|
||||
envelopes := c.receive()
|
||||
for _, env := range envelopes {
|
||||
e := env.Envelope
|
||||
|
||||
// Outgoing sync — Johan replied from his own phone → clear Fully alert
|
||||
if e.SyncMessage != nil && e.SyncMessage.SentMessage != nil {
|
||||
dest := e.SyncMessage.SentMessage.Destination
|
||||
if dest != "" {
|
||||
c.clearAlerts(dest)
|
||||
log.Printf("[signal] Johan sent to %s — cleared alerts", c.resolveName(dest))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip our own bot number echoing back
|
||||
if e.Source == c.cfg.Number {
|
||||
continue
|
||||
}
|
||||
|
||||
// Incoming data message
|
||||
if e.DataMessage == nil {
|
||||
continue
|
||||
}
|
||||
msg := strings.TrimSpace(e.DataMessage.Message)
|
||||
if msg == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
name := e.SourceName
|
||||
if name == "" {
|
||||
name = c.resolveName(e.Source)
|
||||
}
|
||||
|
||||
log.Printf("[signal] Incoming from %s (%s): %s", name, e.Source, truncateStr(msg, 80))
|
||||
|
||||
alertText := fmt.Sprintf("📱 %s: %s", name, truncateStr(msg, 200))
|
||||
alertID := postFullyAlert(alertText, "info", "signal:"+e.Source)
|
||||
|
||||
c.mu.Lock()
|
||||
c.alerts[e.Source] = append(c.alerts[e.Source], alertID)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SignalConnector) clearAlerts(sender string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
for _, id := range c.alerts[sender] {
|
||||
if id != "" {
|
||||
removeFullyAlert(id)
|
||||
}
|
||||
}
|
||||
delete(c.alerts, sender)
|
||||
}
|
||||
|
||||
func (c *SignalConnector) resolveName(number string) string {
|
||||
if name, ok := c.cfg.Contacts[number]; ok {
|
||||
return name
|
||||
}
|
||||
return number
|
||||
}
|
||||
|
||||
// Send sends a Signal message to a recipient number via JSON-RPC
|
||||
func (c *SignalConnector) Send(recipient, message string) error {
|
||||
payload, _ := json.Marshal(map[string]interface{}{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "send",
|
||||
"params": map[string]interface{}{
|
||||
"recipient": recipient,
|
||||
"message": message,
|
||||
},
|
||||
"id": fmt.Sprintf("%d", time.Now().UnixMilli()),
|
||||
})
|
||||
url := c.cfg.APIURL + "/api/v1/rpc"
|
||||
resp, err := http.Post(url, "application/json", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return fmt.Errorf("signal send: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var rpcResp struct {
|
||||
Error *struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
if json.Unmarshal(body, &rpcResp) == nil && rpcResp.Error != nil {
|
||||
return fmt.Errorf("signal rpc error %d: %s", rpcResp.Error.Code, rpcResp.Error.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Loading…
Reference in New Issue