]> git.r.bdr.sh - rbdr/mobius/blame - hotline/transaction.go
Refactoring, cleanup, test backfilling
[rbdr/mobius] / hotline / transaction.go
CommitLineData
6988a057
JH
1package hotline
2
3import (
95159e55 4 "bufio"
9174dbe8 5 "bytes"
6988a057
JH
6 "encoding/binary"
7 "errors"
8 "fmt"
95159e55 9 "io"
a2ef262a 10 "log/slog"
6988a057 11 "math/rand"
9c44621e 12 "slices"
6988a057
JH
13)
14
a2ef262a
JH
15var (
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
6988a057
JH
74)
75
76type Transaction struct {
a2ef262a
JH
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
6988a057 85 Fields []Field
95159e55 86
a2ef262a 87 clientID [2]byte // Internal identifier for target client
95159e55 88 readOffset int // Internal offset to track read progress
6988a057
JH
89}
90
a2ef262a
JH
91type TranType [2]byte
92
93var 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",
143}
6988a057 144
a2ef262a
JH
145func (t TranType) LogValue() slog.Value {
146 return slog.StringValue(tranTypeNames[t])
147}
6988a057 148
a2ef262a
JH
149// NewTransaction creates a new Transaction with the specified type, client ID, and optional fields.
150func NewTransaction(t, clientID [2]byte, fields ...Field) Transaction {
151 transaction := Transaction{
152 Type: t,
153e2eac 153 clientID: clientID,
153e2eac 154 Fields: fields,
6988a057 155 }
a2ef262a
JH
156
157 binary.BigEndian.PutUint32(transaction.ID[:], rand.Uint32())
158
159 return transaction
6988a057
JH
160}
161
a2ef262a
JH
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.
854a92fc 165func (t *Transaction) Write(p []byte) (n int, err error) {
a2ef262a
JH
166 // Make sure we have the minimum number of bytes for a transaction.
167 if len(p) < 22 {
168 return 0, errors.New("buffer too small")
169 }
6988a057 170
a2ef262a
JH
171 // Read the total size field.
172 totalSize := binary.BigEndian.Uint32(p[12:16])
6988a057
JH
173 tranLen := int(20 + totalSize)
174
a2ef262a
JH
175 paramCount := binary.BigEndian.Uint16(p[20:22])
176
177 t.Flags = p[0]
178 t.IsReply = p[1]
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])
95159e55 185
95159e55
JH
186 scanner := bufio.NewScanner(bytes.NewReader(p[22:tranLen]))
187 scanner.Split(fieldScanner)
188
a2ef262a
JH
189 for i := 0; i < int(paramCount); i++ {
190 if !scanner.Scan() {
191 return 0, fmt.Errorf("error scanning field: %w", scanner.Err())
192 }
95159e55
JH
193
194 var field Field
195 if _, err := field.Write(scanner.Bytes()); err != nil {
196 return 0, fmt.Errorf("error reading field: %w", err)
197 }
198 t.Fields = append(t.Fields, field)
6988a057
JH
199 }
200
a2ef262a
JH
201 if err := scanner.Err(); err != nil {
202 return 0, fmt.Errorf("scanner error: %w", err)
203 }
204
205 return len(p), nil
6988a057
JH
206}
207
3178ae58 208const tranHeaderLen = 20 // fixed length of transaction fields before the variable length fields
6988a057 209
3178ae58
JH
210// transactionScanner implements bufio.SplitFunc for parsing incoming byte slices into complete tokens
211func 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
213 if len(data) < 16 {
214 return 0, nil, nil
215 }
6988a057 216
3178ae58 217 totalSize := binary.BigEndian.Uint32(data[12:16])
6988a057 218
3178ae58
JH
219 // tranLen represents the length of bytes that are part of the transaction
220 tranLen := int(tranHeaderLen + totalSize)
221 if tranLen > len(data) {
222 return 0, nil, nil
6988a057
JH
223 }
224
3178ae58 225 return tranLen, data[0:tranLen], nil
6988a057
JH
226}
227
228const minFieldLen = 4
229
230func 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))
234 }
235
236 // A Field consists of:
237 // ID: 2 bytes
238 // Size: 2 bytes
239 // Data: FieldSize number of bytes
240 var fields []Field
241 for i := 0; i < paramCountInt; i++ {
242 if len(buf) < minFieldLen {
243 return []Field{}, fmt.Errorf("invalid field length %v", len(buf))
244 }
245 fieldID := buf[0:2]
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")
251 }
252
253 fields = append(fields, Field{
95159e55
JH
254 ID: [2]byte(fieldID),
255 FieldSize: [2]byte(fieldSize),
6988a057
JH
256 Data: buf[4 : 4+fieldSizeInt],
257 })
258
259 buf = buf[fieldSizeInt+4:]
260 }
261
262 if len(buf) != 0 {
263 return []Field{}, fmt.Errorf("extra field bytes")
264 }
265
266 return fields, nil
267}
268
95159e55
JH
269// Read implements the io.Reader interface for Transaction
270func (t *Transaction) Read(p []byte) (int, error) {
6988a057
JH
271 payloadSize := t.Size()
272
273 fieldCount := make([]byte, 2)
274 binary.BigEndian.PutUint16(fieldCount, uint16(len(t.Fields)))
275
95159e55
JH
276 bbuf := new(bytes.Buffer)
277
6988a057 278 for _, field := range t.Fields {
0ed51327
JH
279 f := field
280 _, err := bbuf.ReadFrom(&f)
95159e55
JH
281 if err != nil {
282 return 0, fmt.Errorf("error reading field: %w", err)
283 }
6988a057
JH
284 }
285
95159e55 286 buf := slices.Concat(
6988a057 287 []byte{t.Flags, t.IsReply},
153e2eac
JH
288 t.Type[:],
289 t.ID[:],
290 t.ErrorCode[:],
6988a057
JH
291 payloadSize,
292 payloadSize, // this is the dataSize field, but seeming the same as totalSize
293 fieldCount,
95159e55
JH
294 bbuf.Bytes(),
295 )
296
297 if t.readOffset >= len(buf) {
298 return 0, io.EOF // All bytes have been read
299 }
300
301 n := copy(p, buf[t.readOffset:])
302 t.readOffset += n
303
304 return n, nil
6988a057
JH
305}
306
307// Size returns the total size of the transaction payload
0a92e50b 308func (t *Transaction) Size() []byte {
6988a057
JH
309 bs := make([]byte, 4)
310
311 fieldSize := 0
312 for _, field := range t.Fields {
313 fieldSize += len(field.Data) + 4
314 }
315
316 binary.BigEndian.PutUint32(bs, uint32(fieldSize+2))
317
318 return bs
319}
320
a2ef262a 321func (t *Transaction) GetField(id [2]byte) Field {
6988a057 322 for _, field := range t.Fields {
a2ef262a 323 if id == field.ID {
6988a057
JH
324 return field
325 }
326 }
327
328 return Field{}
329}