23 TranSendInstantMsg = 108
24 TranShowAgreement = 109
25 TranDisconnectUser = 110
26 TranDisconnectMsg = 111 // TODO: implement server initiated friendly disconnect
27 TranInviteNewChat = 112
28 TranInviteToChat = 113
29 TranRejectChatInvite = 114
32 TranNotifyChatChangeUser = 117
33 TranNotifyChatDeleteUser = 118
34 TranNotifyChatSubject = 119
35 TranSetChatSubject = 120
37 TranServerBanner = 122
38 TranGetFileNameList = 200
39 TranDownloadFile = 202
46 TranMakeFileAlias = 209
47 TranDownloadFldr = 210
48 TranDownloadInfo = 211 // TODO: implement file transfer queue
49 TranDownloadBanner = 212
51 TranGetUserNameList = 300
52 TranNotifyChangeUser = 301
53 TranNotifyDeleteUser = 302
54 TranGetClientInfoText = 303
55 TranSetClientUserInfo = 304
63 TranUserBroadcast = 355
64 TranGetNewsCatNameList = 370
65 TranGetNewsArtNameList = 371
69 TranGetNewsArtData = 400
75 type Transaction struct {
76 Flags byte // Reserved (should be 0)
77 IsReply byte // Request (0) or reply (1)
78 Type [2]byte // Requested operation (user defined)
79 ID [4]byte // Unique transaction ID (must be != 0)
80 ErrorCode [4]byte // Used in the reply (user defined, 0 = no error)
81 TotalSize [4]byte // Total data size for the transaction (all parts)
82 DataSize [4]byte // Size of data in this transaction part. This allows splitting large transactions into smaller parts.
83 ParamCount [2]byte // Number of the parameters for this transaction
86 clientID *[]byte // Internal identifier for target client
87 readOffset int // Internal offset to track read progress
90 func NewTransaction(t int, clientID *[]byte, fields ...Field) *Transaction {
91 typeSlice := make([]byte, 2)
92 binary.BigEndian.PutUint16(typeSlice, uint16(t))
94 idSlice := make([]byte, 4)
95 binary.BigEndian.PutUint32(idSlice, rand.Uint32())
99 Type: [2]byte(typeSlice),
100 ID: [4]byte(idSlice),
105 // Write implements io.Writer interface for Transaction
106 func (t *Transaction) Write(p []byte) (n int, err error) {
107 totalSize := binary.BigEndian.Uint32(p[12:16])
109 // the buf may include extra bytes that are not part of the transaction
110 // tranLen represents the length of bytes that are part of the transaction
111 tranLen := int(20 + totalSize)
113 if tranLen > len(p) {
114 return n, errors.New("buflen too small for tranLen")
117 // Create a new scanner for parsing incoming bytes into transaction tokens
118 scanner := bufio.NewScanner(bytes.NewReader(p[22:tranLen]))
119 scanner.Split(fieldScanner)
121 for i := 0; i < int(binary.BigEndian.Uint16(p[20:22])); i++ {
125 if _, err := field.Write(scanner.Bytes()); err != nil {
126 return 0, fmt.Errorf("error reading field: %w", err)
128 t.Fields = append(t.Fields, field)
133 t.Type = [2]byte(p[2:4])
134 t.ID = [4]byte(p[4:8])
135 t.ErrorCode = [4]byte(p[8:12])
136 t.TotalSize = [4]byte(p[12:16])
137 t.DataSize = [4]byte(p[16:20])
138 t.ParamCount = [2]byte(p[20:22])
143 const tranHeaderLen = 20 // fixed length of transaction fields before the variable length fields
145 // transactionScanner implements bufio.SplitFunc for parsing incoming byte slices into complete tokens
146 func transactionScanner(data []byte, _ bool) (advance int, token []byte, err error) {
147 // The bytes that contain the size of a transaction are from 12:16, so we need at least 16 bytes
152 totalSize := binary.BigEndian.Uint32(data[12:16])
154 // tranLen represents the length of bytes that are part of the transaction
155 tranLen := int(tranHeaderLen + totalSize)
156 if tranLen > len(data) {
160 return tranLen, data[0:tranLen], nil
163 const minFieldLen = 4
165 func ReadFields(paramCount []byte, buf []byte) ([]Field, error) {
166 paramCountInt := int(binary.BigEndian.Uint16(paramCount))
167 if paramCountInt > 0 && len(buf) < minFieldLen {
168 return []Field{}, fmt.Errorf("invalid field length %v", len(buf))
171 // A Field consists of:
174 // Data: FieldSize number of bytes
176 for i := 0; i < paramCountInt; i++ {
177 if len(buf) < minFieldLen {
178 return []Field{}, fmt.Errorf("invalid field length %v", len(buf))
181 fieldSize := buf[2:4]
182 fieldSizeInt := int(binary.BigEndian.Uint16(buf[2:4]))
183 expectedLen := minFieldLen + fieldSizeInt
184 if len(buf) < expectedLen {
185 return []Field{}, fmt.Errorf("field length too short")
188 fields = append(fields, Field{
189 ID: [2]byte(fieldID),
190 FieldSize: [2]byte(fieldSize),
191 Data: buf[4 : 4+fieldSizeInt],
194 buf = buf[fieldSizeInt+4:]
198 return []Field{}, fmt.Errorf("extra field bytes")
204 // Read implements the io.Reader interface for Transaction
205 func (t *Transaction) Read(p []byte) (int, error) {
206 payloadSize := t.Size()
208 fieldCount := make([]byte, 2)
209 binary.BigEndian.PutUint16(fieldCount, uint16(len(t.Fields)))
211 bbuf := new(bytes.Buffer)
213 for _, field := range t.Fields {
215 _, err := bbuf.ReadFrom(&f)
217 return 0, fmt.Errorf("error reading field: %w", err)
221 buf := slices.Concat(
222 []byte{t.Flags, t.IsReply},
227 payloadSize, // this is the dataSize field, but seeming the same as totalSize
232 if t.readOffset >= len(buf) {
233 return 0, io.EOF // All bytes have been read
236 n := copy(p, buf[t.readOffset:])
242 // Size returns the total size of the transaction payload
243 func (t *Transaction) Size() []byte {
244 bs := make([]byte, 4)
247 for _, field := range t.Fields {
248 fieldSize += len(field.Data) + 4
251 binary.BigEndian.PutUint32(bs, uint32(fieldSize+2))
256 func (t *Transaction) GetField(id int) Field {
257 for _, field := range t.Fields {
258 if id == int(binary.BigEndian.Uint16(field.ID[:])) {
266 func (t *Transaction) IsError() bool {
267 return t.ErrorCode == [4]byte{0, 0, 0, 1}