package hotline
import (
+ "bufio"
"bytes"
"encoding/binary"
"errors"
"fmt"
- "github.com/jhalter/mobius/concat"
+ "io"
"math/rand"
+ "slices"
)
const (
- tranError = 0
- tranGetMsgs = 101
- tranNewMsg = 102
- tranOldPostNews = 103
- tranServerMsg = 104
- tranChatSend = 105
- tranChatMsg = 106
- tranLogin = 107
- tranSendInstantMsg = 108
- tranShowAgreement = 109
- tranDisconnectUser = 110
- // tranDisconnectMsg = 111 TODO: implement friendly disconnect
- tranInviteNewChat = 112
- tranInviteToChat = 113
- tranRejectChatInvite = 114
- tranJoinChat = 115
- tranLeaveChat = 116
- tranNotifyChatChangeUser = 117
- tranNotifyChatDeleteUser = 118
- tranNotifyChatSubject = 119
- tranSetChatSubject = 120
- tranAgreed = 121
- tranServerBanner = 122
- tranGetFileNameList = 200
- tranDownloadFile = 202
- tranUploadFile = 203
- tranNewFolder = 205
- tranDeleteFile = 204
- tranGetFileInfo = 206
- tranSetFileInfo = 207
- tranMoveFile = 208
- tranMakeFileAlias = 209
- tranDownloadFldr = 210
- // tranDownloadInfo = 211 TODO: implement file transfer queue
- tranDownloadBanner = 212
- tranUploadFldr = 213
- tranGetUserNameList = 300
- tranNotifyChangeUser = 301
- tranNotifyDeleteUser = 302
- tranGetClientInfoText = 303
- tranSetClientUserInfo = 304
- tranListUsers = 348
- tranUpdateUser = 349
- tranNewUser = 350
- tranDeleteUser = 351
- tranGetUser = 352
- tranSetUser = 353
- tranUserAccess = 354
- tranUserBroadcast = 355
- tranGetNewsCatNameList = 370
- tranGetNewsArtNameList = 371
- tranDelNewsItem = 380
- tranNewNewsFldr = 381
- tranNewNewsCat = 382
- tranGetNewsArtData = 400
- tranPostNewsArt = 410
- tranDelNewsArt = 411
- tranKeepAlive = 500
+ TranError = 0
+ TranGetMsgs = 101
+ TranNewMsg = 102
+ TranOldPostNews = 103
+ TranServerMsg = 104
+ TranChatSend = 105
+ TranChatMsg = 106
+ TranLogin = 107
+ TranSendInstantMsg = 108
+ TranShowAgreement = 109
+ TranDisconnectUser = 110
+ TranDisconnectMsg = 111 // TODO: implement server initiated friendly disconnect
+ TranInviteNewChat = 112
+ TranInviteToChat = 113
+ TranRejectChatInvite = 114
+ TranJoinChat = 115
+ TranLeaveChat = 116
+ TranNotifyChatChangeUser = 117
+ TranNotifyChatDeleteUser = 118
+ TranNotifyChatSubject = 119
+ TranSetChatSubject = 120
+ TranAgreed = 121
+ TranServerBanner = 122
+ TranGetFileNameList = 200
+ TranDownloadFile = 202
+ TranUploadFile = 203
+ TranNewFolder = 205
+ TranDeleteFile = 204
+ TranGetFileInfo = 206
+ TranSetFileInfo = 207
+ TranMoveFile = 208
+ TranMakeFileAlias = 209
+ TranDownloadFldr = 210
+ TranDownloadInfo = 211 // TODO: implement file transfer queue
+ TranDownloadBanner = 212
+ TranUploadFldr = 213
+ TranGetUserNameList = 300
+ TranNotifyChangeUser = 301
+ TranNotifyDeleteUser = 302
+ TranGetClientInfoText = 303
+ TranSetClientUserInfo = 304
+ TranListUsers = 348
+ TranUpdateUser = 349
+ TranNewUser = 350
+ TranDeleteUser = 351
+ TranGetUser = 352
+ TranSetUser = 353
+ TranUserAccess = 354
+ TranUserBroadcast = 355
+ TranGetNewsCatNameList = 370
+ TranGetNewsArtNameList = 371
+ TranDelNewsItem = 380
+ TranNewNewsFldr = 381
+ TranNewNewsCat = 382
+ TranGetNewsArtData = 400
+ TranPostNewsArt = 410
+ TranDelNewsArt = 411
+ TranKeepAlive = 500
)
type Transaction struct {
- clientID *[]byte
-
- Flags byte // Reserved (should be 0)
- IsReply byte // Request (0) or reply (1)
- Type []byte // Requested operation (user defined)
- ID []byte // Unique transaction ID (must be != 0)
- ErrorCode []byte // Used in the reply (user defined, 0 = no error)
- TotalSize []byte // Total data size for the transaction (all parts)
- DataSize []byte // Size of data in this transaction part. This allows splitting large transactions into smaller parts.
- ParamCount []byte // Number of the parameters for this transaction
+ Flags byte // Reserved (should be 0)
+ IsReply byte // Request (0) or reply (1)
+ Type [2]byte // Requested operation (user defined)
+ ID [4]byte // Unique transaction ID (must be != 0)
+ ErrorCode [4]byte // Used in the reply (user defined, 0 = no error)
+ TotalSize [4]byte // Total data size for the transaction (all parts)
+ DataSize [4]byte // Size of data in this transaction part. This allows splitting large transactions into smaller parts.
+ ParamCount [2]byte // Number of the parameters for this transaction
Fields []Field
+
+ clientID *[]byte // Internal identifier for target client
+ readOffset int // Internal offset to track read progress
}
func NewTransaction(t int, clientID *[]byte, fields ...Field) *Transaction {
binary.BigEndian.PutUint32(idSlice, rand.Uint32())
return &Transaction{
- clientID: clientID,
- Flags: 0x00,
- IsReply: 0x00,
- Type: typeSlice,
- ID: idSlice,
- ErrorCode: []byte{0, 0, 0, 0},
- Fields: fields,
+ clientID: clientID,
+ Type: [2]byte(typeSlice),
+ ID: [4]byte(idSlice),
+ Fields: fields,
}
}
if tranLen > len(p) {
return n, errors.New("buflen too small for tranLen")
}
- fields, err := ReadFields(p[20:22], p[22:tranLen])
- if err != nil {
- return n, err
+
+ // Create a new scanner for parsing incoming bytes into transaction tokens
+ scanner := bufio.NewScanner(bytes.NewReader(p[22:tranLen]))
+ scanner.Split(fieldScanner)
+
+ for i := 0; i < int(binary.BigEndian.Uint16(p[20:22])); i++ {
+ scanner.Scan()
+
+ var field Field
+ if _, err := field.Write(scanner.Bytes()); err != nil {
+ return 0, fmt.Errorf("error reading field: %w", err)
+ }
+ t.Fields = append(t.Fields, field)
}
t.Flags = p[0]
t.IsReply = p[1]
- t.Type = p[2:4]
- t.ID = p[4:8]
- t.ErrorCode = p[8:12]
- t.TotalSize = p[12:16]
- t.DataSize = p[16:20]
- t.ParamCount = p[20:22]
- t.Fields = fields
+ t.Type = [2]byte(p[2:4])
+ t.ID = [4]byte(p[4:8])
+ t.ErrorCode = [4]byte(p[8:12])
+ t.TotalSize = [4]byte(p[12:16])
+ t.DataSize = [4]byte(p[16:20])
+ t.ParamCount = [2]byte(p[20:22])
return len(p), err
}
}
fields = append(fields, Field{
- ID: fieldID,
- FieldSize: fieldSize,
+ ID: [2]byte(fieldID),
+ FieldSize: [2]byte(fieldSize),
Data: buf[4 : 4+fieldSizeInt],
})
return fields, nil
}
-func (t *Transaction) MarshalBinary() (data []byte, err error) {
+// Read implements the io.Reader interface for Transaction
+func (t *Transaction) Read(p []byte) (int, error) {
payloadSize := t.Size()
fieldCount := make([]byte, 2)
binary.BigEndian.PutUint16(fieldCount, uint16(len(t.Fields)))
- var fieldPayload []byte
+ bbuf := new(bytes.Buffer)
+
for _, field := range t.Fields {
- fieldPayload = append(fieldPayload, field.Payload()...)
+ f := field
+ _, err := bbuf.ReadFrom(&f)
+ if err != nil {
+ return 0, fmt.Errorf("error reading field: %w", err)
+ }
}
- return concat.Slices(
+ buf := slices.Concat(
[]byte{t.Flags, t.IsReply},
- t.Type,
- t.ID,
- t.ErrorCode,
+ t.Type[:],
+ t.ID[:],
+ t.ErrorCode[:],
payloadSize,
payloadSize, // this is the dataSize field, but seeming the same as totalSize
fieldCount,
- fieldPayload,
- ), err
+ bbuf.Bytes(),
+ )
+
+ if t.readOffset >= len(buf) {
+ return 0, io.EOF // All bytes have been read
+ }
+
+ n := copy(p, buf[t.readOffset:])
+ t.readOffset += n
+
+ return n, nil
}
// Size returns the total size of the transaction payload
func (t *Transaction) GetField(id int) Field {
for _, field := range t.Fields {
- if id == int(binary.BigEndian.Uint16(field.ID)) {
+ if id == int(binary.BigEndian.Uint16(field.ID[:])) {
return field
}
}
}
func (t *Transaction) IsError() bool {
- return bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 1})
+ return t.ErrorCode == [4]byte{0, 0, 0, 1}
}