prism/service/integration/signal/client.go
2026-02-26 18:35:11 -08:00

151 lines
3.3 KiB
Go

package signal
import (
"bytes"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
const (
DefaultDeviceName = "Prism"
DefaultConfigPath = ".local/share/signal-cli"
)
type Client struct {
ConfigPath string
}
func NewClient() *Client {
if _, err := exec.LookPath("signal-cli"); err != nil {
return nil
}
return &Client{
ConfigPath: filepath.Join(os.Getenv("HOME"), DefaultConfigPath),
}
}
type Account struct {
Number string
UUID string
}
func (c *Client) exec(args ...string) ([]byte, error) {
baseArgs := []string{"--config", c.ConfigPath, "--output=json"}
cmd := exec.Command("signal-cli", append(baseArgs, args...)...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
if stderr.Len() > 0 {
return nil, fmt.Errorf("signal-cli: %s", strings.TrimSpace(stderr.String()))
}
return nil, err
}
return stdout.Bytes(), nil
}
func (c *Client) GetLinkedAccount() (*Account, error) {
if c == nil {
return nil, nil
}
accountsFile := filepath.Join(c.ConfigPath, "data", "accounts.json")
if _, err := os.Stat(accountsFile); os.IsNotExist(err) {
return nil, nil
}
data, err := os.ReadFile(accountsFile)
if err != nil {
return nil, err
}
var accountsData struct {
Accounts []struct {
Number string `json:"number"`
UUID string `json:"uuid"`
} `json:"accounts"`
}
if err := json.Unmarshal(data, &accountsData); err != nil {
return nil, err
}
if len(accountsData.Accounts) == 0 {
return nil, nil
}
account := &Account{
Number: accountsData.Accounts[0].Number,
UUID: accountsData.Accounts[0].UUID,
}
cmd := exec.Command("signal-cli", "-a", account.Number, "receive", "--timeout", "0")
output, err := cmd.CombinedOutput()
if err != nil {
errStr := strings.ToLower(string(output))
if strings.Contains(errStr, "not registered") || strings.Contains(errStr, "authorization failed") {
return nil, nil
}
}
return account, nil
}
func (c *Client) CreateGroup(name string) (string, string, error) {
if c == nil {
return "", "", fmt.Errorf("signal client not initialized")
}
account, err := c.GetLinkedAccount()
if err != nil {
return "", "", fmt.Errorf("failed to get linked account: %w", err)
}
if account == nil {
return "", "", fmt.Errorf("no linked Signal account")
}
output, err := c.exec("-o", "json", "-a", account.Number, "updateGroup", "-n", name)
if err != nil {
return "", "", fmt.Errorf("failed to create group: %w", err)
}
var response struct {
GroupID string `json:"groupId"`
}
lines := bytes.Split(output, []byte("\n"))
for _, line := range lines {
if len(line) == 0 {
continue
}
if err := json.Unmarshal(line, &response); err == nil && response.GroupID != "" {
return response.GroupID, account.Number, nil
}
}
return "", "", fmt.Errorf("failed to parse group creation response")
}
func (c *Client) SendGroupMessage(groupID, message string) error {
if c == nil {
return fmt.Errorf("signal client not initialized")
}
account, err := c.GetLinkedAccount()
if err != nil {
return fmt.Errorf("failed to get linked account: %w", err)
}
if account == nil {
return fmt.Errorf("no linked Signal account")
}
_, err = c.exec("-a", account.Number, "send", "-g", groupID, "--notify-self", "-m", message)
return err
}