mirror of
https://github.com/bbernhard/signal-cli-rest-api.git
synced 2026-08-12 03:26:01 +00:00
Merge 090432e596fa4e3e898799ebf090701ef188d556 into 5cc379fde09edf0753be8d8a871ca5a3003913f7
This commit is contained in:
commit
8ad04bfd83
@ -4,11 +4,12 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
utils "github.com/bbernhard/signal-cli-rest-api/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
utils "github.com/bbernhard/signal-cli-rest-api/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type CliClient struct {
|
||||
@ -49,6 +50,34 @@ func stripInfoAndWarnMessages(input string) (string, string, string) {
|
||||
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) {
|
||||
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")
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
combinedOutput := stdoutBuffer.String() + stderrBuffer.String()
|
||||
log.Debug("signal-cli output (stdout): ", stdoutBuffer.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 (stderr): ", stderrBuffer.String())
|
||||
strippedOutput, infoMessages, warnMessages := stripInfoAndWarnMessages(combinedOutput)
|
||||
strippedOutput, infoMessages, warnMessages := classifySignalCliOutput(stdoutBuffer.String(), stderrBuffer.String())
|
||||
for _, line := range strings.Split(infoMessages, "\n") {
|
||||
if 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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -497,6 +498,18 @@ func (s *SignalClient) send(signalCliSendRequest ds.SignalCliSendRequest) (*ds.S
|
||||
textstyleParser := utils.NewTextstyleParser(signalCliSendRequest.Message)
|
||||
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 = ""
|
||||
if signalCliSendRequest.RecipientType == ds.Group {
|
||||
@ -1068,22 +1081,71 @@ func (s *SignalClient) Receive(number string, timeout int64, ignoreAttachments b
|
||||
return "", err
|
||||
}
|
||||
|
||||
out = strings.Trim(out, "\n")
|
||||
lines := strings.Split(out, "\n")
|
||||
|
||||
jsonStr := "["
|
||||
for i, line := range lines {
|
||||
jsonStr += line
|
||||
if i != (len(lines) - 1) {
|
||||
jsonStr += ","
|
||||
}
|
||||
}
|
||||
jsonStr += "]"
|
||||
|
||||
return jsonStr, nil
|
||||
return marshalJsonStream(out)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
jsonRpc2Client, err := s.getJsonRpc2Client()
|
||||
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 = '\\'
|
||||
|
||||
func getUtf16StringLength(s string) int {
|
||||
func UTF16StringLength(s string) int {
|
||||
runes := []rune(s) //turn string to slice
|
||||
|
||||
length := 0
|
||||
@ -115,13 +115,13 @@ func (l *TextstyleParser) peek() rune {
|
||||
|
||||
func (l *TextstyleParser) handleToken(tokenType int, signalCliStylingType string) {
|
||||
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 {
|
||||
if l.tokens.Peek().Token == tokenType {
|
||||
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 {
|
||||
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