mirror of
https://github.com/bbernhard/signal-cli-rest-api.git
synced 2026-11-04 10:19:51 +00:00
Merge 9e3ec82811837a1c692d1657cbf675dd53627c70 into 787d204e155d0c73840a615a5e90c697bacaa6f1
This commit is contained in:
commit
6afab0d168
@ -55,3 +55,20 @@ func TestAccountIdentifierFromParam(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// An ACI has no phone number to send a code to, so it can only be registered with a recovery key and never verified.
|
||||
func TestRegisterByACIRequiresRecoveryKey(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
aci := gin.Params{{Key: "number", Value: "11111111-1111-4111-8111-111111111111"}, {Key: "token", Value: "123456"}}
|
||||
|
||||
for name, handler := range map[string]gin.HandlerFunc{"register": NewApi(nil).RegisterNumber, "verify": NewApi(nil).VerifyRegisteredNumber} {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = aci
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/", strings.NewReader("{}"))
|
||||
handler(c)
|
||||
if w.Code != 400 || !strings.Contains(w.Body.String(), "recovery_key") {
|
||||
t.Fatalf("%s: status = %d, body %s", name, w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,8 +84,11 @@ type Configuration struct {
|
||||
}
|
||||
|
||||
type RegisterNumberRequest struct {
|
||||
UseVoice bool `json:"use_voice,omitempty"`
|
||||
Captcha string `json:"captcha,omitempty"`
|
||||
UseVoice bool `json:"use_voice,omitempty"`
|
||||
Captcha string `json:"captcha,omitempty"`
|
||||
RecoveryKey string `json:"recovery_key,omitempty"` // 64-character Recovery Key shown by Signal Android. Recovers the account without SMS verification.
|
||||
Totp string `json:"totp,omitempty"` // Six-digit TOTP token, only needed if recovery asks for it.
|
||||
Reregister bool `json:"reregister,omitempty"` // Register even if the account is still registered locally.
|
||||
}
|
||||
|
||||
type UnregisterNumberRequest struct {
|
||||
@ -290,7 +293,7 @@ func (a *Api) About(c *gin.Context) {
|
||||
|
||||
// @Summary Register a phone number.
|
||||
// @Tags Devices
|
||||
// @Description Register a phone number with the signal network.
|
||||
// @Description Register a phone number with the signal network. An existing account can instead be recovered with its Recovery Key (recovery_key), in which case no verification is needed. Accounts without a phone number can only be recovered this way, using their ACI (Account Key) as identifier. If the response asks for a TOTP token, repeat the request with totp.
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 201
|
||||
@ -299,7 +302,7 @@ func (a *Api) About(c *gin.Context) {
|
||||
// @Param data body RegisterNumberRequest false "Additional Settings"
|
||||
// @Router /v1/register/{number} [post]
|
||||
func (a *Api) RegisterNumber(c *gin.Context) {
|
||||
_, number, err := accountIdentifierFromParam(c)
|
||||
typ, number, err := accountIdentifierFromParam(c)
|
||||
if err != nil {
|
||||
c.JSON(400, Error{Msg: err.Error()})
|
||||
return
|
||||
@ -326,7 +329,13 @@ func (a *Api) RegisterNumber(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err = a.signalClient.RegisterNumber(number, req.UseVoice, req.Captcha)
|
||||
// without a phone number there is nothing to send a verification code to
|
||||
if typ == utils.IdentifierACI && req.RecoveryKey == "" {
|
||||
c.JSON(400, Error{Msg: "Couldn't process request - registering by ACI requires a recovery_key"})
|
||||
return
|
||||
}
|
||||
|
||||
err = a.signalClient.RegisterNumber(number, req.UseVoice, req.Captcha, req.RecoveryKey, req.Totp, req.Reregister)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case *client.InvalidTransportError:
|
||||
@ -430,11 +439,15 @@ func (a *Api) DeleteLocalAccountData(c *gin.Context) {
|
||||
// @Param token path string true "Verification Code"
|
||||
// @Router /v1/register/{number}/verify/{token} [post]
|
||||
func (a *Api) VerifyRegisteredNumber(c *gin.Context) {
|
||||
_, number, err := accountIdentifierFromParam(c)
|
||||
typ, number, err := accountIdentifierFromParam(c)
|
||||
if err != nil {
|
||||
c.JSON(400, Error{Msg: err.Error()})
|
||||
return
|
||||
}
|
||||
if typ == utils.IdentifierACI {
|
||||
c.JSON(400, Error{Msg: "Couldn't process request - only phone numbers can be verified, accounts registered with a recovery_key need no verification"})
|
||||
return
|
||||
}
|
||||
token := c.Param("token")
|
||||
|
||||
pin := ""
|
||||
@ -724,7 +737,8 @@ func (a *Api) Receive(c *gin.Context) {
|
||||
}
|
||||
defer ws.Close()
|
||||
var stop = make(chan struct{})
|
||||
go a.handleSignalReceive(ws, number, stop)
|
||||
// receive events carry the phone number of numbered accounts, so filter by the canonical account
|
||||
go a.handleSignalReceive(ws, a.signalClient.CanonicalAccount(number), stop)
|
||||
go a.wsPing(ws, stop)
|
||||
wsPong(ws, stop)
|
||||
} else {
|
||||
|
||||
@ -56,6 +56,18 @@ func isAccountSelector(candidate string) bool {
|
||||
return utils.IsPhoneNumber(candidate)
|
||||
}
|
||||
|
||||
// redactSecretArgs returns a copy of args with secret values masked, so they don't end up in the logs.
|
||||
func redactSecretArgs(args []string) []string {
|
||||
redacted := append([]string{}, args...)
|
||||
for i := 1; i < len(redacted); i++ {
|
||||
switch redacted[i-1] {
|
||||
case "--recovery-key", "--totp", "--pin", "setPin":
|
||||
redacted[i] = "***"
|
||||
}
|
||||
}
|
||||
return redacted
|
||||
}
|
||||
|
||||
func (s *CliClient) Execute(wait bool, args []string, stdin string) (string, error) {
|
||||
containerId, err := getContainerId()
|
||||
|
||||
@ -100,7 +112,7 @@ func (s *CliClient) Execute(wait bool, args []string, stdin string) (string, err
|
||||
if stdin != "" {
|
||||
fullCmd += "echo '" + stdin + "' | "
|
||||
}
|
||||
fullCmd += signalCliBinary + " " + strings.Join(args, " ")
|
||||
fullCmd += signalCliBinary + " " + strings.Join(redactSecretArgs(args), " ")
|
||||
|
||||
log.Debug("*) su signal-api")
|
||||
log.Debug("*) ", fullCmd)
|
||||
|
||||
@ -436,7 +436,7 @@ func (s *SignalClient) GetSignalCliMode() SignalCliMode {
|
||||
}
|
||||
|
||||
func (s *SignalClient) Init(maxRetries int) error {
|
||||
s.signalCliApiConfig = utils.NewSignalCliApiConfig()
|
||||
s.signalCliApiConfig = utils.NewSignalCliApiConfig(s.signalCliConfig)
|
||||
err := s.signalCliApiConfig.Load(s.signalCliApiConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -774,16 +774,21 @@ func (s *SignalClient) About() About {
|
||||
return about
|
||||
}
|
||||
|
||||
func (s *SignalClient) RegisterNumber(number string, useVoice bool, captcha string) error {
|
||||
// RegisterNumber registers an account. With a recoveryKey an existing account is recovered instead
|
||||
// (no verification needed), which is the only way to register an account without a phone number.
|
||||
func (s *SignalClient) RegisterNumber(number string, useVoice bool, captcha string, recoveryKey string, totp string, reregister bool) error {
|
||||
var err error
|
||||
var jsonRpc2Client *JsonRpc2Client
|
||||
if s.signalCliMode == JsonRpc {
|
||||
type Request struct {
|
||||
UseVoice bool `json:"voice,omitempty"`
|
||||
Captcha string `json:"captcha,omitempty"`
|
||||
Account string `json:"account,omitempty"`
|
||||
UseVoice bool `json:"voice,omitempty"`
|
||||
Captcha string `json:"captcha,omitempty"`
|
||||
Account string `json:"account,omitempty"`
|
||||
RecoveryKey string `json:"recoveryKey,omitempty"`
|
||||
Totp string `json:"totp,omitempty"`
|
||||
Reregister bool `json:"reregister,omitempty"`
|
||||
}
|
||||
request := Request{Account: number}
|
||||
request := Request{Account: number, RecoveryKey: recoveryKey, Totp: totp, Reregister: reregister}
|
||||
|
||||
if useVoice {
|
||||
request.UseVoice = useVoice
|
||||
@ -809,6 +814,18 @@ func (s *SignalClient) RegisterNumber(number string, useVoice bool, captcha stri
|
||||
command = append(command, []string{"--captcha", captcha}...)
|
||||
}
|
||||
|
||||
if recoveryKey != "" {
|
||||
command = append(command, []string{"--recovery-key", recoveryKey}...)
|
||||
}
|
||||
|
||||
if totp != "" {
|
||||
command = append(command, []string{"--totp", totp}...)
|
||||
}
|
||||
|
||||
if reregister {
|
||||
command = append(command, "--reregister")
|
||||
}
|
||||
|
||||
_, err = s.cliClient.Execute(true, command, "")
|
||||
}
|
||||
|
||||
@ -1093,6 +1110,11 @@ func (s *SignalClient) Receive(number string, timeout int64, ignoreAttachments b
|
||||
}
|
||||
}
|
||||
|
||||
// CanonicalAccount maps the ACI of a numbered account to its phone number (see utils.CanonicalAccount).
|
||||
func (s *SignalClient) CanonicalAccount(account string) string {
|
||||
return utils.CanonicalAccount(s.signalCliConfig, account)
|
||||
}
|
||||
|
||||
func (s *SignalClient) GetReceiveChannel(number string) (chan JsonRpc2ReceivedMessage, string, error) {
|
||||
jsonRpc2Client, err := s.getJsonRpc2Client()
|
||||
if err != nil {
|
||||
|
||||
@ -15,6 +15,7 @@ import (
|
||||
"github.com/bbernhard/signal-cli-rest-api/utils"
|
||||
uuid "github.com/gofrs/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
@ -116,6 +117,16 @@ func (r *JsonRpc2Client) Dial(address string, maxRetries int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// redactSecretParams returns a copy of a JSON-RPC request with secret params masked, so they don't end up in the logs.
|
||||
func redactSecretParams(request []byte) []byte {
|
||||
for _, key := range []string{"recoveryKey", "totp", "pin"} {
|
||||
if gjson.GetBytes(request, "params."+key).Exists() {
|
||||
request, _ = sjson.SetBytes(request, "params."+key, "***")
|
||||
}
|
||||
}
|
||||
return request
|
||||
}
|
||||
|
||||
func (r *JsonRpc2Client) getRaw(command string, account *string, args interface{}) (string, error) {
|
||||
type Request struct {
|
||||
JsonRpc string `json:"jsonrpc"`
|
||||
@ -163,7 +174,7 @@ func (r *JsonRpc2Client) getRaw(command string, account *string, args interface{
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug("json-rpc command: ", string(fullCommandBytes))
|
||||
log.Debug("json-rpc command: ", string(redactSecretParams(fullCommandBytes)))
|
||||
|
||||
_, err = r.conn.Write([]byte(string(fullCommandBytes) + "\n"))
|
||||
if err != nil {
|
||||
|
||||
37
src/client/redact_test.go
Normal file
37
src/client/redact_test.go
Normal file
@ -0,0 +1,37 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRedactSecretArgs(t *testing.T) {
|
||||
args := []string{"-a", "+12025550123", "register", "--recovery-key", "KEY", "--totp", "123456"}
|
||||
want := []string{"-a", "+12025550123", "register", "--recovery-key", "***", "--totp", "***"}
|
||||
if got := redactSecretArgs(args); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
if args[4] != "KEY" {
|
||||
t.Fatal("redactSecretArgs modified the arguments that are passed to signal-cli")
|
||||
}
|
||||
if got := redactSecretArgs([]string{"-a", "+12025550123", "setPin", "1234"}); got[3] != "***" {
|
||||
t.Fatalf("setPin pin not redacted: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactSecretParams(t *testing.T) {
|
||||
request := []byte(`{"jsonrpc":"2.0","method":"register","params":{"account":"+12025550123","recoveryKey":"KEY","totp":"123456"}}`)
|
||||
original := string(request)
|
||||
|
||||
got := string(redactSecretParams(request))
|
||||
if strings.Contains(got, "KEY") || strings.Contains(got, "123456") || !strings.Contains(got, `"recoveryKey":"***"`) {
|
||||
t.Fatalf("secrets not redacted: %s", got)
|
||||
}
|
||||
if string(request) != original {
|
||||
t.Fatal("redactSecretParams modified the request that is sent to signal-cli")
|
||||
}
|
||||
if strings.Contains(got, `"pin"`) {
|
||||
t.Fatalf("absent param was added: %s", got)
|
||||
}
|
||||
}
|
||||
@ -282,6 +282,18 @@ const docTemplate = `{
|
||||
"captcha": {
|
||||
"type": "string"
|
||||
},
|
||||
"recovery_key": {
|
||||
"description": "64-character Recovery Key shown by Signal Android. Recovers the account without SMS verification.",
|
||||
"type": "string"
|
||||
},
|
||||
"reregister": {
|
||||
"description": "Register even if the account is still registered locally.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"totp": {
|
||||
"description": "Six-digit TOTP token, only needed if recovery asks for it.",
|
||||
"type": "string"
|
||||
},
|
||||
"use_voice": {
|
||||
"type": "boolean"
|
||||
}
|
||||
@ -4656,7 +4668,7 @@ const docTemplate = `{
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"description": "Register a phone number with the signal network.",
|
||||
"description": "Register a phone number with the signal network. An existing account can instead be recovered with its Recovery Key (recovery_key), in which case no verification is needed. Accounts without a phone number can only be recovered this way, using their ACI (Account Key) as identifier. If the response asks for a TOTP token, repeat the request with totp.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Account identifier (E.164 phone number or ACI UUID)",
|
||||
|
||||
@ -277,6 +277,18 @@
|
||||
"captcha": {
|
||||
"type": "string"
|
||||
},
|
||||
"recovery_key": {
|
||||
"description": "64-character Recovery Key shown by Signal Android. Recovers the account without SMS verification.",
|
||||
"type": "string"
|
||||
},
|
||||
"reregister": {
|
||||
"description": "Register even if the account is still registered locally.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"totp": {
|
||||
"description": "Six-digit TOTP token, only needed if recovery asks for it.",
|
||||
"type": "string"
|
||||
},
|
||||
"use_voice": {
|
||||
"type": "boolean"
|
||||
}
|
||||
@ -4651,7 +4663,7 @@
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"description": "Register a phone number with the signal network.",
|
||||
"description": "Register a phone number with the signal network. An existing account can instead be recovered with its Recovery Key (recovery_key), in which case no verification is needed. Accounts without a phone number can only be recovered this way, using their ACI (Account Key) as identifier. If the response asks for a TOTP token, repeat the request with totp.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Account identifier (E.164 phone number or ACI UUID)",
|
||||
|
||||
@ -17,6 +17,7 @@ require (
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.0
|
||||
github.com/swaggo/swag v1.16.4
|
||||
github.com/tidwall/gjson v1.14.4
|
||||
github.com/tidwall/sjson v1.2.5
|
||||
github.com/yuin/gopher-lua v1.1.2
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
@ -51,7 +52,6 @@ require (
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/tidwall/gjson v1.14.4 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
@ -16,21 +19,53 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
e164AccountPattern = regexp.MustCompile(`^\+?[1-9]\d{1,14}$`)
|
||||
aciAccountPattern = regexp.MustCompile(`(?i)^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
|
||||
e164AccountPattern = regexp.MustCompile(`^\+[1-9]\d{1,14}$`)
|
||||
aciAccountPattern = regexp.MustCompile(`^[0-9a-f]{32}$`)
|
||||
)
|
||||
|
||||
// ParseAccountIdentifier accepts an E.164 phone number or an ACI UUID.
|
||||
// ACI values are returned in lowercase, which is the form signal-cli stores.
|
||||
// ParseAccountIdentifier accepts an E.164 phone number (signal-cli requires the leading +) or an ACI.
|
||||
// Like signal-cli, an ACI may be given with or without dashes and whitespace (e.g. the Account Key
|
||||
// shown by Signal Android). It is returned as a lowercase UUID with dashes, the form signal-cli stores.
|
||||
func ParseAccountIdentifier(s string) (IdentifierType, string, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if e164AccountPattern.MatchString(s) {
|
||||
return IdentifierE164, s, nil
|
||||
}
|
||||
if aciAccountPattern.MatchString(s) {
|
||||
return IdentifierACI, strings.ToLower(s), nil
|
||||
compact := strings.ToLower(strings.ReplaceAll(strings.Join(strings.Fields(s), ""), "-", ""))
|
||||
if aciAccountPattern.MatchString(compact) {
|
||||
return IdentifierACI, compact[:8] + "-" + compact[8:12] + "-" + compact[12:16] + "-" + compact[16:20] + "-" + compact[20:], nil
|
||||
}
|
||||
return 0, "", errors.New("Couldn't process request - invalid account identifier (expected an E.164 phone number or an ACI UUID)")
|
||||
return 0, "", errors.New("Couldn't process request - invalid account identifier (expected an E.164 phone number with leading + or an ACI)")
|
||||
}
|
||||
|
||||
// CanonicalAccount returns the phone number of the local account whose ACI is id, otherwise id unchanged.
|
||||
// signal-cli reports numbered accounts by phone number (e.g. in receive events), so mapping the ACI
|
||||
// to that number gives every account a single key in the API's own bookkeeping.
|
||||
func CanonicalAccount(signalCliConfigDir string, id string) string {
|
||||
if typ, _, err := ParseAccountIdentifier(id); err != nil || typ != IdentifierACI {
|
||||
return id
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(signalCliConfigDir, "data", "accounts.json"))
|
||||
if err != nil {
|
||||
return id
|
||||
}
|
||||
var stored struct {
|
||||
Accounts []struct {
|
||||
Number string `json:"number"`
|
||||
Uuid string `json:"uuid"`
|
||||
} `json:"accounts"`
|
||||
}
|
||||
if json.Unmarshal(data, &stored) != nil {
|
||||
return id
|
||||
}
|
||||
|
||||
for _, account := range stored.Accounts {
|
||||
if account.Number != "" && strings.EqualFold(account.Uuid, id) {
|
||||
return account.Number
|
||||
}
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// SignalCliAccountSelector returns the global signal-cli account flag and value.
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
package utils
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseAccountIdentifierE164(t *testing.T) {
|
||||
cases := []string{"+12025550123", "+12", "12025550123"}
|
||||
cases := []string{"+12025550123", "+12"}
|
||||
for _, raw := range cases {
|
||||
typ, value, err := ParseAccountIdentifier(raw)
|
||||
if err != nil {
|
||||
@ -50,6 +54,62 @@ func TestParseAccountIdentifierACI(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// signal-cli also accepts the Account Key form: 32 hex characters, optionally grouped by whitespace.
|
||||
func TestParseAccountIdentifierACIWithoutDashes(t *testing.T) {
|
||||
want := "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"
|
||||
for _, raw := range []string{"AAAAAAAABBBB4CCC8DDDEEEEEEEEEEEE", "aaaa aaaa bbbb 4ccc 8ddd eeee eeee eeee"} {
|
||||
typ, value, err := ParseAccountIdentifier(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("%q: %v", raw, err)
|
||||
}
|
||||
if typ != IdentifierACI || value != want {
|
||||
t.Fatalf("%q: type=%v value=%q, want %q", raw, typ, value, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writeAccountsJson creates a signal-cli config dir with one numbered and one numberless account.
|
||||
func writeAccountsJson(t *testing.T) string {
|
||||
dir := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(dir, "data"), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
accounts := `{"accounts":[
|
||||
{"path":"1","environment":"LIVE","number":"+12025550123","uuid":"aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"},
|
||||
{"path":"2","environment":"LIVE","number":null,"uuid":"11111111-1111-4111-8111-111111111111"}
|
||||
],"version":2}`
|
||||
if err := os.WriteFile(filepath.Join(dir, "data", "accounts.json"), []byte(accounts), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func TestCanonicalAccount(t *testing.T) {
|
||||
dir := writeAccountsJson(t)
|
||||
cases := map[string]string{
|
||||
"aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee": "+12025550123", // ACI of a numbered account
|
||||
"11111111-1111-4111-8111-111111111111": "11111111-1111-4111-8111-111111111111", // numberless account
|
||||
"22222222-2222-4222-8222-222222222222": "22222222-2222-4222-8222-222222222222", // unknown account
|
||||
"+12025550123": "+12025550123",
|
||||
}
|
||||
for id, want := range cases {
|
||||
if got := CanonicalAccount(dir, id); got != want {
|
||||
t.Fatalf("%q: got %q, want %q", id, got, want)
|
||||
}
|
||||
}
|
||||
if got := CanonicalAccount(t.TempDir(), "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"); got != "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee" {
|
||||
t.Fatalf("missing accounts.json: got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustModeSharedByNumberAndACI(t *testing.T) {
|
||||
config := NewSignalCliApiConfig(writeAccountsJson(t))
|
||||
config.SetTrustModeForNumber("aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", AlwaysTrust)
|
||||
if trustMode, err := config.GetTrustModeForNumber("+12025550123"); err != nil || trustMode != AlwaysTrust {
|
||||
t.Fatalf("got %v %v, want AlwaysTrust", trustMode, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAccountIdentifierInvalid(t *testing.T) {
|
||||
cases := []string{
|
||||
"",
|
||||
@ -58,9 +118,9 @@ func TestParseAccountIdentifierInvalid(t *testing.T) {
|
||||
"+1",
|
||||
"+1234567890123456",
|
||||
"+12 345",
|
||||
"12025550123",
|
||||
"not-an-account",
|
||||
"11111111-1111-4111-8111-11111111111",
|
||||
"11111111111141118111111111111111",
|
||||
"GGGGGGGG-1111-4111-8111-111111111111",
|
||||
}
|
||||
for _, raw := range cases {
|
||||
|
||||
@ -85,12 +85,14 @@ type SignalCliApiConfigEntries struct {
|
||||
}
|
||||
|
||||
type SignalCliApiConfig struct {
|
||||
config SignalCliApiConfigEntries
|
||||
path string
|
||||
config SignalCliApiConfigEntries
|
||||
path string
|
||||
signalCliConfigDir string
|
||||
}
|
||||
|
||||
func NewSignalCliApiConfig() *SignalCliApiConfig {
|
||||
return &SignalCliApiConfig{}
|
||||
// NewSignalCliApiConfig creates the API config. signalCliConfigDir is used to key accounts canonically (see CanonicalAccount).
|
||||
func NewSignalCliApiConfig(signalCliConfigDir string) *SignalCliApiConfig {
|
||||
return &SignalCliApiConfig{signalCliConfigDir: signalCliConfigDir}
|
||||
}
|
||||
|
||||
func (c *SignalCliApiConfig) Load(path string) error {
|
||||
@ -110,7 +112,10 @@ func (c *SignalCliApiConfig) Load(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTrustModeForNumber and SetTrustModeForNumber key by CanonicalAccount, so the setting
|
||||
// applies whether a numbered account is addressed by its phone number or its ACI.
|
||||
func (c *SignalCliApiConfig) GetTrustModeForNumber(number string) (SignalCliTrustMode, error) {
|
||||
number = CanonicalAccount(c.signalCliConfigDir, number)
|
||||
if val, ok := c.config.Entries[number]; ok {
|
||||
return val.TrustMode, nil
|
||||
}
|
||||
@ -119,6 +124,7 @@ func (c *SignalCliApiConfig) GetTrustModeForNumber(number string) (SignalCliTrus
|
||||
}
|
||||
|
||||
func (c *SignalCliApiConfig) SetTrustModeForNumber(number string, trustMode SignalCliTrustMode) {
|
||||
number = CanonicalAccount(c.signalCliConfigDir, number)
|
||||
if c.config.Entries == nil {
|
||||
c.config.Entries = make(map[string]SignalCliApiConfigEntry)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user