Skip to content

Commit a118929

Browse files
authored
Merge pull request #260 from zeetabit/master
Support attachment base64 and custom filename.
2 parents 352e068 + 98a130d commit a118929

File tree

8 files changed

+388
-71
lines changed

8 files changed

+388
-71
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ COPY src/go.mod /tmp/signal-cli-rest-api-src/
118118
COPY src/go.sum /tmp/signal-cli-rest-api-src/
119119

120120
# build signal-cli-rest-api
121-
RUN cd /tmp/signal-cli-rest-api-src && swag init && go build
121+
RUN cd /tmp/signal-cli-rest-api-src && swag init && go test ./client -v && go build
122122

123123
# build supervisorctl_config_creator
124124
RUN cd /tmp/signal-cli-rest-api-src/scripts && go build -o jsonrpc2-helper

src/api/api.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ type RegisterNumberRequest struct {
6969
}
7070

7171
type UnregisterNumberRequest struct {
72-
DeleteAccount bool `json:"delete_account" example:"false"`
72+
DeleteAccount bool `json:"delete_account" example:"false"`
7373
DeleteLocalData bool `json:"delete_local_data" example:"false"`
7474
}
7575

@@ -88,15 +88,15 @@ type SendMessageV1 struct {
8888
Number string `json:"number"`
8989
Recipients []string `json:"recipients"`
9090
Message string `json:"message"`
91-
Base64Attachment string `json:"base64_attachment"`
91+
Base64Attachment string `json:"base64_attachment" example:"'<BASE64 ENCODED DATA>' OR 'data:<MIME-TYPE>;base64,<BASE64 ENCODED DATA>' OR 'data:<MIME-TYPE>;filename=<FILENAME>;base64,<BASE64 ENCODED DATA>'"`
9292
IsGroup bool `json:"is_group"`
9393
}
9494

9595
type SendMessageV2 struct {
9696
Number string `json:"number"`
9797
Recipients []string `json:"recipients"`
9898
Message string `json:"message"`
99-
Base64Attachments []string `json:"base64_attachments"`
99+
Base64Attachments []string `json:"base64_attachments" example:"<BASE64 ENCODED DATA>,data:<MIME-TYPE>;base64<comma><BASE64 ENCODED DATA>,data:<MIME-TYPE>;filename=<FILENAME>;base64<comma><BASE64 ENCODED DATA>"`
100100
}
101101

102102
type TypingIndicatorRequest struct {

src/client/attachment.go

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package client
2+
3+
import (
4+
"encoding/base64"
5+
"errors"
6+
"os"
7+
"reflect"
8+
"strings"
9+
10+
"github.com/gabriel-vasile/mimetype"
11+
uuid "github.com/gofrs/uuid"
12+
)
13+
14+
type AttachmentEntry struct {
15+
MimeInfo string
16+
FileName string
17+
DirName string
18+
Base64 string
19+
FilePath string
20+
attachmentTmpDir string
21+
}
22+
23+
func NewAttachmentEntry(attachmentData string, attachmentTmpDir string) *AttachmentEntry {
24+
attachmentEntry := AttachmentEntry{
25+
MimeInfo: "",
26+
FileName: "",
27+
DirName: "",
28+
Base64: "",
29+
FilePath: "",
30+
attachmentTmpDir: attachmentTmpDir,
31+
}
32+
33+
attachmentEntry.extractMetaData(attachmentData)
34+
35+
return &attachmentEntry
36+
}
37+
38+
func (attachmentEntry *AttachmentEntry) extractMetaData(attachmentData string) {
39+
base64FlagIndex := strings.LastIndex(attachmentData, "base64,")
40+
41+
if !strings.Contains(attachmentData, "data:") || base64FlagIndex == -1 {
42+
attachmentEntry.Base64 = attachmentData
43+
return
44+
}
45+
46+
attachmentEntry.Base64 = attachmentData[base64FlagIndex+len("base64,"):]
47+
metaDataKeys := map[string]string{
48+
"data:": "MimeInfo",
49+
"filename=": "FileName",
50+
}
51+
52+
for _, metaDataLineItem := range strings.Split(attachmentData[:base64FlagIndex-1], ";") {
53+
for metaDataKey, metaDataFieldName := range metaDataKeys {
54+
flagIndex := strings.Index(metaDataLineItem, metaDataKey)
55+
56+
if flagIndex != 0 {
57+
continue
58+
}
59+
60+
attachmentEntry.setFieldValueByName(metaDataFieldName, metaDataLineItem[len(metaDataKey):])
61+
}
62+
}
63+
}
64+
65+
func (attachmentEntry *AttachmentEntry) storeBase64AsTemporaryFile() error {
66+
if strings.Compare(attachmentEntry.Base64, "") == 0 {
67+
return errors.New("The base64 data does not exist.")
68+
}
69+
70+
dec, err := base64.StdEncoding.DecodeString(attachmentEntry.Base64)
71+
if err != nil {
72+
return err
73+
}
74+
75+
// if no custom filename
76+
if strings.Compare(attachmentEntry.FileName, "") == 0 {
77+
mimeType := mimetype.Detect(dec)
78+
79+
fileNameUuid, err := uuid.NewV4()
80+
if err != nil {
81+
return err
82+
}
83+
attachmentEntry.FileName = fileNameUuid.String() + mimeType.Extension()
84+
}
85+
86+
dirNameUuid, err := uuid.NewV4()
87+
if err != nil {
88+
return err
89+
}
90+
91+
attachmentEntry.DirName = dirNameUuid.String()
92+
dirPath := attachmentEntry.attachmentTmpDir + attachmentEntry.DirName
93+
if err := os.Mkdir(dirPath, os.ModePerm); err != nil {
94+
return err
95+
}
96+
97+
attachmentEntry.FilePath = dirPath + string(os.PathSeparator) + attachmentEntry.FileName
98+
99+
f, err := os.Create(attachmentEntry.FilePath)
100+
if err != nil {
101+
return err
102+
}
103+
defer f.Close()
104+
105+
if _, err := f.Write(dec); err != nil {
106+
attachmentEntry.cleanUp()
107+
return err
108+
}
109+
if err := f.Sync(); err != nil {
110+
attachmentEntry.cleanUp()
111+
return err
112+
}
113+
f.Close()
114+
115+
return nil
116+
}
117+
118+
func (attachmentEntry *AttachmentEntry) cleanUp() {
119+
if strings.Compare(attachmentEntry.FilePath, "") != 0 {
120+
os.Remove(attachmentEntry.FilePath)
121+
}
122+
123+
if strings.Compare(attachmentEntry.DirName, "") != 0 {
124+
dirPath := attachmentEntry.attachmentTmpDir + attachmentEntry.DirName
125+
os.Remove(dirPath)
126+
}
127+
}
128+
129+
func (attachmentEntry *AttachmentEntry) setFieldValueByName(fieldName string, fieldValue string) {
130+
reflectPointer := reflect.ValueOf(attachmentEntry)
131+
reflectStructure := reflectPointer.Elem()
132+
133+
if reflectStructure.Kind() != reflect.Struct {
134+
return
135+
}
136+
137+
field := reflectStructure.FieldByName(fieldName)
138+
if !field.IsValid() {
139+
return
140+
}
141+
142+
if !field.CanSet() || field.Kind() != reflect.String {
143+
return
144+
}
145+
146+
field.SetString(fieldValue)
147+
}
148+
149+
func (attachmentEntry *AttachmentEntry) isWithMetaData() bool {
150+
return len(attachmentEntry.MimeInfo) > 0 && len(attachmentEntry.Base64) > 0
151+
}
152+
153+
func (attachmentEntry *AttachmentEntry) toDataForSignal() string {
154+
if len(attachmentEntry.FilePath) > 0 {
155+
return attachmentEntry.FilePath
156+
}
157+
158+
if !attachmentEntry.isWithMetaData() {
159+
return attachmentEntry.Base64
160+
}
161+
162+
result := "data:" + attachmentEntry.MimeInfo
163+
164+
if len(attachmentEntry.FileName) > 0 {
165+
result = result + ";filename=" + attachmentEntry.FileName
166+
}
167+
168+
return result + ";base64," + attachmentEntry.Base64
169+
}

src/client/attachment_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package client
2+
3+
import (
4+
"flag"
5+
"os"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func Test_Attachment_ExtractMetadata_ShouldPrepareDataFor_toDataForSignal(t *testing.T) {
11+
testCases := []struct {
12+
nameTest string
13+
inputData string
14+
resultIsWithMetaData bool
15+
base64Expected string
16+
base64Valid bool
17+
fileNameExpected string
18+
mimeInfoExpected string
19+
toDataForSignal string
20+
}{
21+
{
22+
"BC base64 - compatibility", "MTIzNDU=", false, "MTIzNDU=", true, "", "", "MTIzNDU=",
23+
},
24+
{
25+
"+base64 -data -filename", "base64,MTIzNDU=", false, "base64,MTIzNDU=", false, "", "", "base64,MTIzNDU=",
26+
},
27+
{
28+
"+base64 +data -filename", "data:someData;base64,MTIzNDU=", true, "MTIzNDU=", true, "", "someData", "data:someData;base64,MTIzNDU=",
29+
},
30+
{
31+
"+base64 -data +filename", "filename=file.name;base64,MTIzNDU=", false, "filename=file.name;base64,MTIzNDU=", false, "", "", "filename=file.name;base64,MTIzNDU=",
32+
},
33+
{
34+
"+base64 +data +filename", "data:someData;filename=file.name;base64,MTIzNDU=", true, "MTIzNDU=", true, "file.name", "someData", "data:someData;filename=file.name;base64,MTIzNDU=",
35+
},
36+
{
37+
"-base64 -data -filename", "INVALIDMTIzNDU=", false, "INVALIDMTIzNDU=", false, "", "", "INVALIDMTIzNDU=",
38+
},
39+
{
40+
"-base64 +data -filename", "data:someData;INVALIDMTIzNDU=", false, "data:someData;INVALIDMTIzNDU=", false, "", "", "data:someData;INVALIDMTIzNDU=",
41+
},
42+
{
43+
"-base64 -data +filename", "filename=file.name;INVALIDMTIzNDU=", false, "filename=file.name;INVALIDMTIzNDU=", false, "", "", "filename=file.name;INVALIDMTIzNDU=",
44+
},
45+
{
46+
"-base64 +data +filename", "data:someData;filename=file.name;INVALIDMTIzNDU=", false, "data:someData;filename=file.name;INVALIDMTIzNDU=", false, "", "", "data:someData;filename=file.name;INVALIDMTIzNDU=",
47+
},
48+
}
49+
50+
attachmentTmp := flag.String("attachment-tmp-dir", string(os.PathSeparator)+"tmp"+string(os.PathSeparator), "Attachment tmp directory")
51+
52+
for _, tt := range testCases {
53+
t.Run(tt.nameTest, func(t *testing.T) {
54+
attachmentEntry := NewAttachmentEntry(tt.inputData, *attachmentTmp)
55+
56+
if attachmentEntry.isWithMetaData() != tt.resultIsWithMetaData {
57+
t.Errorf("isWithMetaData() got \"%v\", want \"%v\"", attachmentEntry.isWithMetaData(), tt.resultIsWithMetaData)
58+
}
59+
60+
if strings.Compare(attachmentEntry.Base64, tt.base64Expected) != 0 {
61+
t.Errorf("Base64 got \"%v\", want \"%v\"", attachmentEntry.Base64, tt.base64Expected)
62+
}
63+
64+
if strings.Compare(attachmentEntry.FileName, tt.fileNameExpected) != 0 {
65+
t.Errorf("FileName got \"%v\", want \"%v\"", attachmentEntry.FileName, tt.fileNameExpected)
66+
}
67+
68+
if strings.Compare(attachmentEntry.MimeInfo, tt.mimeInfoExpected) != 0 {
69+
t.Errorf("MimeInfo got \"%v\", want \"%v\"", attachmentEntry.MimeInfo, tt.mimeInfoExpected)
70+
}
71+
72+
if strings.Compare(attachmentEntry.toDataForSignal(), tt.toDataForSignal) != 0 {
73+
t.Errorf("toDataForSignal() got \"%v\", want \"%v\"", attachmentEntry.toDataForSignal(), tt.toDataForSignal)
74+
}
75+
76+
err := attachmentEntry.storeBase64AsTemporaryFile()
77+
if err != nil && tt.base64Valid {
78+
t.Error("storeBase64AsTemporaryFile error: %w", err)
79+
return
80+
}
81+
82+
info, err := os.Stat(attachmentEntry.FilePath)
83+
if os.IsNotExist(err) && tt.base64Valid {
84+
t.Error("file not exists after storeBase64AsTemporaryFile: %w", err)
85+
return
86+
}
87+
if (info == nil || info.IsDir()) && tt.base64Valid {
88+
t.Error("is not a file by path after storeBase64AsTemporaryFile")
89+
t.Error(attachmentEntry)
90+
return
91+
}
92+
93+
attachmentEntry.cleanUp()
94+
95+
info, err = os.Stat(attachmentEntry.FilePath)
96+
if info != nil && !os.IsNotExist(err) && tt.base64Valid {
97+
t.Error("no info or file exists after cleanUp")
98+
return
99+
}
100+
info, err = os.Stat(*attachmentTmp + attachmentEntry.DirName)
101+
if info != nil && !os.IsNotExist(err) && tt.base64Valid {
102+
t.Error("dir exists after cleanUp")
103+
return
104+
}
105+
})
106+
}
107+
}

0 commit comments

Comments
 (0)