mirror of
https://github.com/bbernhard/signal-cli-rest-api.git
synced 2026-09-20 06:09:28 +00:00
Merge 090432e596fa4e3e898799ebf090701ef188d556 into 17b86412f37cfd4936fafac9a6ee4ca57e9ac475
This commit is contained in:
commit
4f9b0d0f20
@ -4,11 +4,12 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
utils "github.com/bbernhard/signal-cli-rest-api/utils"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
utils "github.com/bbernhard/signal-cli-rest-api/utils"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CliClient struct {
|
type CliClient struct {
|
||||||
@ -49,6 +50,34 @@ func stripInfoAndWarnMessages(input string) (string, string, string) {
|
|||||||
return output, infoMessages, warnMessages
|
return output, infoMessages, warnMessages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func classifySignalCliOutput(stdout string, stderr string) (string, string, string) {
|
||||||
|
stdout = strings.TrimRight(stdout, "\r\n")
|
||||||
|
output, infoMessages, warnMessages := stripInfoAndWarnMessages(stdout)
|
||||||
|
stderr = strings.TrimSpace(stderr)
|
||||||
|
if stderr != "" {
|
||||||
|
stderrOutput, stderrInfoMessages, stderrWarnMessages := stripInfoAndWarnMessages(stderr)
|
||||||
|
if stderrInfoMessages != "" {
|
||||||
|
if infoMessages != "" {
|
||||||
|
infoMessages += "\n"
|
||||||
|
}
|
||||||
|
infoMessages += stderrInfoMessages
|
||||||
|
}
|
||||||
|
if stderrWarnMessages != "" {
|
||||||
|
if warnMessages != "" {
|
||||||
|
warnMessages += "\n"
|
||||||
|
}
|
||||||
|
warnMessages += stderrWarnMessages
|
||||||
|
}
|
||||||
|
if stderrOutput != "" {
|
||||||
|
if warnMessages != "" {
|
||||||
|
warnMessages += "\n"
|
||||||
|
}
|
||||||
|
warnMessages += stderrOutput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output, infoMessages, warnMessages
|
||||||
|
}
|
||||||
|
|
||||||
func (s *CliClient) Execute(wait bool, args []string, stdin string) (string, error) {
|
func (s *CliClient) Execute(wait bool, args []string, stdin string) (string, error) {
|
||||||
containerId, err := getContainerId()
|
containerId, err := getContainerId()
|
||||||
|
|
||||||
@ -132,17 +161,15 @@ func (s *CliClient) Execute(wait bool, args []string, stdin string) (string, err
|
|||||||
return "", errors.New("process killed as timeout reached")
|
return "", errors.New("process killed as timeout reached")
|
||||||
case err := <-done:
|
case err := <-done:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
combinedOutput := stdoutBuffer.String() + stderrBuffer.String()
|
|
||||||
log.Debug("signal-cli output (stdout): ", stdoutBuffer.String())
|
log.Debug("signal-cli output (stdout): ", stdoutBuffer.String())
|
||||||
log.Debug("signal-cli output (stderr): ", stderrBuffer.String())
|
log.Debug("signal-cli output (stderr): ", stderrBuffer.String())
|
||||||
return "", errors.New(combinedOutput)
|
return "", errors.New(strings.TrimSpace(stdoutBuffer.String() + stderrBuffer.String()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
combinedOutput := stdoutBuffer.String() + stderrBuffer.String()
|
|
||||||
log.Debug("signal-cli output (stdout): ", stdoutBuffer.String())
|
log.Debug("signal-cli output (stdout): ", stdoutBuffer.String())
|
||||||
log.Debug("signal-cli output (stderr): ", stderrBuffer.String())
|
log.Debug("signal-cli output (stderr): ", stderrBuffer.String())
|
||||||
strippedOutput, infoMessages, warnMessages := stripInfoAndWarnMessages(combinedOutput)
|
strippedOutput, infoMessages, warnMessages := classifySignalCliOutput(stdoutBuffer.String(), stderrBuffer.String())
|
||||||
for _, line := range strings.Split(infoMessages, "\n") {
|
for _, line := range strings.Split(infoMessages, "\n") {
|
||||||
if line != "" {
|
if line != "" {
|
||||||
log.Info(line)
|
log.Info(line)
|
||||||
|
|||||||
26
src/client/cli_test.go
Normal file
26
src/client/cli_test.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClassifySignalCliOutputKeepsStderrOutOfResponse(t *testing.T) {
|
||||||
|
stdout := "{\"account\":\"+380000000001\"}\n"
|
||||||
|
stderr := "INFO Manager - Routine status\nWARN IncomingMessageHandler - Invalid content! reason\njava.lang.Throwable\n\tat example"
|
||||||
|
|
||||||
|
output, infos, warnings := classifySignalCliOutput(stdout, stderr)
|
||||||
|
|
||||||
|
if output != strings.TrimSpace(stdout) {
|
||||||
|
t.Fatalf("got output %q, wanted %q", output, strings.TrimSpace(stdout))
|
||||||
|
}
|
||||||
|
if !strings.Contains(infos, "INFO Manager - Routine status") {
|
||||||
|
t.Fatalf("INFO stderr was not preserved at INFO severity: %q", infos)
|
||||||
|
}
|
||||||
|
if strings.Contains(warnings, "INFO Manager - Routine status") {
|
||||||
|
t.Fatalf("INFO stderr was promoted to warning severity: %q", warnings)
|
||||||
|
}
|
||||||
|
if !strings.Contains(warnings, "Invalid content! reason") || !strings.Contains(warnings, "java.lang.Throwable") {
|
||||||
|
t.Fatalf("warnings did not preserve stderr: %q", warnings)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -497,6 +498,18 @@ func (s *SignalClient) send(signalCliSendRequest ds.SignalCliSendRequest) (*Send
|
|||||||
textstyleParser := utils.NewTextstyleParser(signalCliSendRequest.Message)
|
textstyleParser := utils.NewTextstyleParser(signalCliSendRequest.Message)
|
||||||
signalCliSendRequest.Message, signalCliTextFormatStrings = textstyleParser.Parse()
|
signalCliSendRequest.Message, signalCliTextFormatStrings = textstyleParser.Parse()
|
||||||
}
|
}
|
||||||
|
if err := validateBodyRanges(signalCliSendRequest.Message,
|
||||||
|
signalCliSendRequest.Mentions,
|
||||||
|
signalCliTextFormatStrings); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if signalCliSendRequest.QuoteMessage != nil {
|
||||||
|
if err := validateMentionRanges(*signalCliSendRequest.QuoteMessage, signalCliSendRequest.QuoteMentions); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid quote mention: %w", err)
|
||||||
|
}
|
||||||
|
} else if len(signalCliSendRequest.QuoteMentions) > 0 {
|
||||||
|
return nil, errors.New("quote mentions require a quote message")
|
||||||
|
}
|
||||||
|
|
||||||
var groupId string = ""
|
var groupId string = ""
|
||||||
if signalCliSendRequest.RecipientType == ds.Group {
|
if signalCliSendRequest.RecipientType == ds.Group {
|
||||||
@ -1053,22 +1066,71 @@ func (s *SignalClient) Receive(number string, timeout int64, ignoreAttachments b
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
out = strings.Trim(out, "\n")
|
return marshalJsonStream(out)
|
||||||
lines := strings.Split(out, "\n")
|
|
||||||
|
|
||||||
jsonStr := "["
|
|
||||||
for i, line := range lines {
|
|
||||||
jsonStr += line
|
|
||||||
if i != (len(lines) - 1) {
|
|
||||||
jsonStr += ","
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jsonStr += "]"
|
|
||||||
|
|
||||||
return jsonStr, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func marshalJsonStream(output string) (string, error) {
|
||||||
|
decoder := json.NewDecoder(strings.NewReader(output))
|
||||||
|
messages := make([]json.RawMessage, 0)
|
||||||
|
for {
|
||||||
|
var message json.RawMessage
|
||||||
|
if err := decoder.Decode(&message); err != nil {
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("invalid JSON from signal-cli receive: %w", err)
|
||||||
|
}
|
||||||
|
messages = append(messages, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := json.Marshal(messages)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to marshal signal-cli receive response: %w", err)
|
||||||
|
}
|
||||||
|
return string(result), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateBodyRanges(message string, mentions []ds.MessageMention, textStyles []string) error {
|
||||||
|
if err := validateMentionRanges(message, mentions); err != nil {
|
||||||
|
return fmt.Errorf("invalid mention: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyLength := int64(utils.UTF16StringLength(message))
|
||||||
|
for i, textStyle := range textStyles {
|
||||||
|
parts := strings.SplitN(textStyle, ":", 3)
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return fmt.Errorf("invalid text style at index %d", i)
|
||||||
|
}
|
||||||
|
start, err := strconv.ParseInt(parts[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid text style start at index %d: %w", i, err)
|
||||||
|
}
|
||||||
|
length, err := strconv.ParseInt(parts[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid text style length at index %d: %w", i, err)
|
||||||
|
}
|
||||||
|
if !bodyRangeWithinBounds(start, length, bodyLength) {
|
||||||
|
return fmt.Errorf("text style at index %d is outside the final UTF-16 message length %d", i, bodyLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateMentionRanges(message string, mentions []ds.MessageMention) error {
|
||||||
|
bodyLength := int64(utils.UTF16StringLength(message))
|
||||||
|
for i, mention := range mentions {
|
||||||
|
if !bodyRangeWithinBounds(mention.Start, mention.Length, bodyLength) {
|
||||||
|
return fmt.Errorf("mention at index %d is outside the final UTF-16 message length %d", i, bodyLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func bodyRangeWithinBounds(start int64, length int64, bodyLength int64) bool {
|
||||||
|
return start >= 0 && length >= 0 && start <= bodyLength && length <= bodyLength-start
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SignalClient) GetReceiveChannel() (chan JsonRpc2ReceivedMessage, string, error) {
|
func (s *SignalClient) GetReceiveChannel() (chan JsonRpc2ReceivedMessage, string, error) {
|
||||||
jsonRpc2Client, err := s.getJsonRpc2Client()
|
jsonRpc2Client, err := s.getJsonRpc2Client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
50
src/client/receive_test.go
Normal file
50
src/client/receive_test.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
ds "github.com/bbernhard/signal-cli-rest-api/datastructs"
|
||||||
|
utils "github.com/bbernhard/signal-cli-rest-api/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMarshalJsonStream(t *testing.T) {
|
||||||
|
output, err := marshalJsonStream("{\"account\":\"one\"}\n{\"account\":\"two\"}\n")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var messages []map[string]string
|
||||||
|
if err := json.Unmarshal([]byte(output), &messages); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(messages) != 2 || messages[0]["account"] != "one" || messages[1]["account"] != "two" {
|
||||||
|
t.Fatalf("unexpected messages: %#v", messages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalJsonStreamRejectsTrailingStderr(t *testing.T) {
|
||||||
|
_, err := marshalJsonStream("{\"account\":\"one\"}\njava.lang.Throwable\n")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected malformed trailing output to be rejected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateBodyRangesUsesFinalUTF16Length(t *testing.T) {
|
||||||
|
parser := utils.NewTextstyleParser("👋 **hello**")
|
||||||
|
message, styles := parser.Parse()
|
||||||
|
mentions := []ds.MessageMention{{Start: 3, Length: 5, Author: "aci"}}
|
||||||
|
|
||||||
|
if err := validateBodyRanges(message, mentions, styles); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateBodyRangesRejectsUTF8ByteOffsets(t *testing.T) {
|
||||||
|
message := "👋 hello"
|
||||||
|
mentions := []ds.MessageMention{{Start: 5, Length: 5, Author: "aci"}}
|
||||||
|
|
||||||
|
if err := validateBodyRanges(message, mentions, nil); err == nil {
|
||||||
|
t.Fatal("expected UTF-8 byte offset to exceed the UTF-16 message length")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -27,7 +27,7 @@ const (
|
|||||||
|
|
||||||
const EscapeCharacter rune = '\\'
|
const EscapeCharacter rune = '\\'
|
||||||
|
|
||||||
func getUtf16StringLength(s string) int {
|
func UTF16StringLength(s string) int {
|
||||||
runes := []rune(s) //turn string to slice
|
runes := []rune(s) //turn string to slice
|
||||||
|
|
||||||
length := 0
|
length := 0
|
||||||
@ -115,13 +115,13 @@ func (l *TextstyleParser) peek() rune {
|
|||||||
|
|
||||||
func (l *TextstyleParser) handleToken(tokenType int, signalCliStylingType string) {
|
func (l *TextstyleParser) handleToken(tokenType int, signalCliStylingType string) {
|
||||||
if l.tokens.Empty() {
|
if l.tokens.Empty() {
|
||||||
l.tokens.Push(TokenState{BeginPos: getUtf16StringLength(l.fullString), Token: tokenType})
|
l.tokens.Push(TokenState{BeginPos: UTF16StringLength(l.fullString), Token: tokenType})
|
||||||
} else {
|
} else {
|
||||||
if l.tokens.Peek().Token == tokenType {
|
if l.tokens.Peek().Token == tokenType {
|
||||||
tokenBeginState := l.tokens.Pop()
|
tokenBeginState := l.tokens.Pop()
|
||||||
l.signalCliFormatStrings = append(l.signalCliFormatStrings, strconv.Itoa(tokenBeginState.BeginPos)+":"+strconv.Itoa(getUtf16StringLength(l.fullString)-tokenBeginState.BeginPos)+":"+signalCliStylingType)
|
l.signalCliFormatStrings = append(l.signalCliFormatStrings, strconv.Itoa(tokenBeginState.BeginPos)+":"+strconv.Itoa(UTF16StringLength(l.fullString)-tokenBeginState.BeginPos)+":"+signalCliStylingType)
|
||||||
} else {
|
} else {
|
||||||
l.tokens.Push(TokenState{BeginPos: getUtf16StringLength(l.fullString), Token: tokenType})
|
l.tokens.Push(TokenState{BeginPos: UTF16StringLength(l.fullString), Token: tokenType})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user