16 TranError = [2]byte{0x00, 0x00} // 0
17 TranGetMsgs = [2]byte{0x00, 0x65} // 101
18 TranNewMsg = [2]byte{0x00, 0x66} // 102
19 TranOldPostNews = [2]byte{0x00, 0x67} // 103
20 TranServerMsg = [2]byte{0x00, 0x68} // 104
21 TranChatSend = [2]byte{0x00, 0x69} // 105
22 TranChatMsg = [2]byte{0x00, 0x6A} // 106
23 TranLogin = [2]byte{0x00, 0x6B} // 107
24 TranSendInstantMsg = [2]byte{0x00, 0x6C} // 108
25 TranShowAgreement = [2]byte{0x00, 0x6D} // 109
26 TranDisconnectUser = [2]byte{0x00, 0x6E} // 110
27 TranDisconnectMsg = [2]byte{0x00, 0x6F} // 111
28 TranInviteNewChat = [2]byte{0x00, 0x70} // 112
29 TranInviteToChat = [2]byte{0x00, 0x71} // 113
30 TranRejectChatInvite = [2]byte{0x00, 0x72} // 114
31 TranJoinChat = [2]byte{0x00, 0x73} // 115
32 TranLeaveChat = [2]byte{0x00, 0x74} // 116
33 TranNotifyChatChangeUser = [2]byte{0x00, 0x75} // 117
34 TranNotifyChatDeleteUser = [2]byte{0x00, 0x76} // 118
35 TranNotifyChatSubject = [2]byte{0x00, 0x77} // 119
36 TranSetChatSubject = [2]byte{0x00, 0x78} // 120
37 TranAgreed = [2]byte{0x00, 0x79} // 121
38 TranServerBanner = [2]byte{0x00, 0x7A} // 122
39 TranGetFileNameList = [2]byte{0x00, 0xC8} // 200
40 TranDownloadFile = [2]byte{0x00, 0xCA} // 202
41 TranUploadFile = [2]byte{0x00, 0xCB} // 203
42 TranNewFolder = [2]byte{0x00, 0xCD} // 205
43 TranDeleteFile = [2]byte{0x00, 0xCC} // 204
44 TranGetFileInfo = [2]byte{0x00, 0xCE} // 206
45 TranSetFileInfo = [2]byte{0x00, 0xCF} // 207
46 TranMoveFile = [2]byte{0x00, 0xD0} // 208
47 TranMakeFileAlias = [2]byte{0x00, 0xD1} // 209
48 TranDownloadFldr = [2]byte{0x00, 0xD2} // 210
49 TranDownloadInfo = [2]byte{0x00, 0xD3} // 211
50 TranDownloadBanner = [2]byte{0x00, 0xD4} // 212
51 TranUploadFldr = [2]byte{0x00, 0xD5} // 213
52 TranGetUserNameList = [2]byte{0x01, 0x2C} // 300
53 TranNotifyChangeUser = [2]byte{0x01, 0x2D} // 301
54 TranNotifyDeleteUser = [2]byte{0x01, 0x2E} // 302
55 TranGetClientInfoText = [2]byte{0x01, 0x2F} // 303
56 TranSetClientUserInfo = [2]byte{0x01, 0x30} // 304
57 TranListUsers = [2]byte{0x01, 0x5C} // 348
58 TranUpdateUser = [2]byte{0x01, 0x5D} // 349
59 TranNewUser = [2]byte{0x01, 0x5E} // 350
60 TranDeleteUser = [2]byte{0x01, 0x5F} // 351
61 TranGetUser = [2]byte{0x01, 0x60} // 352
62 TranSetUser = [2]byte{0x01, 0x61} // 353
63 TranUserAccess = [2]byte{0x01, 0x62} // 354
64 TranUserBroadcast = [2]byte{0x01, 0x63} // 355
65 TranGetNewsCatNameList = [2]byte{0x01, 0x72} // 370
66 TranGetNewsArtNameList = [2]byte{0x01, 0x73} // 371
67 TranDelNewsItem = [2]byte{0x01, 0x7C} // 380
68 TranNewNewsFldr = [2]byte{0x01, 0x7D} // 381
69 TranNewNewsCat = [2]byte{0x01, 0x7E} // 382
70 TranGetNewsArtData = [2]byte{0x01, 0x90} // 400
71 TranPostNewsArt = [2]byte{0x01, 0x9A} // 410
72 TranDelNewsArt = [2]byte{0x01, 0x9B} // 411
73 TranKeepAlive = [2]byte{0x01, 0xF4} // 500
76 type Transaction struct {
77 Flags byte // Reserved (should be 0)
78 IsReply byte // Request (0) or reply (1)
79 Type TranType // Requested operation (user defined)
80 ID [4]byte // Unique transaction ID (must be != 0)
81 ErrorCode [4]byte // Used in the reply (user defined, 0 = no error)
82 TotalSize [4]byte // Total data size for the fields in this transaction.
83 DataSize [4]byte // Size of data in this transaction part. This allows splitting large transactions into smaller parts.
84 ParamCount [2]byte // Number of the parameters for this transaction
87 clientID [2]byte // Internal identifier for target client
88 readOffset int // Internal offset to track read progress
93 var tranTypeNames = map[TranType]string{
94 TranChatMsg: "Receive Chat",
95 TranNotifyChangeUser: "TranNotifyChangeUser",
96 TranError: "TranError",
97 TranShowAgreement: "TranShowAgreement",
98 TranUserAccess: "TranUserAccess",
99 TranNotifyDeleteUser: "TranNotifyDeleteUser",
100 TranAgreed: "TranAgreed",
101 TranChatSend: "Send Chat",
102 TranDelNewsArt: "TranDelNewsArt",
103 TranDelNewsItem: "TranDelNewsItem",
104 TranDeleteFile: "TranDeleteFile",
105 TranDeleteUser: "TranDeleteUser",
106 TranDisconnectUser: "TranDisconnectUser",
107 TranDownloadFile: "TranDownloadFile",
108 TranDownloadFldr: "TranDownloadFldr",
109 TranGetClientInfoText: "TranGetClientInfoText",
110 TranGetFileInfo: "TranGetFileInfo",
111 TranGetFileNameList: "TranGetFileNameList",
112 TranGetMsgs: "TranGetMsgs",
113 TranGetNewsArtData: "TranGetNewsArtData",
114 TranGetNewsArtNameList: "TranGetNewsArtNameList",
115 TranGetNewsCatNameList: "TranGetNewsCatNameList",
116 TranGetUser: "TranGetUser",
117 TranGetUserNameList: "tranHandleGetUserNameList",
118 TranInviteNewChat: "TranInviteNewChat",
119 TranInviteToChat: "TranInviteToChat",
120 TranJoinChat: "TranJoinChat",
121 TranKeepAlive: "TranKeepAlive",
122 TranLeaveChat: "TranJoinChat",
123 TranListUsers: "TranListUsers",
124 TranMoveFile: "TranMoveFile",
125 TranNewFolder: "TranNewFolder",
126 TranNewNewsCat: "TranNewNewsCat",
127 TranNewNewsFldr: "TranNewNewsFldr",
128 TranNewUser: "TranNewUser",
129 TranUpdateUser: "TranUpdateUser",
130 TranOldPostNews: "TranOldPostNews",
131 TranPostNewsArt: "TranPostNewsArt",
132 TranRejectChatInvite: "TranRejectChatInvite",
133 TranSendInstantMsg: "TranSendInstantMsg",
134 TranSetChatSubject: "TranSetChatSubject",
135 TranMakeFileAlias: "TranMakeFileAlias",
136 TranSetClientUserInfo: "TranSetClientUserInfo",
137 TranSetFileInfo: "TranSetFileInfo",
138 TranSetUser: "TranSetUser",
139 TranUploadFile: "TranUploadFile",
140 TranUploadFldr: "TranUploadFldr",
141 TranUserBroadcast: "TranUserBroadcast",
142 TranDownloadBanner: "TranDownloadBanner",
145 func (t TranType) LogValue() slog.Value {
146 return slog.StringValue(tranTypeNames[t])
149 // NewTransaction creates a new Transaction with the specified type, client ID, and optional fields.
150 func NewTransaction(t, clientID [2]byte, fields ...Field) Transaction {
151 transaction := Transaction{
157 binary.BigEndian.PutUint32(transaction.ID[:], rand.Uint32())
162 // Write implements io.Writer interface for Transaction.
163 // Transactions read from the network are read as complete tokens with a bufio.Scanner, so
164 // the arg p is guaranteed to have the full byte payload of a complete transaction.
165 func (t *Transaction) Write(p []byte) (n int, err error) {
166 // Make sure we have the minimum number of bytes for a transaction.
168 return 0, errors.New("buffer too small")
171 // Read the total size field.
172 totalSize := binary.BigEndian.Uint32(p[12:16])
173 tranLen := int(20 + totalSize)
175 paramCount := binary.BigEndian.Uint16(p[20:22])
179 copy(t.Type[:], p[2:4])
180 copy(t.ID[:], p[4:8])
181 copy(t.ErrorCode[:], p[8:12])
182 copy(t.TotalSize[:], p[12:16])
183 copy(t.DataSize[:], p[16:20])
184 copy(t.ParamCount[:], p[20:22])
186 scanner := bufio.NewScanner(bytes.NewReader(p[22:tranLen]))
187 scanner.Split(fieldScanner)
189 for i := 0; i < int(paramCount); i++ {
191 return 0, fmt.Errorf("error scanning field: %w", scanner.Err())
195 if _, err := field.Write(scanner.Bytes()); err != nil {
196 return 0, fmt.Errorf("error reading field: %w", err)
198 t.Fields = append(t.Fields, field)
201 if err := scanner.Err(); err != nil {
202 return 0, fmt.Errorf("scanner error: %w", err)
208 const tranHeaderLen = 20 // fixed length of transaction fields before the variable length fields
210 // transactionScanner implements bufio.SplitFunc for parsing incoming byte slices into complete tokens
211 func transactionScanner(data []byte, _ bool) (advance int, token []byte, err error) {
212 // The bytes that contain the size of a transaction are from 12:16, so we need at least 16 bytes
217 totalSize := binary.BigEndian.Uint32(data[12:16])
219 // tranLen represents the length of bytes that are part of the transaction
220 tranLen := int(tranHeaderLen + totalSize)
221 if tranLen > len(data) {
225 return tranLen, data[0:tranLen], nil
228 const minFieldLen = 4
230 func ReadFields(paramCount []byte, buf []byte) ([]Field, error) {
231 paramCountInt := int(binary.BigEndian.Uint16(paramCount))
232 if paramCountInt > 0 && len(buf) < minFieldLen {
233 return []Field{}, fmt.Errorf("invalid field length %v", len(buf))
236 // A Field consists of:
239 // Data: FieldSize number of bytes
241 for i := 0; i < paramCountInt; i++ {
242 if len(buf) < minFieldLen {
243 return []Field{}, fmt.Errorf("invalid field length %v", len(buf))
246 fieldSize := buf[2:4]
247 fieldSizeInt := int(binary.BigEndian.Uint16(buf[2:4]))
248 expectedLen := minFieldLen + fieldSizeInt
249 if len(buf) < expectedLen {
250 return []Field{}, fmt.Errorf("field length too short")
253 fields = append(fields, Field{
254 ID: [2]byte(fieldID),
255 FieldSize: [2]byte(fieldSize),
256 Data: buf[4 : 4+fieldSizeInt],
259 buf = buf[fieldSizeInt+4:]
263 return []Field{}, fmt.Errorf("extra field bytes")
269 // Read implements the io.Reader interface for Transaction
270 func (t *Transaction) Read(p []byte) (int, error) {
271 payloadSize := t.Size()
273 fieldCount := make([]byte, 2)
274 binary.BigEndian.PutUint16(fieldCount, uint16(len(t.Fields)))
276 bbuf := new(bytes.Buffer)
278 for _, field := range t.Fields {
280 _, err := bbuf.ReadFrom(&f)
282 return 0, fmt.Errorf("error reading field: %w", err)
286 buf := slices.Concat(
287 []byte{t.Flags, t.IsReply},
292 payloadSize, // this is the dataSize field, but seeming the same as totalSize
297 if t.readOffset >= len(buf) {
298 return 0, io.EOF // All bytes have been read
301 n := copy(p, buf[t.readOffset:])
307 // Size returns the total size of the transaction payload
308 func (t *Transaction) Size() []byte {
309 bs := make([]byte, 4)
312 for _, field := range t.Fields {
313 fieldSize += len(field.Data) + 4
316 binary.BigEndian.PutUint32(bs, uint32(fieldSize+2))
321 func (t *Transaction) GetField(id [2]byte) Field {
322 for _, field := range t.Fields {